/* ==========================================================================
   SOUND FRAME — Artist Website Premium
   Master Template Stylesheet
   STEP 05: Static DOM / Site Grid / Navigation / Page Transition Baseline

   This file provides STRUCTURAL FOUNDATION only.
   Final Visual Direction (color/typography/treatment) is decided per
   Client Production via CSS Custom Properties below (CONFIGURABLE tokens).
   Do not hardcode client-specific colors, fonts, or imagery references here.
   ========================================================================== */

/* --------------------------------------------------------------------------
   01. RESET
   -------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html, body, h1, h2, h3, h4, p, figure, blockquote, dl, dd, ul, ol { margin: 0; }
ul[class], ol[class] { padding: 0; list-style: none; }
html { -webkit-text-size-adjust: 100%; scroll-padding-top: var(--nav-height); }
body { min-height: 100vh; line-height: var(--lh-body); }
img, picture, video { display: block; max-width: 100%; height: auto; }
button, input, select, textarea { font: inherit; color: inherit; }
a { color: inherit; text-decoration: none; }
button { background: none; border: none; cursor: pointer; }

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

/* --------------------------------------------------------------------------
   02. TOKEN SYSTEM — CONFIGURABLE (Client Production edits values, not structure)
   -------------------------------------------------------------------------- */
:root {
  /* --- Color tokens (structure fixed / values configurable) ---
     STEP 05-A: Master Sample Sample values applied. Source of truth for these
     values = Artist Website Basic §16-A / Artist EPK §Brand Color System.
     Token STRUCTURE (8 roles) unchanged — only values replaced. */
  --color-base: #F2F3F6; /* Pearl Grey — SEOA artist base (bright editorial) */
  --color-surface: #E6E8EE; /* Silver Mist */
  --color-text-primary: #14161C; /* Graphite */
  --color-text-secondary: #565C69;
  --color-accent: #4548F5; /* Electric Iris — SEOA signature */
  --color-accent-2: #8E92B8;
  /* --color-accent-dim — STEP 06-F §4-3: dedicated low-opacity accent fill
     for hover states (Follow links), matching Artist Website Basic's
     --accent-dim role/value so the two products read as the same
     interaction language. CONFIGURABLE per Client Production alongside
     --color-accent. */
  --color-accent-dim: rgba(69, 72, 245, 0.08);
  --color-border: #D3D6DE;
  --color-overlay: rgba(10, 11, 13, 0.92);

  /* --color-overlay-text — Master-level addition (STEP 05-A QA fix, not
     Master Sample-specific). Several structural components (nav overlay, H2/P3
     text-over-media, archive modal close) were hardcoded to var(--color-base)
     assuming a light-theme base. That breaks contrast the moment an artist's
     base token goes dark (as here). This token is the dedicated "light text
     over a permanent dark scrim" role, independent of the site's own base/text
     tokens, so both light- and dark-themed Client Productions render correctly.
     Applies to ALL future artists, not just this sample — see README. */
  --color-overlay-text: #F5F5F4;

  /* --- Typography tokens ---
     STEP 05-A: Primary/Display/Body = Pretendard Variable (Master Sample Basic/EPK
     standard). Secondary/Editorial serif kept as a limited, opt-in accent
     token only (not applied to headings by default). */
  --font-display: 'Pretendard Variable', -apple-system, BlinkMacSystemFont, 'Malgun Gothic', 'Segoe UI', sans-serif;
  --font-body: 'Pretendard Variable', -apple-system, BlinkMacSystemFont, 'Malgun Gothic', 'Segoe UI', Helvetica, Arial, sans-serif;
  --font-accent: Georgia, 'Noto Serif KR', serif;

  --scale-display: clamp(3rem, 5vw, 5.6rem); /* STEP 07 §7-2: mobile floor raised 2.6rem→3rem to read closer to Basic's mobile artist-name size; desktop/ultra-wide top end unchanged from STEP 06-D. */
  --scale-h2: clamp(1.6rem, 3vw, 2.6rem);
  --scale-h3: clamp(1.2rem, 2vw, 1.6rem);
  --scale-body: 1rem;
  --scale-small: 0.875rem;

  --lh-display: 1.05;
  --lh-heading: 1.15;
  --lh-body: 1.6;
  --tracking-label: 0.02em;

  /* --- Spacing / Density tokens (base = D2 Balanced) ---
     STEP 07 Master Sample Web/Mobile Final Patch §03: section-to-section
     rhythm expanded to Basic's spacing sense (Basic uses
     padding-block: clamp(72px,10vw,140px) per section = ~144-280px
     between two sections). Premium was 128px flat; raised to 176px
     desktop / 144px tablet / 112px mobile — still its own scale
     (Premium's density variants d1/d3 still apply on top), but no
     longer reads tighter than Basic. */
  --space-unit: 8px;
  --section-gap: calc(var(--space-unit) * 22);   /* 176px */
  --block-gap: calc(var(--space-unit) * 6);      /* 48px */
  --inline-gap: calc(var(--space-unit) * 2);     /* 16px */
  --grid-occupancy: 0.86;

  /* --- Container / Width Tier tokens --- */
  --page-margin: clamp(20px, 5vw, 72px);
  --width-reading: 720px;
  --width-content: 1280px;
  --width-wide: 1600px;
  /* WIDTH_FULL = 100vw, handled per-component */

  /* --- Structural tokens --- */
  --nav-height: 72px;
  --radius: 0px; /* Master default: no forced rounding; CONFIGURABLE per client */
  --z-nav: 100;
  --z-overlay: 200;
  --z-transition: 300;
  --z-modal: 400;

  /* --- Motion tokens (Level default = M2 Balanced) --- */
  --motion-fast: 220ms;
  --motion-base: 420ms;
  --motion-slow: 640ms;
  /* --motion-nav — STEP 06-D Fix Patch §3-7/§06-5: dedicated, deliberately
     fast token for cross-page navigation only (page-transition layer).
     Decoupled from --motion-slow (which still drives slider/reveal) so
     internal link clicks never feel delayed. Kept out of the M1/M3 motion
     level overrides below on purpose — nav speed is a UX-speed fix, not a
     "more/less animation" preference. */
  /* STEP 06-E Refinement Patch §6-1: dedicated fast token for cross-page
     nav only, tightened further (was 150ms) to sit inside the requested
     80–140ms outgoing range; JS clamp below caps it regardless. */
  --motion-nav: 120ms;
  --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
}

/* STEP 06-E Refinement Patch §02: section rhythm gets its own scale per
   breakpoint instead of one fixed value at every width — desktop stays in
   the 112–160px band (density variants already vary within that), tablet
   drops to 88–120px, mobile to 64–88px. Rhythm variation (big→small→big)
   is applied selectively via .section--tight / .section--loose utilities
   in specific sections below, not by changing this base scale per-section. */
@media (max-width: 900px) {
  :root { --section-gap: calc(var(--space-unit) * 18); } /* 144px */
}
@media (max-width: 600px) {
  :root { --section-gap: calc(var(--space-unit) * 14); } /* 112px */
}

/* Density variants — applied at <body data-density="d1|d2|d3"> */
body[data-density="d1"] {
  --section-gap: calc(var(--space-unit) * 20);
  --block-gap: calc(var(--space-unit) * 8);
  --grid-occupancy: 0.72;
}
body[data-density="d3"] {
  --section-gap: calc(var(--space-unit) * 10);
  --block-gap: calc(var(--space-unit) * 4);
  --grid-occupancy: 0.96;
}
/* Mobile forced downgrade: D3 never survives to mobile (LOCK rule STEP 03 #08) */
@media (max-width: 767px) {
  body[data-density="d3"] {
    --section-gap: calc(var(--space-unit) * 12);
    --block-gap: calc(var(--space-unit) * 5);
    --grid-occupancy: 0.88;
  }
}

/* Motion level variants — applied at <body data-motion="m1|m2|m3"> */
body[data-motion="m1"] { --motion-base: 0ms; --motion-slow: 0ms; }
body[data-motion="m3"] { --motion-base: 520ms; --motion-slow: 780ms; }

/* --------------------------------------------------------------------------
   03. BASE TYPOGRAPHY
   -------------------------------------------------------------------------- */
body {
  font-family: var(--font-body);
  font-size: var(--scale-body);
  color: var(--color-text-primary);
  background: var(--color-base);
}
h1, .h1 { font-family: var(--font-display); font-size: var(--scale-display); line-height: var(--lh-display); }
h2, .h2 { font-family: var(--font-display); font-size: var(--scale-h2); line-height: var(--lh-heading); }
h3, .h3 { font-family: var(--font-display); font-size: var(--scale-h3); line-height: var(--lh-heading); }
/* STEP 06-E Refinement Patch §3-5/§4-1/§5-1: eyebrow/section-label color
   moved from secondary to primary text (was too dim), size bumped slightly,
   weight/tracking increased so it reads distinct from the section title
   below it. The accent tick mark (::before) is kept as-is so the signature
   color doesn't disappear once the label itself goes to primary/white. */
.eyebrow {
  font-size: clamp(0.85rem, 1vw + 0.6rem, 1rem);
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--color-text-primary);
  position: relative; padding-left: 16px; display: inline-block;
}
.eyebrow::before {
  /* STEP 06-C §2-1: signature-color accent mark, applied to every eyebrow
     label sitewide instead of leaving the accent confined to nav state. */
  content: ""; position: absolute; left: 0; top: 50%; translate: 0 -50%;
  width: 9px; height: 1px; background: var(--color-accent);
}
.hero-copy .eyebrow { color: var(--color-accent); } /* STEP 06-C §3-1 */
.body-copy { max-width: var(--width-reading); color: var(--color-text-secondary); }
body { word-break: keep-all; overflow-wrap: break-word; } /* STEP 06-E §07: global KR line-break safety */

/* STEP 06-E Refinement Patch §3-1: Hero copy line-break rule.
   Desktop/large-desktop/tablet: explicit break after "기반의", second line
   forced onto one line (nowrap). Mobile: nowrap released, second line wraps
   naturally with the viewport. word-break/overflow-wrap prevent any
   mid-word break at any width. */
.hero-desc {
  word-break: keep-all;
  overflow-wrap: break-word;
  line-break: strict;
}
.hero-desc-line2 { white-space: nowrap; }
@media (max-width: 600px) {
  .hero-desc-line2 { white-space: normal; }
}

/* --------------------------------------------------------------------------
   04. LAYOUT / WIDTH TIER SYSTEM
   -------------------------------------------------------------------------- */
.tier-reading { max-width: var(--width-reading); margin-inline: auto; }
.tier-content { max-width: var(--width-content); margin-inline: auto; padding-inline: var(--page-margin); }
.tier-wide    { max-width: var(--width-wide);    margin-inline: auto; padding-inline: var(--page-margin); }
.tier-full    { width: 100%; }

.section { padding-block: calc(var(--section-gap) / 2); }
.section + .section { border-top: none; }

/* STEP 07 §4-1/§06: compensates for .site-header now being position:fixed
   (removed from flow) on every page. HOME's hero is designed to bleed
   under the transparent header, so it cancels the padding back to 0. */
main { padding-top: var(--nav-height); }
body[data-page="home"] main { padding-top: 0; }

/* --------------------------------------------------------------------------
   05. ACCESSIBILITY STRUCTURE
   -------------------------------------------------------------------------- */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--color-accent);
  color: var(--color-base);
  padding: var(--inline-gap) var(--block-gap);
  z-index: 1000;
}
.skip-link:focus {
  left: var(--page-margin);
  top: var(--inline-gap);
}
:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 3px;
}
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   06. GLOBAL HEADER / NAVIGATION — N1 / N2 / N3 share one semantic DOM
   -------------------------------------------------------------------------- */
/* STEP 07 §4-1: header now fixed (was sticky) so it can sit transparently
   over the HOME hero and pick up a solid surface once the page scrolls —
   Basic's header behavior. Every other page keeps the header solid from
   the start (see body[data-page="home"] override below) since only HOME
   has a full-bleed hero under it; PROJECTS/ARCHIVE intros are plain
   text/content and would lose contrast under a transparent bar. Because
   the header is now out of normal flow on every page, `main` picks up a
   compensating top padding equal to --nav-height (see LAYOUT section) —
   HOME cancels that padding so its hero can still bleed to the true top. */
.site-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: var(--z-nav);
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: var(--nav-height);
  padding-inline: var(--page-margin);
  background: var(--color-base);
  border-bottom: 1px solid var(--color-border);
  transition: background var(--motion-base) var(--ease-standard),
              border-color var(--motion-base) var(--ease-standard);
}
/* HOME only: transparent-over-hero until the user scrolls, then translucent
   solid — toggled by initHeaderScroll() in script.js adding/removing
   .is-scrolled. JS-off fallback: header stays solid from the start (safe,
   readable). Fix 02 §3-1: was a fully opaque var(--color-base) fill, which
   read as a hard cut against the hero; matched to Basic's exact
   transparent → translucent treatment instead — rgba(10,11,13,0.86) (same
   value as --color-base, alpha'd) + backdrop-filter blur so the hero still
   reads faintly through the bar instead of a flat opaque block. */
/* Master Sample root-cause patch: guard the transparent state with :not() +
   !important so it always wins regardless of rule order, and lower the
   scrolled fill from 0.86 (read as near-solid black) into the 0.58–0.68
   range so the hero still shows faintly through the blurred bar. */
body[data-page="home"] .site-header:not(.is-scrolled) {
  background: transparent !important;
  border-bottom-color: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
body[data-page="home"] .site-header.is-scrolled {
  background: rgba(242, 243, 246, 0.78);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  border-bottom-color: var(--color-border);
}
.brand { font-family: var(--font-display); font-size: 1.1rem; letter-spacing: var(--tracking-label); }
.nav-groups { display: flex; align-items: center; gap: calc(var(--space-unit) * 5); }
.nav-primary, .nav-utility { display: flex; align-items: center; gap: var(--block-gap); list-style: none; }
.nav-primary a, .nav-utility a { font-size: var(--scale-small); }
.nav-primary a[aria-current="page"] { color: var(--color-accent); border-bottom: 1px solid var(--color-accent); }
.nav-utility { padding-left: var(--block-gap); border-left: 1px solid var(--color-border); opacity: 0.85; }
.nav-trigger { display: none; }

/* N1 Minimal Top — default: header as above, no further change */
body[data-nav="n1"] .site-header { /* default state */ }

/* N2 Editorial Edge — nav pinned to page edge, brand emphasized */
body[data-nav="n2"] .site-header {
  border-bottom: none;
  padding-block: var(--inline-gap);
}
body[data-nav="n2"] .nav-groups { flex-direction: column; align-items: flex-end; gap: var(--inline-gap); }

/* N3 Overlay — trigger visible, primary/utility groups hidden until opened */
body[data-nav="n3"] .nav-groups { display: none; }
body[data-nav="n3"] .nav-trigger { display: inline-flex; }

/* STEP 07 §7-1: mobile menu rebuilt to Basic's hamburger drawer pattern —
   bars-icon button that morphs to an X, drawer opens DOWN from the header
   (max-height 0→auto) instead of covering the full screen. Basic has no
   separate close button; the same trigger closes it, so .nav-overlay-close
   is removed from markup (initNavOverlay() already guards for its absence). */
.nav-trigger {
  align-items: center;
  gap: 8px;
}
.nav-trigger-bars { display: flex; flex-direction: column; gap: 5px; width: 18px; }
.nav-trigger-bars span { display: block; width: 100%; height: 1px; background: currentColor; transition: transform var(--motion-fast) var(--ease-standard); }
.nav-trigger-label { font-size: var(--scale-small); letter-spacing: var(--tracking-label); text-transform: uppercase; }
.nav-trigger[aria-expanded="true"] .nav-trigger-bars span:first-child { transform: translateY(3px) rotate(45deg); }
.nav-trigger[aria-expanded="true"] .nav-trigger-bars span:last-child { transform: translateY(-3px) rotate(-45deg); }

/* Fix 02 §6-1: background raised one step off pure --color-base to
   --color-surface (Basic's drawer uses --bg-elevated, its own step above
   base) so the open drawer reads as a distinct elevated panel instead of
   flush with the page behind it — same relationship Basic uses. */
.nav-overlay {
  position: fixed;
  top: var(--nav-height); left: 0; right: 0;
  z-index: var(--z-overlay);
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  max-height: 0;
  overflow: hidden;
  padding-inline: var(--page-margin);
  transition: max-height var(--motion-base) var(--ease-standard);
}
.nav-overlay[data-open="true"] { max-height: 420px; }
.nav-overlay .nav-primary, .nav-overlay .nav-utility {
  flex-direction: column;
  align-items: flex-start;
  gap: 0;
  border: none;
  padding: 0;
  padding-block: 12px;
}
.nav-overlay .nav-utility { border-top: 1px solid var(--color-border); border-left: none; opacity: 1; padding-left: 0; }
/* Fix 02 §6-1: per-item divider added between links (Basic's
   .utility-nav li border-bottom pattern) — each row now reads as a
   distinct list item on open, instead of one unbroken block of links.
   Premium's nav-overlay links are direct <a> children of <nav> (no <li>
   wrapper in this markup), so the divider is applied to the <a> itself. */
.nav-overlay .nav-primary a,
.nav-overlay .nav-utility a { border-bottom: 1px solid var(--color-border); }
.nav-overlay .nav-primary a:last-child,
.nav-overlay .nav-utility a:last-child { border-bottom: none; }
.nav-overlay a {
  display: flex; align-items: center; min-height: 44px; width: 100%;
  font-size: var(--scale-body); font-family: var(--font-body);
}

/* --------------------------------------------------------------------------
   07. MOBILE NAVIGATION — shared drawer core across N1/N2/N3 (STEP 05 #33)
   -------------------------------------------------------------------------- */
@media (max-width: 900px) {
  .nav-groups { display: none; }
  .nav-trigger { display: flex; } /* overrides both the n1/n2 default (none) and n3 (already inline-flex) — trigger always visible on mobile regardless of nav variant */
  body[data-nav="n1"] .nav-overlay,
  body[data-nav="n2"] .nav-overlay,
  body[data-nav="n3"] .nav-overlay { display: block; }
}

/* --------------------------------------------------------------------------
   08. HERO — H1 / H2 / H3 share one DOM, presentation via data-hero
   -------------------------------------------------------------------------- */
.hero {
  display: grid;
  gap: var(--block-gap);
  padding-block: calc(var(--section-gap) * 0.7);
  padding-inline: var(--page-margin);
}
.hero-copy { display: flex; flex-direction: column; gap: var(--inline-gap); }
.hero-media { position: relative; overflow: hidden; }
.hero-media img, .hero-media video { width: 100%; height: 100%; object-fit: cover; }
.hero-meta { display: flex; gap: var(--block-gap); font-size: var(--scale-small); color: var(--color-text-secondary); }
.hero-motif { pointer-events: none; }

/* H1 Editorial — copy + media balanced, two-column on desktop.
   STEP 06 Master Sample Patch: same H1 structure, reinforced presence —
   min-height + media bleeds to the viewport edge instead of sitting
   inside page-margin on both sides, so it reads as a hero, not a card. */
body[data-hero="h1"] .hero {
  grid-template-columns: 1.1fr 0.9fr;
  min-height: 85svh;
  padding-inline: 0;
  padding-block: calc(var(--section-gap) * 0.55);
}
body[data-hero="h1"] .hero-copy { padding-inline-start: var(--page-margin); justify-content: center; }
body[data-hero="h1"][data-hero-align="media-right"] .hero-media { order: 2; padding-inline-end: 0; }
body[data-hero="h1"][data-hero-align="media-left"] .hero-media { order: -1; padding-inline-start: 0; }
body[data-hero="h1"][data-hero-align="media-right"] .hero-copy { padding-inline-end: var(--block-gap); }
body[data-hero="h1"][data-hero-align="media-left"] .hero-copy { padding-inline-start: var(--block-gap); order: 2; }
body[data-hero="h1"] .hero-media { aspect-ratio: auto; height: 100%; align-self: stretch; }

/* H2 Image-Dominant — full-bleed media, copy overlaid at base */
body[data-hero="h2"] .hero {
  grid-template-columns: 1fr;
  padding-inline: 0;
  min-height: 88svh;
  position: relative;
}
body[data-hero="h2"] .hero-media { position: absolute; inset: 0; z-index: 0; aspect-ratio: auto; height: 100%; }
/* STEP 06-D Fix Patch §3-1: object-position is CONFIGURABLE per client via
   --hero-focal (defaults to a safe center-weighted crop so the subject
   never disappears off-frame at very wide or very narrow viewports). */
body[data-hero="h2"] .hero-media img { object-position: var(--hero-focal, 50% 22%); }
/* Fix 02 §3-2: top/bottom padding was symmetric (--section-gap both sides),
   which made the copy box tall enough to read as floating mid-hero instead
   of anchored low. Top padding alone reduced (box stays bottom-anchored via
   align-self:flex-end, so a shorter box = its top edge sits closer to the
   bottom = text reads lower, more of Master Sample's hero image visible above
   it) — bottom padding unchanged, so text never nears the fold. Desktop/
   tablet only; mobile has its own override below (max-width:600px). */
/* Master Sample root-cause patch §03: align-self:flex-end + padding-coefficient
   tuning wasn't producing a visually confirmable position change. Switched
   to an explicit bottom-anchored absolute overlay so the copy block's
   position is set directly by `bottom`, independent of content height or
   grid alignment quirks — a real, checkable distance from the hero's
   bottom edge instead of an indirect padding trick. */
body[data-hero="h2"] .hero-copy {
  position: absolute;
  left: 0; right: 0;
  bottom: clamp(72px, 10vh, 128px);
  z-index: 1;
  padding-inline: var(--page-margin);
  padding-block: 0;
  color: var(--color-overlay-text);
}
/* Full-stage scrim (Basic .hero-scrim pattern) instead of a copy-only
   pseudo-element: covers the ENTIRE hero, not just the text block, so the
   bottom edge of the image never reads as an unscrimmed "cut" regardless
   of copy length/breakpoint. A soft top gradient keeps the top of the
   image legible without darkening it, and the bottom stop is intentionally
   near-solid so text always has guaranteed contrast. */
body[data-hero="h2"] .hero-media::after {
  content: "";
  position: absolute; inset: 0;
  background:
    linear-gradient(to top, rgba(0,0,0,0.78) 0%, rgba(0,0,0,0.38) 38%, rgba(0,0,0,0.05) 68%, rgba(0,0,0,0) 100%);
  pointer-events: none;
}

/* H3 Typographic — copy-only, no media required */
body[data-hero="h3"] .hero { grid-template-columns: 1fr; max-width: var(--width-content); margin-inline: auto; }
body[data-hero="h3"] .hero-media { display: none; }
body[data-hero="h3"] .hero-copy h1 { font-size: clamp(3rem, 9vw, 8rem); }

/* Master Sample root-cause patch §05: rebuilt on the same absolute+bottom
   overlay mechanic as the desktop fix above (not a padding-coefficient
   tweak). Copy block is explicitly anchored near the hero's bottom edge,
   given Basic's full-width / --edge-style horizontal padding instead of
   the old fixed --page-margin box, and given Basic's smaller mobile type
   scale so the block doesn't visually crowd the hero image. */
@media (max-width: 600px) {
  body[data-hero="h2"] .hero-copy {
    left: 0; right: 0;
    bottom: clamp(28px, 6vh, 52px);
    padding-inline: clamp(20px, 6vw, 28px);
    width: 100%;
    max-width: none;
  }
  body[data-hero="h2"] .hero-copy h1 {
    /* 텍스트 정보 영역 폰트 크기 축소 요청: 기존 40–58px → 36–52px로 소폭 축소 */
    font-size: clamp(36px, 12vw, 52px);
  }
  body[data-hero="h2"] .hero-media::after {
    background:
      linear-gradient(to top, rgba(0,0,0,0.82) 0%, rgba(0,0,0,0.3) 32%, rgba(0,0,0,0.03) 60%, rgba(0,0,0,0) 100%);
  }
}

@media (max-width: 900px) {
  .hero { grid-template-columns: 1fr !important; }
  body[data-hero="h1"] .hero { min-height: auto; padding-inline: var(--page-margin); }
  body[data-hero="h1"] .hero-copy { padding-inline: 0; }
  body[data-hero="h1"] .hero-media,
  body[data-hero="h1"][data-hero-align="media-right"] .hero-media,
  body[data-hero="h1"][data-hero-align="media-left"] .hero-media {
    order: -1; aspect-ratio: 4/3; height: auto; align-self: auto;
    margin-inline: calc(var(--page-margin) * -1); width: auto;
  }
}

/* --------------------------------------------------------------------------
   09. HOME SECTIONS — Featured Project + Listen / Contact
   STEP 06-C §3-2: Current Signal section removed entirely (superseded by
   Featured Project + Listen below — no more Signal → Deep Feature split).
   -------------------------------------------------------------------------- */
/* Featured Project — STEP 06 Patch: promoted to tier-wide with a taller,
   dominant media column so it reads as a deep feature, not a repeat of
   Current Signal's compact strip. */
.featured-project {
  display: grid;
  grid-template-columns: 1.3fr 1fr;
  gap: var(--section-gap);
  align-items: center;
}
.featured-project .fp-media { aspect-ratio: 4 / 5; overflow: hidden; }
.featured-project .fp-media img, .featured-project .fp-media video { width: 100%; height: 100%; object-fit: cover; }
.featured-project .fp-copy h3 { font-size: var(--scale-h2); }

@media (max-width: 900px) {
  .featured-project { grid-template-columns: 1fr; }
}

/* Optional module wrapper — only ever present in DOM if actually used (STEP 05 #11) */
.optional-module { padding-block: calc(var(--block-gap) * 1.2); }

/* --------------------------------------------------------------------------
   10. PROJECTS — shared semantic structure across P1 / P2 / P3
   -------------------------------------------------------------------------- */
.project-sequence { display: flex; flex-direction: column; gap: var(--section-gap); }
/* STEP 06-C §4-2: cover column fixed to a modest square (was 1fr 1fr, 4:5) —
   covers are square source art; sizing them down and squaring the crop
   frees width for project text so info no longer reads "붙어" (cramped). */
.project-item { display: grid; grid-template-columns: minmax(220px, 360px) 1fr; gap: var(--section-gap); align-items: start; }
.project-media { aspect-ratio: 1 / 1; overflow: hidden; }
.project-media img, .project-media video { width: 100%; height: 100%; object-fit: cover; }
.project-header { display: flex; flex-direction: column; gap: calc(var(--inline-gap) / 2); }
.project-meta { display: flex; gap: var(--inline-gap); font-size: var(--scale-small); color: var(--color-text-secondary); }
.project-context { max-width: min(var(--width-reading), 62ch); color: var(--color-text-secondary); }
.project-credits { font-size: var(--scale-small); color: var(--color-text-secondary); margin-top: calc(var(--inline-gap) / 2); }
/* STEP 06-E §4-2: project-body rhythm tightened (was --block-gap/48px
   uniform, read as too much air between metadata/context/player) — same
   elements now sit in a stepped relationship, closer within one project,
   while .project-sequence's own --section-gap keeps project-to-project
   spacing wide. */
.project-body { display: flex; flex-direction: column; gap: calc(var(--inline-gap) * 1.25); }
.project-links { display: flex; gap: var(--inline-gap); flex-wrap: wrap; }

/* P1 Editorial Sequence — alternating flip via nth-child, override via data-layout on item */
body[data-project-layout="p1"] .project-item:nth-child(even) .project-media { order: 2; }
.project-item[data-layout="reverse"] .project-media { order: 2; }
.project-item[data-layout="forward"] .project-media { order: -1; }

/* P2 Editorial Index — compact, expandable */
body[data-project-layout="p2"] .project-item { grid-template-columns: 220px 1fr; align-items: center; }
body[data-project-layout="p2"] .project-media { aspect-ratio: 1/1; }
body[data-project-layout="p2"] .project-details[hidden] { display: none; }
body[data-project-layout="p2"] .project-expand-toggle { display: inline-flex; }
.project-expand-toggle { display: none; font-size: var(--scale-small); }

/* P3 Campaign Panels — full-bleed, large-scale, high-impact */
body[data-project-layout="p3"] .project-item {
  grid-template-columns: 1fr;
  min-height: 80vh;
  position: relative;
}
body[data-project-layout="p3"] .project-media { position: absolute; inset: 0; aspect-ratio: auto; height: 100%; z-index: 0; }
body[data-project-layout="p3"] .project-header,
body[data-project-layout="p3"] .project-meta,
body[data-project-layout="p3"] .project-context,
body[data-project-layout="p3"] .project-credits,
body[data-project-layout="p3"] .project-links {
  position: relative; z-index: 1; color: var(--color-overlay-text);
}
body[data-project-layout="p3"] .project-item::before {
  content: ""; position: absolute; inset: 0; z-index: 0;
  background: linear-gradient(to top, rgba(0,0,0,0.6), rgba(0,0,0,0.05));
}

/* Per-item rhythm modifiers — STEP 06 Patch §04: same P1/P2/P3 semantic
   DOM, visual weight varied via media ratio / column split / ordering
   instead of a new layout system, so no two sequence items repeat
   identically (Sample-B / Sample-C / release-index differentiation). */
/* STEP 06-C §4-2: rhythm now comes from column-width variation only —
   covers stay uniformly square (no more 3/4, 3/5 crops) per the patch's
   "정사각형 이미지로 변경" requirement. */
.project-item--wide { grid-template-columns: minmax(220px, 320px) 1fr; }
/* STEP 06-E §4-3: Sample-C cover reduced further (was minmax(200px,300px))
   and expressed as a % cap so text/player column reliably lands in the
   requested 58–64% range while the cover stays in 36–42% — square ratio
   unchanged, image/info no longer forced to an arbitrary 50:50 split. */
.project-item--tall { grid-template-columns: minmax(180px, 38%) 1fr; }
.project-item--compact-media .project-media { aspect-ratio: 1 / 1; }
/* STEP 06-D §4-1: embedded player width reduced for Sample-A / Glass
   Room so the Spotify embed no longer stretches "가로로 길게" — same
   capped-width pattern already used for HOME's compact Listen player. */
#project-featured .player-container,
#project-02 .player-container { max-width: 420px; }
/* STEP 06-D §4-1: release date split onto its own line, separate from the
   context sentence (clearer 2-line hierarchy: context, then date). */
.project-release-date { font-size: var(--scale-small); color: var(--color-text-secondary); margin-top: -4px; }

/* Featured project — lives outside .project-sequence in its own tier-wide
   wrapper so it reads as WIDE/dominant, distinct from the sequence below */
/* STEP 06-F §05-1: cover column widened slightly (was 260/420) so LOW
   SIGNAL reads a touch larger than the sequence below it, per patch
   request; align-items:start already keeps the info column top-aligned
   to its own content height rather than stretching to cover height. */
/* STEP 07 §5-1: cover column widened slightly further (was 460px cap) so
   Sample-A's cover can read a touch larger, per patch request. Info
   column's internal gap now matches the sequence items below it
   (calc(--inline-gap*1.25), was --block-gap/48px) so Sample-A's info
   rhythm doesn't read differently from Sample-B / Sample-C etc. —
   align-items:start (unchanged) already keeps it top-aligned rather than
   stretched to cover height. */
/* Fix 02 §4-1: outer column gap matched .project-sequence's --section-gap
   (176px after STEP 07's global rhythm bump) — between a square cover and
   its own info column that reads far too loose/disconnected, unlike the
   sequence items below it. Tightened to --block-gap so cover and info read
   as one unit; align-items:start (unchanged) keeps info top-aligned to the
   cover, matching Sample-B / Sample-C. */
.project-featured-wrap { display: grid; grid-template-columns: minmax(280px, 500px) 1fr; gap: var(--block-gap); align-items: start; }
.project-featured-wrap .project-media { aspect-ratio: 1 / 1; }
.project-featured-wrap .project-body { display: flex; flex-direction: column; gap: calc(var(--inline-gap) * 1.25); }
@media (max-width: 900px) { .project-featured-wrap { grid-template-columns: 1fr; } }

/* Player embed — STEP 06 Patch §07/§12, lazy-mounted by script.js via
   [data-player-slot]; noscript / no-embed fallback link always present */
.player-container {
  min-height: 80px;
  display: flex;
  align-items: center;
}
.player-fallback-link { font-size: var(--scale-small); }
.player-container[data-embedded="true"] .player-fallback-link.js-player-fallback { display: none; }
.player-container iframe { width: 100%; border: 0; }

/* Release Index — STEP 06-D §4-2/§4-4: converted from text-only rows to a
   1-row-of-3 square-cover card layout, referencing Basic's Selected Works
   density (square 1:1 cover, type/year, title, 1-line role/context). Cover
   images are existing sample assets re-cropped square (no new imagery
   generated) — per §21 Sample Data Rule this is development-stage Master
   Sample content; flag for real cover art at Client Production. This row
   fully replaces the earlier text-only pattern, so there is no separate
   "selected release" section left anywhere on the page. */
.release-index { border-top: 1px solid var(--color-border); }
.release-index-title { padding-block: var(--block-gap); }
.release-index-list {
  display: grid;
  grid-template-columns: repeat(4, 1fr); /* SEOA: 4 releases; 2x2 on tablet */
  gap: var(--block-gap);
}
.release-index-cover { aspect-ratio: 1 / 1; overflow: hidden; }
.release-index-cover img { width: 100%; height: 100%; object-fit: cover; transition: transform var(--motion-base) var(--ease-standard); }
.release-index-info { padding-top: calc(var(--inline-gap) / 2); }
.release-index-meta { font-size: var(--scale-small); color: var(--color-text-secondary); }
.release-index-item-title { font-family: var(--font-display); font-size: var(--scale-h3); margin-top: 2px; transition: color var(--motion-fast) var(--ease-standard); }
.release-index-role { color: var(--color-text-secondary); font-size: var(--scale-small); margin-top: 4px; }
/* STEP 06-E §4-5: whole card now wrapped in a real <a> (Release Index link
   behavior — no broken/dead anchor), hover/focus mechanic matched to
   Basic's Selected Works — cover zoom + title accent color. */
.release-index-link { display: block; }
.release-index-link:hover .release-index-cover img,
.release-index-link:focus-visible .release-index-cover img { transform: scale(1.04); }
.release-index-link:hover .release-index-item-title,
.release-index-link:focus-visible .release-index-item-title { color: var(--color-accent); }
@media (max-width: 1100px) {
  .release-index-list { grid-template-columns: repeat(2, 1fr); gap: var(--block-gap) var(--inline-gap); }
}
@media (max-width: 560px) {
  .release-index-list { grid-template-columns: 1fr; gap: var(--inline-gap); }
}


@media (max-width: 900px) {
  .project-item, body[data-project-layout="p2"] .project-item { grid-template-columns: 1fr !important; }
  body[data-project-layout="p1"] .project-item:nth-child(even) .project-media { order: -1; }
}

/* --------------------------------------------------------------------------
   11. ARCHIVE — shared structure across A1 / A2, group-based
   -------------------------------------------------------------------------- */
.archive-group { padding-block: var(--block-gap); border-top: 1px solid var(--color-border); }
.archive-group:first-of-type { border-top: none; }
.archive-group-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: var(--block-gap); }
.archive-items { display: grid; gap: var(--inline-gap); }
.archive-item { position: relative; overflow: hidden; }
.archive-item img, .archive-item video { width: 100%; height: 100%; object-fit: cover; }
.archive-caption { font-size: var(--scale-small); color: var(--color-text-secondary); margin-top: calc(var(--inline-gap) / 2); }

/* A1 Grid */
body[data-archive-layout="a1"] .archive-items { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
body[data-archive-layout="a1"] .archive-item { aspect-ratio: 1 / 1; }

/* A2 Editorial — STEP 06 Patch §04: asymmetric editorial rhythm instead of
   uniform 1-column stack. Default item stays full-width 16:9; a group can
   mark one item --lead (larger, first) and pair the rest 2-up so Live and
   Editorial don't read as the same repeating block. */
body[data-archive-layout="a2"] .archive-items {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--inline-gap);
  max-width: var(--width-wide);
  margin-inline: auto;
}
body[data-archive-layout="a2"] .archive-item { aspect-ratio: 4 / 3; }
/* STEP 07 §6-2/§6-3: Live/Editorial first image was 21/9 (a hairline
   banner strip) — raised to 4/3 so it reads as a lead photograph with
   real vertical presence, not a landscape banner.
   Fix 02 §5-2/§5-3: 4/3 read a touch too tall again once seen next to the
   paired grid images below it — eased to 3/2 (still a clear lead photo,
   less vertical block than 4/3). Only --lead is touched; the paired 2-up
   grid images stay 4/3 (not flagged in this patch). */
body[data-archive-layout="a2"] .archive-item--lead { grid-column: 1 / -1; aspect-ratio: 3 / 2; }
@media (max-width: 700px) {
  body[data-archive-layout="a2"] .archive-items { grid-template-columns: 1fr; }
  body[data-archive-layout="a2"] .archive-item--lead { aspect-ratio: 3 / 2; }
  /* Fix 02 §7-1/§7-2/§7-3: mobile Archive images/video were capped by
     .tier-wide's own padding-inline (--page-margin) same as every other
     section — reading narrow/boxed-in on a phone ("너무 좁아 보이지
     않게"). Bled to the viewport edge using the same negative-margin
     pattern already used for the H1 hero media bleed above (§08 section).
     Only media bleeds; .archive-group-header (title) and .archive-caption
     stay padded/readable — full-bleed media, padded text is the
     established pattern sitewide, not a new one. */
  body[data-archive-layout="a2"] .archive-items,
  .archive-group .video-grid {
    margin-inline: calc(var(--page-margin) * -1);
    width: auto;
  }
  .archive-featured-moment .archive-item {
    margin-inline: calc(var(--page-margin) * -1);
    width: auto;
  }
}

/* Archive items that open the media modal read as interactive, not decorative.
   Trigger is a real <button class="archive-item-trigger"> wrapping the image
   (not the whole <figure>), so caption text stays outside the click target. */
/* STEP 06-D §5-2: hover-scale removed (both the top preview moment and the
   Live/Editorial group images) — subtle inset outline feedback instead. */
/* STEP 06-E §5-2: border-line hover removed, replaced with the original
   subtle image-scale feedback (1.015–1.03 range), overflow-hidden on the
   parent .archive-item, 300–500ms easing. No hover on touch; no scale
   under reduced-motion. */
.archive-item-trigger { display: block; width: 100%; height: 100%; cursor: pointer; }
.archive-item-trigger img { width: 100%; height: 100%; object-fit: cover; transition: transform 420ms var(--ease-standard); }
.archive-item-trigger:hover img, .archive-item-trigger:focus-visible img { transform: scale(1.025); }
@media (hover: none) {
  .archive-item-trigger:hover img { transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .archive-item-trigger:hover img, .archive-item-trigger:focus-visible img { transform: none; }
}

/* STEP 06-D §5-3: .archive-anchor-nav markup removed sitewide (Group Link
   nav deleted) — its CSS/JS init below removed accordingly (dead-code
   cleanup, 04 MD §19 CODE/FILE QA). */

/* Archive Live group video point — STEP 06 Patch §08, uses the same
   video-card trigger as the Projects video grid (see §17 below) */
.archive-group .video-grid { margin-top: var(--inline-gap); }

.timeline { display: flex; flex-direction: column; gap: var(--block-gap); border-left: 1px solid var(--color-border); padding-left: var(--block-gap); }
.timeline-entry { display: flex; gap: var(--block-gap); }
.timeline[hidden] { display: none; }

@media (max-width: 600px) {
  body[data-archive-layout="a1"] .archive-items { grid-template-columns: repeat(2, 1fr); }
}

/* --------------------------------------------------------------------------
   12. BRIDGE / CTA COMPONENTS
   -------------------------------------------------------------------------- */
.bridge {
  display: flex; justify-content: space-between; align-items: center;
  padding-block: var(--section-gap);
  border-top: 1px solid var(--color-border);
  flex-wrap: wrap;
  gap: var(--block-gap);
}
.cta-link { font-size: var(--scale-h3); font-family: var(--font-display); transition: color var(--motion-fast) var(--ease-standard); }
.cta-link:hover, .cta-link:focus-visible { color: var(--color-accent); } /* STEP 06-C §2-1 */

/* --------------------------------------------------------------------------
   13. FOOTER — STEP 06 Patch: tone/structure aligned with Basic / EPK
   footer family (copyright + contact/social + "Site Experience by
   SOUND FRAME" signature), kept quiet — not a second sitemap.
   -------------------------------------------------------------------------- */
/* STEP 07 §7-7/§12: padding/type scale brought in line with Basic's quiet
   footer (was padding derived from --section-gap — with §03's larger
   token that read oversized/heavy for a footer; Basic uses a small fixed
   32px block regardless of section rhythm elsewhere on the page). */
.site-footer {
  padding-block: 32px;
  padding-inline: var(--page-margin);
  border-top: 1px solid var(--color-border);
  color: var(--color-text-secondary);
  font-size: 12px;
}
.site-footer a { color: var(--color-text-primary); }
/* STEP 06-D §3-6: Basic footer tone — copyright (left) + SOUND FRAME
   signature (right) only. Middle page-link nav removed; Home/Projects/
   Archive are already one click away via the sticky header. */
.footer-inner {
  max-width: var(--width-content);
  margin-inline: auto;
  display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline;
  gap: 8px;
  min-width: 0;
}
.footer-copyright { color: var(--color-text-secondary); font-size: 12px; }
.footer-signature { color: var(--color-text-secondary); text-align: right; font-size: 12px; }
@media (max-width: 600px) {
  .footer-inner { align-items: flex-start; }
  .footer-signature { text-align: left; }
}

/* --------------------------------------------------------------------------
   14. PAGE TRANSITION — T1 / T2
   -------------------------------------------------------------------------- */
.transition-layer {
  position: fixed;
  inset: 0;
  z-index: var(--z-transition);
  pointer-events: none;
  opacity: 0;
}
body[data-transition="t1"] .transition-layer {
  background: var(--color-base);
  transition: opacity var(--motion-nav) var(--ease-standard);
}
body[data-transition="t1"] .transition-layer[data-state="active"] { opacity: 1; }

body[data-transition="t2"] .transition-layer {
  background: var(--color-accent);
  clip-path: polygon(0 100%, 100% 100%, 100% 100%, 0 100%);
  transition: clip-path var(--motion-nav) var(--ease-standard);
}
body[data-transition="t2"] .transition-layer[data-state="active"] {
  clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}
.transition-motif { position: absolute; inset: 0; /* Motif SVG slot — CONFIGURABLE per client, populated via data.js */ }

main { opacity: 1; transition: opacity var(--motion-nav) var(--ease-standard); } /* STEP 06-E §6-1: incoming fade now uses the fast nav token too */
html.is-navigating main { opacity: 0; }

/* --------------------------------------------------------------------------
   15. MEDIA MODAL — shared <dialog> for Archive image lightbox AND
   Video embed playback (STEP 06 Patch §08/§12: one mechanic, two uses,
   not two separate systems). Renamed from "archive-modal" to
   "media-modal"; base content (image) always exists in Static DOM —
   the dialog only reveals/enlarges it. Video iframe is injected only
   on open (script.js), never present at page load.
   -------------------------------------------------------------------------- */
/* STEP 06-E §5-3: modal stage enlarged toward viewport limits (was capped
   at 1100px/920px, reading small on large desktop) so the image fills as
   much of the screen as possible with no internal scroll. */
.media-modal {
  border: none;
  padding: 0;
  width: min(94vw, 1500px);
  max-width: 94vw;
  max-height: 94vh;
  background: transparent;
  overflow: visible;
}
.media-modal::backdrop { background: var(--color-overlay); }
/* STEP 06-D §5-4/§5-5: image modals get a fixed-size stage regardless of
   source aspect ratio — every image (portrait, landscape, square) reads at
   the SAME box size once enlarged, and nothing overflows into a right-side
   scrollbar. Video modal (.media-modal-frame-wrap) is unaffected — it
   already sizes itself via a fixed 16:9 aspect-ratio. */
.media-modal-image-wrap {
  width: min(92vw, 1400px);
  height: min(90vh, 1000px);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-surface);
  overflow: hidden;
}
.media-modal-image-wrap img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; margin: 0; display: block; }
/* Legacy selector kept as a fallback for any dialog not yet wrapped */
.media-modal > img { max-height: 92vh; width: auto; max-width: 100%; margin-inline: auto; display: block; }
@media (max-width: 700px) {
  .media-modal-image-wrap { width: min(94vw, 640px); height: min(70vh, 520px); }
}
.media-modal-close { position: absolute; top: var(--inline-gap); right: var(--inline-gap); color: var(--color-overlay-text); z-index: 1; transition: color var(--motion-fast) var(--ease-standard); }
.media-modal-close:hover, .media-modal-close:focus-visible { color: var(--color-accent); } /* STEP 06-C §2-1 */
.media-modal-frame-wrap { position: relative; width: 100%; aspect-ratio: 16 / 9; background: var(--color-base); }
.media-modal-frame-wrap iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.media-modal-caption { color: var(--color-overlay-text); font-size: var(--scale-small); padding-top: var(--inline-gap); }

/* --------------------------------------------------------------------------
   16. REVEAL — STEP 06 Patch §12 Reusable Interaction (Hero Entrance /
   Project Reveal / Archive Interaction). Scoped under html.js only, so
   JS-off and reduced-motion always show full opacity content — this is
   a progressive enhancement layer, never a requirement to see content.
   -------------------------------------------------------------------------- */
html.js .reveal {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity var(--motion-slow) var(--ease-standard),
              transform var(--motion-slow) var(--ease-standard);
}
html.js .reveal.is-visible { opacity: 1; transform: none; }
html.js .reveal-stagger .reveal:nth-child(2) { transition-delay: 80ms; }
html.js .reveal-stagger .reveal:nth-child(3) { transition-delay: 160ms; }
html.js .reveal-stagger .reveal:nth-child(4) { transition-delay: 240ms; }
@media (prefers-reduced-motion: reduce) {
  html.js .reveal { opacity: 1; transform: none; transition: none; }
}

/* --------------------------------------------------------------------------
   17. MUSIC PLAYER SECTION — STEP 06 Patch §07 (HOME "Listen")
   -------------------------------------------------------------------------- */
.music-player { display: grid; grid-template-columns: 200px 1fr; gap: var(--block-gap); align-items: center; }
.music-player .mp-art { aspect-ratio: 1 / 1; overflow: hidden; }
.music-player .mp-art img { width: 100%; height: 100%; object-fit: cover; }
.music-player .mp-body { display: flex; flex-direction: column; gap: var(--inline-gap); }
.music-player .mp-meta { font-size: var(--scale-small); color: var(--color-text-secondary); }
@media (max-width: 600px) {
  .music-player { grid-template-columns: 96px 1fr; }
}

/* --------------------------------------------------------------------------
   18. VIDEO GRID — STEP 06 Patch §08 (PROJECTS bottom / ARCHIVE Live group)
   Trigger is a real <button data-modal-target>, base thumbnail + caption
   always present in Static DOM; iframe only exists once modal opens.
   -------------------------------------------------------------------------- */
.video-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--inline-gap); }
.video-card { display: block; width: 100%; text-align: left; position: relative; }
/* ROOT-CAUSE FIX (Master Sample patch): .vc-thumb is a <span> (inline by
   default). Without an explicit block formatting context here, hiding the
   poster <img> on click could collapse this shell's height, since the
   iframe is position:absolute and contributes no height of its own. Making
   this a block-level box with an explicit width means aspect-ratio alone
   is enough to hold the shell's size in both states — poster shown OR
   iframe playing — so play never causes the video area to visually vanish. */
.video-card .vc-thumb { display: block; width: 100%; aspect-ratio: 16 / 9; overflow: hidden; position: relative; }
.video-card .vc-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Master Sample Final Visual/Asset Patch §03: the inset accent outline that
   appeared on the thumbnail itself on hover/focus is removed sitewide
   (per feedback: no border/outline line should appear on video images
   on rollover). Keyboard focus indication moves to the outer card
   instead of the thumbnail, so it never reads as an image border. */
.video-card:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 3px; }
/* STEP 06-E §3-6/§4-6/§5-4: play button mechanic aligned with Basic's
   Official Video interaction — filled dark circle with a soft border,
   fills to the signature accent color + scales up slightly on hover/focus
   (thumbnail image itself never scales — only this button does). */
.video-card .vc-play {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  color: var(--color-overlay-text);
}
/* STEP 07 §10: play button quality patch — size reduced and unified
   (48px everywhere; the old per-context overrides at 68px/76px in
   .video-main/.video-feature below are removed so no video reads a
   different button size). Ring switched from a translucent `border`
   (which read soft/"지글지글" at low alpha, especially mid-transition)
   to an opaque inset box-shadow at higher contrast — crisper at retina
   sizes and doesn't shift the box's layout size on hover. */
.video-card .vc-play::before {
  content: "";
  width: 48px; height: 48px;
  border-radius: 50%;
  background: rgba(10,11,13,0.55);
  box-shadow: inset 0 0 0 1.5px rgba(245,245,244,0.9);
  display: block;
  transition: background var(--motion-fast) var(--ease-standard), transform var(--motion-fast) var(--ease-standard), box-shadow var(--motion-fast) var(--ease-standard);
}
.video-card .vc-play::after {
  content: "";
  position: absolute;
  width: 0; height: 0;
  border-style: solid;
  border-width: 7px 0 7px 11px;
  border-color: transparent transparent transparent currentColor;
  transform: translateX(2px); /* optical centering of the right-pointing triangle inside the circle */
}
.video-card:hover .vc-play::before, .video-card:focus-visible .vc-play::before {
  background: var(--color-accent);
  box-shadow: inset 0 0 0 1.5px var(--color-accent);
  transform: scale(1.04);
}
.video-card:hover .vc-play, .video-card:focus-visible .vc-play { color: var(--color-base); } /* icon reads dark against the filled accent circle */
/* .vc-title / .vc-type rules kept for any markup that still opts in, but
   STEP 06-D §3-4/§4-3/§5-4/§5-5 removes the visible label row from every
   video card actually shipped in HOME/PROJECTS/ARCHIVE markup. */
.video-card .vc-title { padding-top: calc(var(--inline-gap) / 2); font-family: var(--font-display); font-size: var(--scale-h3); }
.video-card .vc-type { font-size: var(--scale-small); color: var(--color-text-secondary); }
/* Inline-play state — STEP 06-F §02: iframe is static DOM from the start
   (hidden), poster/play button hide and the iframe reveals via the
   .is-playing class on the trigger, instead of a DOM swap at click time. */
/* Fix 02 §08 REQUIRED IMPLEMENTATION RULE — re-verified: .vc-thumb (the
   shell) carries its own fixed aspect-ratio + position:relative +
   overflow:hidden (see rule above) and is NEVER targeted by display:none —
   only its poster <img> and .vc-play children are. The iframe fills the
   shell via position:absolute; inset:0, so the shell's box never collapses
   on click, in either direction. z-index/pointer-events made explicit here
   (defensive — harmless no-ops given current stacking, but removes any
   ambiguity for future edits) per the patch's explicit checklist. */
.vc-inline-frame { position: absolute; inset: 0; z-index: 1; width: 100%; height: 100%; border: 0; display: none; pointer-events: none; }
.video-card.is-playing .vc-thumb > img,
.video-card.is-playing .vc-play { display: none; }
.video-card.is-playing .vc-inline-frame { display: block; pointer-events: auto; }

/* --------------------------------------------------------------------------
   19. GENERIC-AI-BAN GUARDRAILS (structural notes, not enforced by CSS alone)
   No default border-radius, no default box-shadow, no gradient decoration
   beyond functional overlays above. Pill-shaped buttons intentionally absent.
   -------------------------------------------------------------------------- */

/* ==========================================================================
   20. STEP 06-C — Master Sample CONTENT / UI PATCH
   New components: Featured + Listen (merged), Artist Story, Media Slider
   (Archive preview), Video Feature (single-video block), Follow section,
   section-tone rhythm utilities. See 05-C patch doc for the source spec.
   ========================================================================== */

/* --- 20-1. Section tone-step rhythm (patch §2-2) ---
   Utility classes only — applied selectively per section, never uniformly,
   so the page keeps a dark identity while avoiding one flat black block. */
/* STEP 06-E §08-1: boundary line so a full-width tone band doesn't read as
   bleeding directly into the next full-width tone band with no separation. */
.tone-elevated { background: var(--color-surface); border-top: 1px solid var(--color-border); }
/* STEP 06-E §3-2: Featured Project full-width background band. The section
   itself now carries the tone color at 100% width (no tier-* class on the
   <section>); its content is wrapped in an inner .tier-wide div (standard
   .tier-wide rule already handles max-width/centering/padding-inline). */
.tone-deep {
  /* SEOA: ARCHIVE / HOME Archive Preview = deeper atmospheric band. Token re-scope only (Master-supported tone state). */
  --color-base: #0E1019; --color-surface: #161926; --color-text-primary: #EEEFF5; --color-text-secondary: #A5AABC;
  --color-border: #262A3C; --color-accent: #8387FF; --color-accent-dim: rgba(131, 135, 255, 0.12);
  background: var(--color-base); color: var(--color-text-primary); position: relative; isolation: isolate;
}
.tone-deep::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: radial-gradient(ellipse at 50% 0%, rgba(110, 114, 255, 0.14), transparent 65%);
  pointer-events: none;
}
.section--tight { padding-block: calc(var(--section-gap) / 3); }
/* Master Sample Final Visual/Asset Patch §02: PROJECTS visible "Projects"
   eyebrow removed; only a visually-hidden H1 remains here for heading
   hierarchy/SEO, so this intro section should take no visual space —
   its previous padding-block existed to give the eyebrow breathing
   room, which no longer applies. */
.page-intro--hidden { padding-block: 0; }
/* Fix 02 §5-1: Archive page only — intro spacer trimmed further (top AND
   bottom) so the Featured Archive Moment image reaches the first viewport
   sooner ("페이지 첫 화면에서 한 번에 더 잘 보이도록"). Scoped to
   data-page="archive" so PROJECTS' own .section--tight intro is untouched. */
body[data-page="archive"] .section--tight { padding-block: calc(var(--section-gap) / 6) calc(var(--section-gap) / 10); }
/* Footer-spacing fix: ARCHIVE's trailing Career Timeline section (Career-
   heavy mode only) sits AFTER the real last visible content (Editorial
   group) and, even with its .timeline content hidden, still rendered its
   own full .section padding-block (top + bottom) — an empty "ghost" block
   that stacked on top of Editorial's own bottom padding, doubling the
   Archive → Footer gap versus PROJECTS' last section (Video), which has
   no such trailing empty section. Collapsing padding only when the
   timeline content is actually hidden means Editorial's content now sits
   the same padding-bottom (calc(var(--section-gap)/2), PROJECTS' own
   last-section value — no new value introduced) away from the footer as
   PROJECTS. If a client config ever shows the timeline (career-heavy
   mode, [hidden] removed), this section holds real content again and
   :has() no longer matches, so it falls back to the normal .section
   padding-block automatically — same rhythm as every other content
   section, unchanged. */
body[data-page="archive"] .section:has(> .timeline[hidden]) { padding-block: 0; }
.section--loose { padding-block: calc(var(--section-gap) * 1.15); }

/* --- 20-2. Featured Project + Listen (merged HOME section, patch §3-3) --- */
.featured-albums {
  display: grid;
  grid-template-columns: repeat(4, 1fr); /* SEOA owner-approved 4-era variation */
  gap: var(--block-gap);
  margin-top: calc(var(--block-gap) * 1.4); /* STEP 06-D §3-2: eyebrow now sits directly above — a touch more air since the redundant "Sample-A" h2 was removed */
}
.featured-album-cover { display: block; aspect-ratio: 1 / 1; overflow: hidden; position: relative; }
.featured-album-cover img { width: 100%; height: 100%; object-fit: cover; transition: transform var(--motion-base) var(--ease-standard); }
.featured-album-cover:hover img, .featured-album-cover:focus-visible img { transform: scale(1.04); } /* STEP 06-C §7 HOME: cover hover-focus feedback */
.featured-album-info { padding-top: calc(var(--inline-gap) / 2); }
.featured-album-info h3 { font-size: var(--scale-h3); }
.featured-album-info p { font-size: var(--scale-small); color: var(--color-text-secondary); margin-top: 2px; }
/* STEP 06-D §3-2: 1-line per-album description, reusing the same real
   sample copy already approved in Basic's Selected Works / Premium's own
   Project context — no new facts invented. */
.featured-album-desc { font-size: var(--scale-small); color: var(--color-text-secondary); margin-top: 6px; }

/* STEP 06-D §3-2: Featured + Listen row — left = player, right = hand-off
   note + CTA, replacing the previous stacked note-row/listen-row pair. */
/* STEP 06-E §3-4: player column widened to 58–64% / info column 36–42%
   (was equal 1fr 1fr with the player additionally capped at 440px, which
   made the Spotify embed UI read as clipped). Left/right structure kept. */
.featured-listen-row {
  display: grid;
  grid-template-columns: 1.7fr 1fr;
  gap: var(--section-gap);
  align-items: center;
  margin-top: var(--section-gap);
  padding-top: var(--block-gap);
  border-top: 1px solid var(--color-border);
}
/* STEP 07 §4-2: hand-off note text and its CTA were separated by the same
   --block-gap (48px) used for unrelated content blocks, reading as two
   disconnected pieces. Tightened so the note and its CTA read as one
   paragraph-and-link unit. */
.featured-note { display: flex; flex-direction: column; gap: var(--inline-gap); }
.featured-note .body-copy { max-width: var(--width-reading); }

.featured-listen {
  width: 100%;
  display: flex;
  gap: var(--block-gap);
  align-items: center;
  padding: var(--inline-gap);
  border: 1px solid var(--color-border);
}
.featured-listen .fl-art { width: 84px; aspect-ratio: 1 / 1; flex-shrink: 0; overflow: hidden; }
.featured-listen .fl-art img { width: 100%; height: 100%; object-fit: cover; }
.featured-listen .fl-body { display: flex; flex-direction: column; gap: calc(var(--inline-gap) / 2); min-width: 0; flex: 1; }
.featured-listen .fl-meta { font-size: var(--scale-small); color: var(--color-text-secondary); }
.featured-listen .player-container { min-height: 80px; }
.featured-listen .player-container iframe { width: 100%; max-width: 100%; }
@media (max-width: 900px) {
  .featured-listen-row { grid-template-columns: 1fr; gap: var(--block-gap); }
}
/* STEP 07 §7-3: the Spotify embed's own iframe reports a fixed intrinsic
   width and could push wider than the viewport inside the flex row on
   narrow phones (a min-width:0 was missing on the flex children, the
   classic flex-overflow trap). Explicit min-width:0 lets .fl-body actually
   shrink to the row's real width instead of being pushed out by its
   iframe child; iframe stays width:100% of that shrunk box. */
@media (max-width: 600px) {
  .featured-listen { flex-direction: column; align-items: stretch; }
  /* Fix 02 §6-3: the small cover thumbnail removed entirely on mobile
     (was a 64px square sitting above the player, redundant real estate on
     a narrow screen) — .fl-body now gets the full row width, and the
     player itself gets a touch more vertical room so its own play/seek UI
     reads clearly instead of being squeezed under the thumbnail. */
  .featured-listen .fl-art { display: none; }
  .featured-listen .fl-body { min-width: 0; width: 100%; }
  .featured-listen .player-container { min-height: 96px; }
  .featured-listen .player-container,
  .featured-listen .player-container iframe { min-width: 0; max-width: 100%; }
  /* Master Sample root-cause patch §06: on mobile the outer decorative border
     and inline padding were eating into the Spotify embed's already-tight
     width, on top of visually boxing the player in unnecessarily. Border
     and horizontal padding removed here (desktop keeps its framed card
     treatment) so the embed uses the full row width, edge to edge within
     the page margin; vertical padding kept so the row doesn't feel glued
     to its neighbors. */
  .featured-listen {
    border: none;
    padding: var(--inline-gap) 0;
  }
  .featured-listen .player-container,
  .featured-listen .player-container iframe { width: 100%; }
}
@media (max-width: 1100px) {
  .featured-albums { grid-template-columns: repeat(2, 1fr); gap: var(--block-gap) var(--inline-gap); } /* SEOA: 4 → 2x2 (never hide 4th era) */
}
@media (max-width: 560px) {
  .featured-albums { grid-template-columns: 1fr; gap: var(--block-gap); } /* mobile: 1-col */
}

/* --- 20-3. Artist Story (new HOME section, patch §3-4) --- */
.artist-story {
  display: grid;
  grid-template-columns: 1fr 0.85fr;
  gap: var(--section-gap);
  align-items: center;
  border-top: 1px solid var(--color-border);
  padding-top: var(--block-gap); /* divider sits close to content, .section already provides the main rhythm gap above */
}
.artist-story-body { display: flex; flex-direction: column; gap: var(--inline-gap); max-width: var(--width-reading); }
.artist-story-more { display: block; }
.artist-story-more p + p { margin-top: var(--inline-gap); }
html.js .artist-story-more { display: none; }
html.js .artist-story-more.is-open { display: block; }
.artist-story-toggle {
  align-self: flex-start;
  display: none;
  align-items: center;
  gap: 8px;
  font-size: var(--scale-small);
  color: var(--color-text-primary);
  border-bottom: 1px solid var(--color-border);
  padding-bottom: 2px;
  transition: color var(--motion-fast) var(--ease-standard), border-color var(--motion-fast) var(--ease-standard);
}
html.js .artist-story-toggle { display: inline-flex; }
.artist-story-toggle:hover, .artist-story-toggle:focus-visible { color: var(--color-accent); border-color: var(--color-accent); }
.artist-story-meta { font-size: var(--scale-small); color: var(--color-text-secondary); margin-top: var(--inline-gap); }
.artist-story-visual { aspect-ratio: 4 / 5; overflow: hidden; }
.artist-story-visual img { width: 100%; height: 100%; object-fit: cover; }
@media (max-width: 900px) {
  .artist-story { grid-template-columns: 1fr; }
  /* STEP 07 §7-4: was 16/10 (landscape) — the source photo itself is a
     portrait editorial shot (see index.html width=1122 height=1402), so
     forcing it landscape here just crops away most of the frame. Kept
     portrait on mobile too (slightly less tall than desktop's 4/5 so it
     doesn't dominate a narrow viewport), object-position left at default
     center via .artist-story-visual img's existing object-fit:cover. */
  .artist-story-visual { order: -1; aspect-ratio: 3 / 4; }
}

/* --- 20-4. Media Slider — Archive preview (patch §3-5), a reusable core
   mechanic (any product can reuse this same swipe/autoplay pattern). --- */
/* STEP 07 §4-3: was 21/9 (read as a wide landscape banner) — raised to 3/2.
   Fix 02 §3-3: 3/2 overcorrected the other way (read as a tall banner) —
   settled on 16/9, the middle value between the two: real vertical
   presence without the hairline-banner OR oversized-block feel. Mobile
   keeps its own 4/3 below (not flagged in this patch). */
/* HOME 아카이브 섹션 Image Slider — 세로폭 축소 요청: 16/9 → 21/9 계열로
   조정해 슬라이드가 조금 더 납작하게(짧은 세로폭) 보이도록 한다. Mobile은
   기존 4/3 유지. */
.media-slider { position: relative; width: 100%; aspect-ratio: 20 / 9; overflow: hidden; background: var(--color-surface); }
.media-slider-track { display: flex; height: 100%; width: 100%; will-change: transform; }
html.js .media-slider-track { transition: transform var(--motion-slow) var(--ease-standard); }
.media-slider-slide { flex: 0 0 100%; height: 100%; }
.media-slider-slide img { width: 100%; height: 100%; object-fit: cover; }
/* STEP 07 §4-3/§7-5: manual arrows removed sitewide (desktop + mobile) —
   swipe (touch) + drag + autoplay remain the only controls, kept more
   minimal per patch request. Rule kept (not deleted) so the control UI
   can be restored per-client without rebuilding it; JS still queries
   data-slider-prev/next harmlessly (no-op on a display:none element). */
.media-slider-arrow { display: none; }
.media-slider-prev { left: var(--inline-gap); }
.media-slider-next { right: var(--inline-gap); }
.media-slider-indicators { position: absolute; bottom: var(--inline-gap); left: 50%; translate: -50% 0; display: flex; gap: 8px; z-index: 2; }
.media-slider-indicators button { width: 20px; height: 2px; background: rgba(245,245,244,0.4); transition: background var(--motion-fast) var(--ease-standard); }
.media-slider-indicators button[aria-current="true"] { background: var(--color-accent); }
@media (max-width: 600px) {
  .media-slider { aspect-ratio: 4 / 3; }
}

.archive-preview-head { padding-bottom: var(--block-gap); }
.archive-preview-foot { padding-top: var(--block-gap); }

/* --- 20-5. Video Feature — single wide video block (HOME §3-7 / patch §7) --- */
/* Master Sample Final Visual/Asset Patch §04: "Video" eyebrow and thumbnail
   read as touching — matches the --block-gap rhythm already used between
   eyebrow/heading and content elsewhere (e.g. .archive-preview-head). */
.video-feature > .eyebrow { display: block; margin-bottom: var(--block-gap); }
.video-feature .video-card { position: relative; }
/* Master Sample Final Visual/Asset Patch §04: 21/9 read too wide on desktop —
   given a taller ratio so the video has more vertical presence without
   becoming near-square. */
.video-feature .vc-thumb { aspect-ratio: 16 / 9; }
/* STEP 07 §10: per-context play-button size override removed — every
   video card sitewide now shares the base 48px button (§18 above).
   Only the thumbnail's own aspect-ratio still varies by context. */
.video-feature .vc-title { font-size: var(--scale-h2); padding-top: var(--inline-gap); }
@media (max-width: 700px) { .video-feature .vc-thumb { aspect-ratio: 4 / 3; } }

/* PROJECTS video: 1 main (wide) + 2 secondary (grid), patch §4-4 */
/* Master Sample Final Visual/Asset Patch §05: "Video" H2 and the main video
   read as touching — margin-top added so it matches the section's own
   --block-gap rhythm (margin-bottom below was already there, for the
   gap to the secondary grid). */
.video-main { margin-top: var(--block-gap); margin-bottom: var(--block-gap); }
/* Master Sample Final Visual/Asset Patch §05: 21/9 read too wide on desktop,
   same adjustment as HOME's Video section — given more vertical presence,
   still clearly wider/"main" than the 16/9 secondary pair below it. */
.video-main .vc-thumb { aspect-ratio: 16 / 9; }
.video-main .vc-title { font-size: var(--scale-h3); }
@media (max-width: 700px) { .video-main .vc-thumb { aspect-ratio: 4 / 3; } }

/* --- 20-6. Follow / Contact (STEP 06-D §3-5/§6) — Basic Follow section
   tone applied for real: centered, elevated surface (Basic --bg-elevated
   equivalent), bordered editorial link rows. --- */
/* STEP 06-E §3-7: Follow section font size / line-height / link hierarchy
   / hover / spacing aligned with Basic's Follow section proportions —
   4-up platform grid (desktop) / 2x2 (mobile), booking as its own centered
   row below.
   STEP 06-F §04-3: platform SVG icon + accent-dim fill-on-hover added, so
   this now matches Basic's .social-link mechanic exactly (icon, label,
   arrow, background fill on hover/focus) instead of the icon-less text
   row it shipped with before this patch. */
.follow-section { border-top: 1px solid var(--color-border); padding-top: var(--section-gap); text-align: center; }
.follow-heading { font-size: var(--scale-display); font-family: var(--font-display); line-height: var(--lh-display); }
.follow-intro { margin: var(--inline-gap) auto 0; max-width: var(--width-reading); }
.follow-links {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 12px;
  width: min(100%, 960px);
  margin: var(--block-gap) auto 0;
}
.follow-link {
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  font-family: var(--font-body); font-size: 15px; font-weight: 500; line-height: var(--lh-body);
  border: 1px solid var(--color-border);
  padding: 16px 12px;
  min-width: 0;
  transition: color var(--motion-fast) var(--ease-standard), background var(--motion-fast) var(--ease-standard), border-color var(--motion-fast) var(--ease-standard);
}
/* STEP 06-F §04-3: icon + background-fill hover added — same mechanic as
   Basic's .social-link (icon color transition, accent-dim fill on
   hover/focus), not just a text-color change. */
.follow-link-icon { flex-shrink: 0; color: var(--color-text-secondary); transition: color var(--motion-fast) var(--ease-standard); }
.follow-link:hover, .follow-link:focus-visible { color: var(--color-accent); border-color: var(--color-accent); background: var(--color-accent-dim); }
.follow-link:hover .follow-link-icon, .follow-link:focus-visible .follow-link-icon { color: var(--color-accent); }
.follow-link-arrow { transition: translate var(--motion-fast) var(--ease-standard); }
.follow-link:hover .follow-link-arrow, .follow-link:focus-visible .follow-link-arrow { translate: 3px -3px; }
.follow-booking-row { display: flex; justify-content: center; margin-top: 14px; }
.follow-booking-row .follow-link { width: min(100%, 260px); }
@media (max-width: 640px) {
  .follow-links { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* --- 20-7. Archive modal caption (patch §5-1/§5-2) --- */
.media-modal-caption { max-width: 640px; }

/* --- 20-7b. Cross-page anchor offset utility (STEP 06-E §3-3/§6-3) ---
   html { scroll-padding-top: var(--nav-height) } already clears the sticky
   site header for every anchor. The project-jump-nav that previously sat
   above these targets has been removed (§4-1), so the only remaining
   clearance needed is the sticky header plus a small breathing margin —
   title/cover/release-date now land together in the first viewport with
   no dead space above the heading. Works for same-page and cross-page
   (index.html → projects.html#project-*) anchors, JS or no JS. */
#project-featured, #project-02, #project-03, #project-04, #project-05, #project-06 {
  scroll-margin-top: calc(var(--nav-height) + 24px);
}

/* --- 20-8. Archive: conditional video module marker (patch §5-3) ---
   [data-conditional="video"] blocks are removable as a whole unit per
   artist if no video asset exists — image-only Archive stays fully valid. */
.archive-group [data-conditional="video"][hidden] { display: none; }


/* ==========================================================================
   SEOA — Client Production additions (token-level / Master-supported only)
   ========================================================================== */
/* Hero CTA row (2 CTAs, reuses .cta-link) */
.hero-cta { display: flex; flex-wrap: wrap; gap: var(--inline-gap) var(--block-gap); margin-top: var(--inline-gap); }
.cta-link--secondary { color: var(--color-text-secondary); }
.hero-cta .cta-link { font-size: 1.05rem; padding: 10px 0; border-bottom: 1px solid currentColor; }
.hero-cta .cta-link:first-child { color: var(--color-accent); }
.hero-cta .cta-link:hover, .hero-cta .cta-link:focus-visible { color: var(--color-text-primary); }
/* Section intro copy under eyebrow / title */
.featured-intro, .archive-preview-intro, .video-intro, .index-intro, .sequence-intro, .timeline-note { margin-top: var(--inline-gap); }
.sequence-intro { margin-top: var(--inline-gap); }
.index-intro { margin-bottom: var(--block-gap); }
.project-featured-wrap { margin-top: var(--block-gap); }
.featured-albums + .featured-listen-row { margin-top: var(--section-gap); }
.archive-timeline .timeline { margin-top: var(--block-gap); }
/* Pending video: static thumbnail, no play affordance, not interactive */
.video-card[data-video-pending] { cursor: default; }
/* Pending social channel: visible but inert until official URLs are supplied */
.follow-link[aria-disabled="true"] { cursor: default; opacity: .9; }
.follow-link[aria-disabled="true"]:hover { color: inherit; border-color: var(--color-border); background: transparent; }
.follow-link[aria-disabled="true"]:hover .follow-link-icon { color: var(--color-text-secondary); }
/* Hero image framing for the portrait master-fit asset */
/* H1 hero: .hero-motif is a decorative overlay slot — must not occupy a grid cell (it pushed the media to row 2 when media-right reorders). */
body[data-hero="h1"] .hero { position: relative; }
body[data-hero="h1"] .hero-motif { position: absolute; inset: 0; }
/* Mobile/tablet H1 hero: media stacks first — start below the fixed header so the face is never covered. */


/* ==========================================================================
   SEOA patch — Hero LEFT image / RIGHT copy (OWNER CUSTOM REVIEW: layout variant of Master h1 / media-left)
   One integrated surface: image bleeds to the viewport edge, soft fade into the copy side. No card, no radius.
   ========================================================================== */
body[data-hero="h1"][data-hero-align="media-left"] .hero {
  grid-template-columns: 56fr 44fr;
  gap: 0;
  min-height: 100svh;
  padding-block: 0;
  padding-inline: 0;
}
body[data-hero="h1"][data-hero-align="media-left"] .hero-media { order: -1; padding: 0; height: 100%; min-height: 100svh; }
body[data-hero="h1"][data-hero-align="media-left"] .hero-media img { object-position: 42% 18%; }
body[data-hero="h1"][data-hero-align="media-left"] .hero-media::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(to right, rgba(242,243,246,0) 62%, var(--color-base) 100%);
}
body[data-hero="h1"][data-hero-align="media-left"] .hero-copy {
  order: 2; justify-content: center;
  padding-inline-start: var(--block-gap); padding-inline-end: var(--page-margin);
  padding-block: calc(var(--nav-height) + var(--block-gap)) var(--block-gap);
}
body[data-hero="h1"] .hero-meta { flex-direction: column; gap: 4px; letter-spacing: var(--tracking-label); }
@media (max-width: 900px) {
  body[data-hero="h1"][data-hero-align="media-left"] .hero { grid-template-columns: 1fr !important; min-height: auto; padding-top: var(--nav-height); }
  body[data-hero="h1"][data-hero-align="media-left"] .hero-media { order: -1; min-height: 0; aspect-ratio: 4 / 5; height: auto; margin-inline: 0; width: 100%; }
  body[data-hero="h1"][data-hero-align="media-left"] .hero-media img { object-position: 40% 10%; }
  body[data-hero="h1"][data-hero-align="media-left"] .hero-media::after { background: linear-gradient(to bottom, rgba(242,243,246,0) 70%, var(--color-base) 100%); }
  body[data-hero="h1"][data-hero-align="media-left"] .hero-copy { order: 2; padding: var(--block-gap) var(--page-margin) calc(var(--block-gap) * 1.5); }
}

/* Other Works — text-only index rows (artwork slots pending; no CSS-fabricated art) */
.release-index-sub { font-weight: 400; color: var(--color-text-secondary); font-size: 0.8em; }
.release-index-item--text .release-index-link { display: block; padding-block: var(--inline-gap) 0; border-top: 1px solid var(--color-border); cursor: default; }
.release-index-item--text .release-index-info { padding-top: calc(var(--inline-gap) / 2); }

/* Media embeds (youtube-nocookie, lazy) — sit in the Master .video-card / .vc-thumb 16:9 box */
.video-card--embed .vc-thumb { background: var(--color-surface); }
.vc-embed { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: block; }
.video-card--embed .vc-type { padding-top: calc(var(--inline-gap) / 2); display: flex; justify-content: space-between; gap: var(--inline-gap); flex-wrap: wrap; }
.vc-fallback { color: var(--color-text-secondary); font-size: var(--scale-small); }
.vc-fallback:hover, .vc-fallback:focus-visible { color: var(--color-accent); }
.archive-group .video-grid { grid-template-columns: 1fr; max-width: var(--width-wide); margin-inline: auto; }

/* Follow: six platform root links */
.follow-links { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 640px) { .follow-links { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
