/* ==========================================================================
   SOUND FRAME — Release Page Master Template
   styles.css — Design / Responsive / Visual Configuration Layer

   Structure:
   1. Reset
   2. Design Tokens (Master structure — values below are SAMPLE ONLY)
   3. Typography (role-based)
   4. Layout primitives / Section Rhythm
   5. Utility Nav (Sample Integration Patch)
   6. Section: Hero
   7. Section: Listen
   8. Section: Story (+ Read More)
   9. Section: Track & Credit (+ Track Link Interaction)
   10. Section: Visual (Campaign) — split from Video, Sample Integration Patch
   11. Section: Video — split from Visual, Sample Integration Patch
   12. Video Modal
   13. Section: Platforms (+ Platform Icons)
   14. Section: Artist Context
   15. Footer
   16. Components (CTA, links)
   17. Utilities
   18. Responsive
   19. Accessibility / Motion
   ========================================================================== */

/* -------------------------------------------------------------------------
   1. RESET
   ------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html {
  -webkit-text-size-adjust: 100%;
  /* Anchor navigation (Utility Nav, Hero CTAs) — turned off under
     reduced motion, styles.css §19. */
  scroll-behavior: smooth;
}
body {
  margin: 0;
  background: var(--bg-primary);
  color: var(--text-primary);
  font-family: var(--font-primary);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  /* Universal safety net (STEP 05 §15 Hero Long Data): every text node
     gets safe wrapping by default so an unusually long, unbreakable
     string (artist name, track title, etc.) can never force horizontal
     overflow. keep-all/line-break:strict stay specific to Korean-heavy
     editorial copy (Story, Hero) where they were already applied. */
  overflow-wrap: break-word;
}
img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
ul, ol { margin: 0; padding: 0; list-style: none; }
dl, dd { margin: 0; }
h1, h2, h3, p, blockquote, figure { margin: 0; }
button { font: inherit; }

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--bg-elevated);
  color: var(--text-primary);
  padding: 0.75em 1.25em;
  z-index: 100;
}
.skip-link:focus {
  left: var(--space-md);
  top: var(--space-md);
}

/* -------------------------------------------------------------------------
   2. DESIGN TOKENS
   Master token *names* are FIXED. Values are CONFIGURABLE per Client/Release.
   Everything under "SAMPLE ONLY" reflects the ROVE — NO SIGNAL AT HOME preview and must be
   replaced (not extended) in Client Production.
   ------------------------------------------------------------------------- */
:root {
  /* --- Color tokens — PREMIUM VISUAL SYSTEM (Master Patch 20260926_01) ---
     ROVE cool-light configuration (gray / off-white / muted green). This is a
     brand-tone alignment, not a per-Sample tweak — see Patch §02/§03-1. */
  --bg-primary: #F3F5F3;      /* cool off-white */
  --bg-secondary: #E6EAE8;    /* pale cool gray surface */
  --bg-elevated: #EDF0EE;
  --text-primary: #14181B;    /* cool near-black */
  --text-secondary: #4F5961;  /* medium cool gray (AA on bg-primary) */
  --text-muted: #5B666E;
  --accent-primary: #A6D45E;  /* muted fluorescent green — FILL only (CTA, tick, hover fill) */
  --accent-hover: #93C44A;
  --accent-text: #3E6A14;     /* accessible green for text / outline / thin borders on light */
  --line: #C3CBCF;            /* pale steel gray */
  --overlay: rgba(243, 245, 243, 0.55);
  --surface-contrast: #D9DFDD;
  --nav-glass: rgba(243, 245, 243, 0.82);
  --hero-wash-a: rgba(237, 240, 238, 0.42);
  --hero-wash-b: rgba(237, 240, 238, 0.65);

  /* --- Type tokens — Master structure FIXED, family CONFIGURABLE ---
     Premium reference typeface applied (Patch §02). */
  --font-primary: 'Pretendard Variable', -apple-system, BlinkMacSystemFont, 'Malgun Gothic', 'Segoe UI', sans-serif;
  --font-secondary: 'Pretendard Variable', -apple-system, BlinkMacSystemFont, 'Malgun Gothic', 'Segoe UI', sans-serif;

  /* --- Typography scale (Patch §02) --- */
  --scale-display: clamp(3rem, 5vw, 5.6rem);
  --scale-h2: clamp(1.6rem, 3vw, 2.6rem);
  --scale-h3: clamp(1.2rem, 2vw, 1.6rem);
  --scale-body: 1rem;
  --scale-small: 0.875rem;

  /* --- Spacing scale (Density tokens) --- */
  --space-xs: 0.5rem;
  --space-sm: 1rem;
  --space-md: 1.75rem;
  --space-lg: 3rem;
  --space-xl: 5rem;
  --space-2xl: 8rem;

  /* --- Layout widths --- */
  --width-wide: 1400px;
  --width-readable: 720px;
  /* Story Alternating Editorial Layout (Final UX Polish §03) — wide
     enough for a Text/Image block to breathe, narrower than the full
     Wide Content Zone so it doesn't read as another full-bleed Section. */
  --width-story: 1040px;
  --edge-margin: 6vw;
}

/* -------------------------------------------------------------------------
   3. TYPOGRAPHY — role-based
   ------------------------------------------------------------------------- */
.artist-name {
  font-family: var(--font-primary);
  font-weight: 500;
  font-size: var(--scale-small);
  line-height: 1.4;
  letter-spacing: 0.02em;
  color: var(--text-secondary);
  margin-bottom: var(--space-xs);
}

.display-release {
  font-family: var(--font-primary);
  font-weight: 700;
  /* Fixed scale, not viewport-relative (Master Patch 20260926_05 §05) —
     clamp()'s vw term meant the title kept growing on ultra-wide
     screens with no ceiling; a flat rem value holds identical size at
     1280/1440/1680/1920/2560, with breakpoint-specific fixed steps
     below for Tablet/Mobile rather than fluid scaling. Reduced again
     (follow-up, 2026-09-26): 4.35rem → 3.9rem, same fixed-step logic
     at every breakpoint below. */
  /* Title fits on ONE line whenever the Hero text column allows it: size follows the
     column width (cqw of .hero-content), capped at the Master step for the breakpoint
     (--title-cap) and floored at 2rem; below the floor it wraps naturally at word
     boundaries. 10.8 ≈ measured title width in em (10.2) + safety margin. */
  --title-cap: 3.9rem;
  font-size: clamp(2rem, calc(100cqw / 10.8), var(--title-cap));
  line-height: 1.05;
  letter-spacing: -0.01em;
  color: var(--text-primary);
  word-break: keep-all;
}

/* .section-label — Premium eyebrow system (Patch §03-3), see §5. UTILITY
   NAV block below for the full ::before tick rule. */
.section-label {
  font-family: var(--font-primary);
  font-size: clamp(0.85rem, 1vw + 0.6rem, 1rem);
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--text-primary);
  position: relative;
  display: inline-block;
  padding-left: 16px;
  margin-bottom: var(--space-md);
}
.section-label::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  translate: 0 -50%;
  width: 9px;
  height: 1px;
  background: var(--accent-primary);
}

.story-body p {
  font-family: var(--font-primary);
  font-size: var(--scale-body);
  line-height: 1.6;
  color: var(--text-secondary);
  word-break: keep-all;
  overflow-wrap: break-word;
  line-break: strict;
}
.story-intro {
  font-family: var(--font-primary);
  font-weight: 500;
  font-size: var(--scale-body);
  line-height: 1.6;
  color: var(--text-secondary);
  margin-bottom: var(--space-lg);
  /* Stays at Reading width even though the Story container is wider now
     (Final UX Polish §03) to fit the alternating Text/Image blocks. */
  max-width: var(--width-readable);
  word-break: keep-all;
  overflow-wrap: break-word;
  line-break: strict;
}

.track-title {
  font-family: var(--font-primary);
  font-weight: 500;
  font-size: 1.05rem;
  color: var(--text-primary);
}

.credit-list dt {
  font-family: var(--font-primary);
  font-size: 0.85rem;
  color: var(--text-muted);
}
.credit-list dd {
  font-family: var(--font-primary);
  font-size: 0.95rem;
  color: var(--text-primary);
}

.platform-name {
  font-family: var(--font-primary);
  font-weight: 500;
  font-size: 1.1rem;
  color: var(--text-primary);
}

.caption, .hero-meta, .track-duration, .track-note {
  font-family: var(--font-primary);
  font-size: 0.85rem;
  color: var(--text-secondary);
}

/* Pull Quote — Premium H3 / editorial emphasis hierarchy; the only
   Story text set at primary color + emphasis size, everything else in
   Story reads at secondary/body scale (Master Patch 20260926_02 §05-3). */
.pull-quote p {
  font-family: var(--font-primary);
  font-style: normal;
  font-size: var(--scale-h3);
  line-height: 1.45;
  color: var(--text-primary);
  word-break: keep-all;
}
.pull-quote cite {
  display: block;
  margin-top: var(--space-sm);
  font-family: var(--font-primary);
  font-style: normal;
  font-size: var(--scale-small);
  color: var(--text-secondary);
}

/* -------------------------------------------------------------------------
   4. LAYOUT PRIMITIVES / SECTION RHYTHM
   ------------------------------------------------------------------------- */
.section {
  padding-block: var(--space-2xl);
}

.section-inner {
  max-width: var(--width-wide);
  margin-inline: auto;
  padding-inline: var(--edge-margin);
}

.section-inner--readable {
  max-width: var(--width-readable);
}

.section-inner--story {
  max-width: var(--width-story);
}

/* Refinement Patch §08A — Artist section widened from the old readable
   width (720px) to the same Story/Video rhythm width (1040px), so the
   larger image + link row actually have room. */
.section-inner--artist {
  max-width: var(--width-story);
}

/* scroll-margin-top keeps anchored section headings clear of the sticky
   Utility Nav below — applies to every anchor target the Nav links to. */
#top, #listen, #story, #track-credit, #visual, #video, #platforms {
  scroll-margin-top: var(--nav-height, 72px);
}

/* -------------------------------------------------------------------------
   5. UTILITY NAV (Sample Integration Patch)
   Minimal Release Utility Nav — not a generic website header. Thin,
   sticky, tonally close to the page background rather than a distinct
   branded bar. No large CTA, no hamburger. Understated at the very top
   of Hero, then a hairline separates it from content once scrolled.
   ------------------------------------------------------------------------- */
:root {
  --nav-height: 72px;
}
/* True transparent overlay over Hero (Master Patch 20260926_05 §04-2) —
   fixed instead of sticky, so at page-top the Header sits over Hero
   content rather than the document's own --bg-primary showing through
   underneath a merely-transparent sticky bar (which read as a black
   header from the very first frame). */
.utility-nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  height: var(--nav-height);
  display: flex;
  align-items: center;
  background: transparent;
  border-bottom: 1px solid transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  transition:
    border-color 0.3s ease,
    background-color 0.3s ease,
    backdrop-filter 0.3s ease;
}
/* Defined only once the page has scrolled — script.js adds this class;
   if JS never runs, the Nav simply keeps its transparent default
   appearance (still fully functional — every link below is a real,
   working <a href="#…">, native browser anchor behavior). Premium
   transparent → scrolled translucent-blur behavior (Master Patch
   20260926_01 §04-1, unchanged by this patch — see §04-3). */
.utility-nav.is-scrolled {
  background: var(--nav-glass);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  border-bottom-color: var(--line);
}

.utility-nav-inner {
  width: 100%;
  max-width: var(--width-wide);
  margin-inline: auto;
  padding-left: max(var(--edge-margin), env(safe-area-inset-left));
  padding-right: max(var(--edge-margin), env(safe-area-inset-right));
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
}

/* Text-only hit area (Master Patch 20260926_03 §04-1) — flex:1 previously
   stretched the <a> across the header's empty center space, making that
   space clickable/hoverable even though no text was there. flex:0 1 auto
   + max-width cap the link to its own text content only; empty space
   between Brand and the right nav links is non-interactive. */
.utility-nav-brand {
  font-family: var(--font-primary);
  font-size: 1.1rem;
  font-weight: 500;
  color: var(--text-primary);
  letter-spacing: 0.02em;

  display: inline-block;
  flex: 0 1 auto;
  min-width: 0;
  max-width: min(42vw, 480px);

  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;

  cursor: pointer;
}
/* Hover: no color interaction — text-only hit area, home (#top)
   navigation still works via the native anchor (Master Patch
   20260926_03 §04-2). */
.utility-nav-brand:hover {
  color: var(--text-primary);
}
/* Keyboard focus keeps a visible indicator even though hover no longer
   changes color (Master Patch 20260926_03 §04-3). */
.utility-nav-brand:focus-visible {
  color: var(--text-primary);
  outline: 1px solid var(--accent-text);
  outline-offset: 4px;
}

.utility-nav-links {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  list-style: none;
  margin: 0;
  padding: 0;
  flex-shrink: 0;
}
.utility-nav-links a {
  font-family: var(--font-primary);
  font-size: var(--scale-small);
  color: var(--text-secondary);
  white-space: nowrap;
  padding-block: 0.4em;
  border-bottom: 1px solid transparent;
  transition: color 0.18s ease, border-color 0.18s ease;
}
/* Premium signature accent on hover/focus/active — replaces the former
   secondary-gray hover state (Master Patch 20260926_01 §04-3). */
.utility-nav-links a:hover,
.utility-nav-links a:focus-visible {
  color: var(--accent-text);
  border-bottom-color: var(--accent-text);
}
/* Active-section indication — script.js toggles this via
   IntersectionObserver; purely cosmetic enhancement, every link works
   as a plain anchor with or without it. */
.utility-nav-links a.is-active {
  color: var(--accent-text);
  border-bottom-color: var(--accent-text);
}

/* -------------------------------------------------------------------------
   6. HERO — Impact
   ------------------------------------------------------------------------- */
.hero {
  /* Header is now position:fixed and out of document flow (Master Patch
     20260926_05 §04-4) — Hero's own top padding absorbs --nav-height so
     content never sits under the overlay bar. No separate margin/spacer
     element; Hero stays the first in-flow section. */
  padding-top: calc(var(--nav-height) + var(--space-xl));
  padding-bottom: var(--space-2xl);
  background-color: var(--bg-elevated);
  /* Hero Visual — restrained supporting layer (Sample Integration Patch),
     not a switch to Variant B. Cover + Type stays the actual content;
     this is atmosphere behind it. The gradient is tuned to
     --bg-elevated's own tone, so with no image set it reads as plain
     --bg-elevated, unchanged from before this Patch. */
  background-image:
    linear-gradient(180deg, var(--hero-wash-a), var(--hero-wash-b)),
    var(--hero-visual-image, none);
  background-size: cover;
  background-position: center;
}
.hero-inner {
  max-width: var(--width-wide);
  margin-inline: auto;
  padding-inline: var(--edge-margin);
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  gap: var(--space-xl);
  align-items: center;
}

.hero-cover {
  width: 100%;
}
.cover-frame {
  aspect-ratio: 1 / 1;
  width: 100%;
  background-color: var(--surface-contrast);
  /* Set only via the --cover-image custom property (see index.html
     Production comment) — never a direct url() here, so a missing Sample
     asset never triggers a network request. */
  background-image: var(--cover-image, none);
  background-size: cover;
  background-position: center;
  border: 1px solid var(--line);
}
/* .is-missing carries no visual rule of its own today — the fallback
   background-color above already covers it. Kept as a state hook for
   Client Production tooling (e.g. an asset-checklist script) to query. */

.hero-content { min-width: 0; container-type: inline-size; }

.hero-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs);
  margin-block: var(--space-md) var(--space-sm);

  font-family: var(--font-primary);
  font-size: var(--scale-small);
  line-height: 1.4;
  color: var(--text-secondary);
}
.hero-meta-sep { color: var(--text-secondary); }

.hero-positioning {
  font-family: var(--font-primary);
  font-size: var(--scale-body);
  line-height: 1.6;
  color: var(--text-secondary);
  max-width: 46ch;
  word-break: keep-all;
  overflow-wrap: break-word;
  margin-bottom: var(--space-lg);
}

.hero-cta-group {
  display: flex;
  gap: var(--space-md);
  flex-wrap: wrap;
}

/* Hero legibility over the revealed corridor photo (Owner-approved colour fix):
   text roles that used --text-secondary move to --text-primary inside Hero only,
   and the transparent top nav does the same until it gains its scrolled glass state. */
.hero .artist-name,
.hero .hero-meta,
.hero .hero-meta-item,
.hero .hero-positioning { color: var(--text-primary); }
.utility-nav:not(.is-scrolled) .utility-nav-brand,
.utility-nav:not(.is-scrolled) .utility-nav-links a {
  color: var(--text-primary);
  /* soft light halo only — keeps dark text readable over the darker parts of the photo */
  text-shadow: 0 0 8px rgba(243, 245, 243, 0.6);
}
.utility-nav:not(.is-scrolled) .utility-nav-links a:hover,
.utility-nav:not(.is-scrolled) .utility-nav-links a:focus-visible,
.utility-nav:not(.is-scrolled) .utility-nav-links a.is-active {
  color: var(--text-primary);
  border-bottom-color: var(--text-primary);
}
.utility-nav.is-scrolled .utility-nav-links a { color: var(--text-primary); }
.utility-nav.is-scrolled .utility-nav-links a:hover,
.utility-nav.is-scrolled .utility-nav-links a:focus-visible,
.utility-nav.is-scrolled .utility-nav-links a.is-active {
  color: var(--accent-text);
  border-bottom-color: var(--accent-text);
}

/* -------------------------------------------------------------------------
   7. LISTEN — Functional
   ------------------------------------------------------------------------- */
.listen {
  background: var(--bg-primary);
  /* Compact Functional Transition (Final UX Polish §02) — tighter than
     Hero/Story on purpose: Listen is a brief functional beat between
     Impact (Hero) and Editorial Breathing (Story), not another full
     Section. Top is tighter (close follow-through from Hero's CTA),
     bottom stays generous so Story doesn't feel like it's crowding in. */
  padding-block: var(--space-lg) var(--space-xl);
}
/* Refinement Patch §02 — 2-column layout so the right side reads as
   controlled breathing space, not empty space. Left ~62%, Right ~38%,
   within the 60–64/36–40 range. Support column is capped narrow on
   purpose (never allowed to grow into a marketing card). */
.listen-layout {
  display: grid;
  grid-template-columns: minmax(0, 1.65fr) minmax(0, 1fr);
  /* Refinement Patch §17-1, nudged further §22-1 — wider gap (not
     transform) moves the support column toward the outer edge; the
     player/left column stays where it is, only the available width the
     grid distributes shifts slightly. */
  gap: calc(var(--space-xl) + var(--space-md));
  align-items: start;
  max-width: var(--width-wide);
}
.listen-main {
  display: grid;
  gap: var(--space-sm);
  align-items: center;
  max-width: var(--width-readable);
}
.listen-support {
  max-width: 30ch;
  /* Refinement Patch §17-1/§22-1 — align-self:center balances the column
     against the player block's own visual center; the left padding
     (increased again §22-1) is what pushes it toward the outer edge
     within its own grid track — still no transform. */
  align-self: center;
  padding-top: var(--space-sm);
  padding-left: var(--space-md);
}
.listen-support-copy {
  font-family: var(--font-secondary);
  font-style: italic;
  font-size: 0.95rem;
  line-height: 1.6;
  color: var(--text-secondary);
  word-break: keep-all;
  margin-bottom: var(--space-md);
}
.listen-facts {
  border-top: 1px solid var(--line);
  padding-top: var(--space-sm);
}
.listen-fact {
  display: flex;
  justify-content: space-between;
  gap: var(--space-sm);
  padding-block: 0.3em;
  font-size: 0.85rem;
  line-height: 1.3;
}
.listen-fact dt { color: var(--text-muted); }
.listen-fact dd { color: var(--text-secondary); font-variant-numeric: tabular-nums; }
.listen-track-title {
  font-size: 1.3rem;
  font-weight: 600;
  color: var(--text-primary);
}
.listen-track-meta { color: var(--text-secondary); margin-top: 0.25rem; }

.player-frame {
  width: 100%;
  min-height: 152px;
  border: 0;
  background: var(--bg-secondary);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}
.player-frame-inner { width: 100%; height: 100%; }
/* Once a real Spotify iframe is mounted inside .player-frame, it should
   fill the frame edge-to-edge — the frame itself keeps the border/bg,
   the iframe carries no styling of its own beyond width/height. No
   overlay, no pointer-events blocking, no fake artwork layered on top
   (Master Patch 20260926_02 §03-4) — Spotify's native embed UI renders
   directly, unobstructed. */
.player-frame iframe {
  width: 100%;
  height: 152px;
  display: block;
  border: 0;
}

/* -------------------------------------------------------------------------
   8. STORY — Editorial (+ Read More)
   ------------------------------------------------------------------------- */
.story {
  background: var(--bg-primary);
  /* Same --bg-primary as Listen right above it — a hairline (not a color
     change) is the differentiator, per Final UX Polish §02/§18: "같은
     배경이면 hairline/spacing/subtle surface change 중 하나로 구분".
     Large Editorial Breathing: padding-block stays at the base
     --space-2xl (already larger than Listen's --space-lg/--space-xl). */
  border-top: 1px solid var(--line);
}
.story-body { display: flex; flex-direction: column; gap: var(--space-md); }

/* Desktop Alternating Editorial Layout (Final UX Polish §03).
   Block A: Text ~60% | Image ~40%, image on the right (DOM order already
   matches — text markup first, image second, no reordering needed).
   Block B: Image ~44% | Text ~56%, image on the left — DOM keeps text
   first for reading order (screen readers, JS-off linear flow); only
   the image's visual position moves via `order` (reset at mobile, so
   source order — Text, Image — is what mobile actually shows). */
.story-block {
  display: grid;
  gap: var(--space-lg);
  align-items: start;
  margin-block: var(--space-xl);
}
.story-block--a { grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); }
.story-block--b { grid-template-columns: minmax(0, 0.8fr) minmax(0, 1fr); }
.story-block--b .story-image { order: -1; }
.story-block .story-body { gap: var(--space-md); }

/* Editorial image break within the Story text flow — a rhythm change,
   not a gallery. Each instance keeps the source image's own ratio and
   its own focal point (set per-image below — never a shared "center
   center" default, Final UX Polish §04) rather than one shared crop. */
.story-image {
  width: 100%;
  background-color: var(--surface-contrast);
  background-image: var(--media-image, none);
  background-size: cover;
  border: 1px solid var(--line);
}
.story-image--portrait {
  aspect-ratio: 4 / 5;
  /* story_01: subject stands left-of-frame, face/upper body just above
     vertical center — favor left so a narrower column doesn't crop him
     out rather than the empty right-side corridor. */
  background-position: 25% 35%;
}
.story-image--landscape {
  aspect-ratio: 3 / 2;
  /* story_02: the lit corridor gap and reflective floor sit right-of-
     center — favor that over the flatter dark wall on the far left. */
  background-position: 60% 50%;
}

.pull-quote {
  margin-top: var(--space-xl);
  padding-left: var(--space-md);
  border-left: 2px solid var(--accent-text);
  /* Refinement Patch §22-2 — widened from the §17-2 620px cap to the
     full Story block visual width: .pull-quote sits directly in
     .section-inner--story alongside .story-block, so removing its own
     max-width lets it inherit that same content-box width (~1040px
     minus edge margins) instead of a narrower reading-width token. At
     that width, combined with the smaller font-size below, the quote
     reads as ~2 lines at Desktop 1280–1440 without a forced <br> or any
     fixed height/clipping/ellipsis — it still wraps naturally at
     narrower viewports. */
  max-width: 100%;
}

/* Read More — Progressive Enhancement only. #story-more (paragraphs 3+
   and the Pull Quote) is fully present and visible in the static DOM;
   script.js is the ONLY thing that ever hides it (native `hidden`
   attribute, added at runtime, same pattern as the Video Modal shell) —
   so JS-off always shows the complete Story. The button ships `hidden`
   in index.html and stays that way unless script.js decides the Story
   is long enough to collapse. */
.story-readmore-button {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  min-height: 44px;
  padding-block: 0.5em;
  margin-top: var(--space-sm);
  color: var(--text-secondary);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--line);
  font-family: var(--font-primary);
  font-size: 0.9rem;
  cursor: pointer;
}
.story-readmore-button:hover,
.story-readmore-button:focus-visible {
  color: var(--accent-text);
  border-color: var(--accent-text);
}
.story-readmore-button[hidden] { display: none; }

/* CSS-specificity fix: .story-more also carries the .story-body class
   (display:flex), which otherwise ties with the UA stylesheet's
   [hidden] { display:none } at equal specificity — and since
   .story-body is declared later (this file loads after the UA sheet),
   it would silently win, leaving the "collapsed" state invisible even
   though the hidden *attribute* and *property* were both correctly
   set. #story-more[hidden] (ID + attribute) outranks .story-body
   (class) unconditionally, so this doesn't depend on rule order. */
#story-more[hidden] { display: none; }

/* -------------------------------------------------------------------------
   9. TRACK & CREDIT — Structured (+ Track Link Interaction)
   ------------------------------------------------------------------------- */
.track-credit {
  background: var(--bg-secondary);
}

.tracklist {
  border-top: 1px solid var(--line);
  /* Breathing room before Credits (Final UX Polish §05) — was --space-md;
     the Show More button (when visible) still adds its own margin-bottom
     on top of this. */
  margin-bottom: var(--space-2xl);
}
.tracklist-item {
  /* Plain wrapper now — the interactive grid lives on .tracklist-link
     below, since the whole row is a real <a> (Track Link System,
     Sample Integration Patch). */
}
.tracklist-link {
  display: grid;
  grid-template-columns: 2.5rem minmax(0, 1fr) auto auto;
  grid-template-areas:
    "no title duration arrow"
    ".  note  note  note";
  column-gap: var(--space-sm);
  align-items: baseline;
  padding-block: var(--space-sm);
  border-bottom: 1px solid var(--line);
  color: inherit;
  transition: border-color 0.18s ease;
}
.track-no {
  grid-area: no;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  transition: opacity 0.18s ease, color 0.18s ease;
}
.track-title { grid-area: title; transition: transform 0.18s ease, color 0.18s ease; }
.track-duration {
  grid-area: duration;
  font-variant-numeric: tabular-nums;
  transition: color 0.18s ease;
}
.track-note {
  grid-area: note;
  /* Distinct from the shared caption/duration rule above — Track Note
     Hierarchy (Final UX Polish §06): quieter than Duration, compact,
     capped width so a longer note never pushes the row's height around
     as much as an uncapped line would. */
  color: var(--text-muted);
  font-size: 0.8rem;
  line-height: 1.3;
  max-width: 60ch;
  margin-top: 0.15em;
  transition: color 0.18s ease;
}
.track-note:empty { display: none; }
.track-arrow {
  grid-area: arrow;
  color: var(--accent-text);
  opacity: 0.45;
  transition: opacity 0.18s ease, transform 0.18s ease;
}

/* Restrained row interaction — line accent, not a filled card; nothing
   here ever hides information, only nudges emphasis (STEP 05/Patch
   Track Row Interaction rule: "hover 없이는 정보가 사라지지 않음").
   Refinement Patch §17-3 expands the Signal Blue accent from Number-only
   to Number / Title / Note / Duration together on hover or focus — the
   External Arrow keeps its own pre-existing opacity/translate accent
   unchanged. No row background fill, no glow, no card effect; only
   `color` (+ the pre-existing transform on Title) ever changes, at the
   same restrained 150–220ms the rest of the row already uses. */
.tracklist-link:hover,
.tracklist-link:focus-visible {
  border-bottom-color: var(--text-secondary);
}
.tracklist-link:hover .track-arrow,
.tracklist-link:focus-visible .track-arrow {
  opacity: 1;
  transform: translateX(2px);
}
.tracklist-link:hover .track-no,
.tracklist-link:focus-visible .track-no,
.tracklist-link:hover .track-title,
.tracklist-link:focus-visible .track-title,
.tracklist-link:hover .track-note,
.tracklist-link:focus-visible .track-note,
.tracklist-link:hover .track-duration,
.tracklist-link:focus-visible .track-duration {
  color: var(--accent-text);
  opacity: 1;
}
@media (hover: hover) {
  .tracklist-link:hover .track-title { transform: translateX(2px); }
}

/* Album Show More/Expand (STEP 05) — collapse is a pure CSS/attribute
   toggle on the list itself, never a removal of DOM nodes: script.js only
   flips [data-collapsed] once track count is confirmed over threshold
   (script.js §3). Items 8+ hidden while collapsed; nothing here hides
   anything unless that attribute is present, so JS-off and ≤7-track
   Releases (this Sample) are entirely unaffected. */
.tracklist[data-collapsed="true"] .tracklist-item:nth-child(n + 8) {
  display: none;
}
.showmore-button {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  min-height: 44px;
  padding-block: 0.5em;
  margin-bottom: var(--space-xl);
  color: var(--text-secondary);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--line);
  font-family: var(--font-primary);
  font-size: 0.9rem;
  cursor: pointer;
}
.showmore-button:hover,
.showmore-button:focus-visible {
  color: var(--accent-text);
  border-color: var(--accent-text);
}
.showmore-button[hidden] { display: none; }

/* Single — Track Focus presentation (STEP 05). Same .tracklist/.tracklist-item
   markup and the same Track data model as EP/Album — only the CSS changes
   per [data-release-type="single"], so nothing new is generated for this
   variant. NO SIGNAL AT HOME ships as a Single, so this rule is ACTIVE
   in this Sample (see qa/single-fixture.html for a rendered check). */
.tracklist[data-release-type="single"] {
  border-top: 0;
}
.tracklist[data-release-type="single"] .tracklist-link {
  grid-template-columns: 1fr auto;
  grid-template-areas:
    "title arrow"
    "duration duration"
    "note note";
  row-gap: 0.35em;
  padding-block: var(--space-lg);
  border-bottom: 0;
}
.tracklist[data-release-type="single"] .track-no {
  display: none;
}
.tracklist[data-release-type="single"] .track-title {
  font-size: clamp(1.4rem, 3vw, 1.8rem);
  font-weight: 600;
}
.tracklist[data-release-type="single"] .track-duration {
  color: var(--text-secondary);
}

.credits-label {
  font-family: var(--font-primary);
  font-size: 0.8rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  margin-bottom: var(--space-md);
}

.credits {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-lg);
}
/* Compact Credits (STEP 05) — 1–2 groups shouldn't stretch across the
   4-column track meant for Standard/Dense; :has() lets this react to
   however many .credit-group children are actually present, with no JS
   and no separate "compact" markup to keep in sync. Scoped to desktop —
   the existing ≤768px rule below already forces a single column on
   mobile regardless of group count, and :has() would otherwise out-
   specificity that rule. */
@media (min-width: 769px) {
  .credits:has(> .credit-group:nth-child(2):last-child),
  .credits:has(> .credit-group:only-child) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    max-width: var(--width-readable);
  }
  .credits:has(> .credit-group:only-child) {
    grid-template-columns: minmax(0, 1fr);
  }
}
.credit-group-title {
  font-family: var(--font-primary);
  color: var(--text-primary);
  font-size: 0.85rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  margin-bottom: var(--space-sm);
  padding-bottom: var(--space-xs);
  border-bottom: 1px solid var(--line);
}
.credit-row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-sm);
  padding-block: 0.35em;
}

/* -------------------------------------------------------------------------
   10. SECTION: VISUAL (Campaign) — Visual
   Split from the former combined "Visual / Media" section into its own
   scroll Section (Sample Integration Patch §13) — approved Master
   Refinement of the STEP 01 Visual/Media architectural group, not a new
   page architecture. Video gets its own Section, §11 below, with a
   different background token so the two don't read as one long gallery.
   ------------------------------------------------------------------------- */
.visual {
  background: var(--bg-elevated);
}

/* --- Campaign Visual composition: Main (large) + Detail + Portrait ---
   Asymmetric grid, not an equal 3-column gallery. */
.campaign-visual {
  display: grid;
  gap: var(--space-md);
}
.campaign-visual[data-count="3"] {
  /* minmax(0, …fr) — without the 0 floor, a grid item carrying
     aspect-ratio can force its track past the container (aspect-ratio +
     fr rows create a sizing feedback loop). Rows are auto, not fr: they
     size from Detail/Portrait's own aspect-ratio, and Main (no
     aspect-ratio of its own on desktop) stretches to match. */
  grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
  grid-template-rows: auto auto;
  grid-template-areas:
    "main detail"
    "main portrait";
}

/* data-count="1" — Wide Editorial Image (STEP 05 Patch). One item, full
   Wide Content Zone width (inherits section-inner's max-width, no Full
   Bleed forcing), not styled as a Card. Default aspect-ratio below is a
   documented placeholder for the missing-asset state — a real image's
   own ratio should replace it (same convention as Detail/Portrait's
   3:2 / 4:5 defaults for this Sample). */
.campaign-visual[data-count="1"] {
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "main";
}
.campaign-visual[data-count="1"] .campaign-item--main {
  aspect-ratio: 16 / 9;
}

/* data-count="2" — Asymmetric Pair (STEP 05 Patch). ~62:38, never 50:50 —
   a genuinely different visual weight between the two images, not a
   generic two-up grid. */
.campaign-visual[data-count="2"] {
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  grid-template-areas: "main secondary";
}
.campaign-visual[data-count="2"] .campaign-item--main {
  aspect-ratio: 4 / 5;
}
.campaign-item--secondary {
  grid-area: secondary;
  aspect-ratio: 3 / 2;
}
.campaign-item {
  position: relative;
  min-width: 0;
  min-height: 0;
  background-color: var(--surface-contrast);
  /* Set only via --media-image (see index.html Production comments) —
     never a direct url() — so a missing Sample asset makes no request. */
  background-image: var(--media-image, none);
  background-size: cover;
  background-position: center;
  border: 1px solid var(--line);
}

/* Click affordance (Refinement Patch §05) — only ever shown once
   script.js has confirmed a real asset and promoted the item to
   [role="button"]; a missing-asset placeholder never looks clickable.
   Restrained: small arrow, no persistent label, no heavy overlay. */
.campaign-item-arrow {
  position: absolute;
  top: var(--space-sm);
  right: var(--space-sm);
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border: 1px solid var(--line);
  background: rgba(11, 13, 16, 0.55);
  color: #F5F5F4;
  font-size: 1rem;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease;
}
.campaign-item[role="button"]:focus-visible .campaign-item-arrow {
  opacity: 1;
}
.campaign-item--main {
  grid-area: main;
  /* No aspect-ratio here on desktop: it stretches to match the combined
     height of Detail + Portrait's own rows (see composition note above). */
}
.campaign-item--detail { grid-area: detail; aspect-ratio: 3 / 2; }
.campaign-item--portrait { grid-area: portrait; aspect-ratio: 4 / 5; }

/* Restrained hover response — never required to see the content underneath */
@media (hover: hover) {
  .campaign-item {
    transition: transform 0.4s ease;
  }
  .campaign-item[role="button"]:hover {
    transform: scale(1.01);
  }
  .campaign-item[role="button"]:hover .campaign-item-arrow {
    opacity: 1;
  }
}

/* -------------------------------------------------------------------------
   11. SECTION: VIDEO — Visual (secondary tone)
   Own Section now (Sample Integration Patch §13). --bg-secondary instead
   of Visual's --bg-elevated so the two Sections read as a deliberate
   rhythm change, not one continuous gallery.
   ------------------------------------------------------------------------- */
.video-section {
  background: var(--bg-secondary);
}

/* --- Inline Video Stage (Refinement Patch §06) --- */
.video-stage { display: flex; }
.video-frame {
  position: relative;
  aspect-ratio: 16 / 9;
  /* Wide Cinematic (Final UX Polish §09, widened again under Refinement
     Patch §06 to the requested 980–1180px range) — wider than
     --width-story (1040px) so Video reads as a genuine stage, not a
     shrunk-down embed next to Campaign Visual's Wide Immersive width
     right above it. */
  max-width: 1180px;
  width: 100%;
  background-color: var(--surface-contrast);
  background-image: var(--media-image, none);
  background-size: cover;
  background-position: center;
  border: 1px solid var(--line);
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: var(--space-md);
  overflow: hidden;
  /* Whole frame is now the click target, not just the button
     (2026-09-26 follow-up) — cursor communicates that across the full
     thumbnail area. */
  cursor: pointer;
}
/* Once script.js mounts a real <iframe> on click, it should fill the
   stage edge-to-edge. */
.video-frame iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}
/* One-click playback crossfade (Refinement Patch §20-4) — the thumbnail
   stays visible as .video-frame's own background-image underneath while
   the new iframe fades in on top, instead of a hard cut to a blank
   frame while YouTube's own content loads. No spinner, no separate
   loading state. Respects reduced motion via the global rule, §19. */
.video-iframe-enter {
  opacity: 0;
  transition: opacity 0.25s ease;
}
.video-iframe-enter.is-visible {
  opacity: 1;
}

/* Real <button> from the start — inert without JS (no click handler
   attached), never a fake/broken control. Bigger + higher-contrast than
   the old modal-trigger affordance so "this plays" reads immediately. */
/* Play control = Premium video control quality (Master Patch 20260926_01
   §05-2). No label row, no glyph character — icon is a pure CSS
   pseudo-element triangle on a 48px circle, matching Premium exactly. */
.video-play-button {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  background: transparent;
  border: 0;
  cursor: pointer;
}
.video-play-icon {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  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);
  transition:
    background 0.22s ease,
    transform 0.22s ease,
    box-shadow 0.22s ease;
}
.video-play-icon::before {
  content: "";
  position: absolute;
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 7px 0 7px 11px;
  border-color: transparent transparent transparent #F5F5F4; /* on dark scrim over thumbnail */
  transform: translateX(2px);
}
/* Full video-frame hover trigger (Master Patch 20260926_02 §02-2) — the
   entire frame is the hover surface; only the central play control reacts
   (no thumbnail scale, no frame border change). Click target stays
   .video-play-button; script.js unchanged. */
@media (hover: hover) {
  .video-frame:hover .video-play-icon {
    background: var(--accent-primary);
    box-shadow: inset 0 0 0 1.5px var(--accent-primary);
    transform: scale(1.04);
  }
  .video-frame:hover .video-play-icon::before {
    border-left-color: var(--text-primary);
  }
}
/* Touch state — Mobile has no hover, so :active carries the same
   interaction language as Desktop's frame-hover (Master Patch
   20260926_05 §07-2). Click target / script.js unchanged. */
.video-frame:active .video-play-icon,
.video-frame:focus-within .video-play-icon {
  background: var(--accent-primary);
  box-shadow: inset 0 0 0 1.5px var(--accent-primary);
  transform: scale(1.04);
}
.video-frame:active .video-play-icon::before,
.video-frame:focus-within .video-play-icon::before {
  border-left-color: var(--text-primary);
}
/* Once script.js mounts the iframe, the button is removed from the DOM
   (innerHTML swap) — nothing further to hide here. */

.video-meta { max-width: var(--width-readable); }
.video-title {
  font-family: var(--font-primary);
  font-size: 1.15rem;
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: 0.25rem;
}
.video-type {
  font-family: var(--font-primary);
  color: var(--text-secondary);
  font-size: 0.9rem;
  margin-bottom: var(--space-sm);
}

/* -------------------------------------------------------------------------
   12. CAMPAIGN VISUAL FULLSCREEN VIEWER (Final UX Polish §07/§08)
   Minimal — image itself is the content, not a Gallery App. No
   thumbnail strip, no glass panel, no rounded card. The Inline Video
   Stage (Refinement Patch §06) replaced the former Video Modal, so this
   is now the only fullscreen overlay on the page.
   ------------------------------------------------------------------------- */
.campaign-viewer {
  /* Viewer stays a dark overlay on the cool-light page: local token scope so
     its controls keep contrast without touching the viewer's DOM/behavior. */
  --text-primary: #F5F5F4;
  --text-secondary: #B9C0C6;
  --accent-hover: #A6D45E;
  --accent-text: #A6D45E;
  --bg-primary: #101316;
  --line: #3A4147;
  position: fixed;
  inset: 0;
  z-index: 210;
  display: flex;
  align-items: center;
  justify-content: center;
}
.campaign-viewer[hidden] { display: none; }

.campaign-viewer-backdrop {
  position: absolute;
  inset: 0;
  background-color: rgba(11, 13, 16, 0.96);
}

.campaign-viewer-image {
  position: relative;
  z-index: 1;
  max-width: 88vw;
  max-height: 80vh;
  width: auto;
  height: auto;
  object-fit: contain;
  border: 1px solid var(--line);
}

.campaign-viewer-close,
.campaign-viewer-prev,
.campaign-viewer-next {
  position: absolute;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  background: transparent;
  color: var(--text-primary);
  border: 1px solid var(--line);
  cursor: pointer;
}
.campaign-viewer-close:hover, .campaign-viewer-close:focus-visible,
.campaign-viewer-prev:hover, .campaign-viewer-prev:focus-visible,
.campaign-viewer-next:hover, .campaign-viewer-next:focus-visible {
  color: var(--accent-hover);
  border-color: var(--text-secondary);
}
.campaign-viewer-close {
  top: var(--space-md);
  right: var(--space-md);
  font-size: 1.4rem;
}
.campaign-viewer-prev {
  left: var(--space-md);
  top: 50%;
  transform: translateY(-50%);
  font-size: 1.1rem;
}
.campaign-viewer-next {
  right: var(--space-md);
  top: 50%;
  transform: translateY(-50%);
  font-size: 1.1rem;
}
/* Single-image case: no prev/next needed — script.js gives the element
   [hidden], never just a disabled/faded control. */
.campaign-viewer-prev[hidden],
.campaign-viewer-next[hidden] { display: none; }

.campaign-viewer-counter {
  position: absolute;
  z-index: 1;
  bottom: var(--space-md);
  left: 50%;
  transform: translateX(-50%);
  font-size: 0.8rem;
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

/* Campaign items become viewer triggers only once script.js confirms a
   real asset is applied (checked via the --media-image custom property
   actually being set) — a missing-asset placeholder never opens an
   empty viewer. Cursor/outline only, never a card treatment. */
.campaign-item[role="button"] {
  cursor: pointer;
}

/* -------------------------------------------------------------------------
   13. SECTION: PLATFORMS — Conversion (+ Platform Icons)
   ------------------------------------------------------------------------- */
.platforms {
  background: var(--bg-primary);
}
/* Refinement Patch §07A — same balance logic as Listen (§02): right
   column reads as controlled space, not empty space. */
.platforms-layout {
  display: grid;
  grid-template-columns: minmax(0, 1.65fr) minmax(0, 1fr);
  /* Refinement Patch §17-5, nudged further §22-3 — same wider-gap
     approach as Listen §22-1, no transform. */
  gap: calc(var(--space-xl) + var(--space-md));
  align-items: start;
  max-width: var(--width-wide);
}
.platform-list {
  border-top: 1px solid var(--line);
}
.platforms-support {
  max-width: 36ch;
  padding-top: var(--space-md);
  padding-left: var(--space-md);
}
.platforms-support-copy {
  font-size: 0.9rem;
  line-height: 1.5;
  color: var(--text-secondary);
  word-break: keep-all;
  margin-bottom: var(--space-md);
}
.platform-item { border-bottom: 1px solid var(--line); }
.platform-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-block: var(--space-md);
  min-height: 44px;
}
.platform-name-group {
  display: flex;
  align-items: center;
  gap: 0.75em;
}
/* Simple neutral pictograms, not brand logos — see README "Platform
   Icons / OFFICIAL PLATFORM ICON REPLACEMENT" gate. Monochrome,
   currentColor-driven so they never introduce brand accent colors. */
.platform-icon {
  width: 22px;
  height: 22px;
  flex-shrink: 0;
  color: var(--text-secondary);
  opacity: 0.85;
  transition: color 0.18s ease, opacity 0.18s ease;
}
.platform-link:hover .platform-icon,
.platform-link:focus-visible .platform-icon {
  color: var(--accent-text);
  opacity: 1;
}
.platform-arrow {
  color: var(--accent-text);
  transition: transform 0.2s ease;
}
.platform-link:hover .platform-arrow,
.platform-link:focus-visible .platform-arrow {
  transform: translateX(4px);
}

/* Share Release — Minimal Utility (Final UX Polish §15). Ships `hidden`
   in the static DOM; script.js reveals it only if the browser actually
   supports navigator.share or the Clipboard API, same "ship hidden,
   reveal only if usable" pattern as Show More/Read More. JS off: no
   button at all — a plain Copy Link button with no JS behind it would
   be a dead control, which this Master doesn't ship. */
.share-button {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  min-height: 44px;
  padding-block: 0.5em;
  margin-top: 0;
  color: var(--text-secondary);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--line);
  font-family: var(--font-primary);
  font-size: 0.9rem;
  cursor: pointer;
}
.share-button:hover,
.share-button:focus-visible {
  color: var(--text-primary);
  border-color: var(--text-secondary);
}
.share-button[hidden] { display: none; }

/* -------------------------------------------------------------------------
   14. ARTIST CONTEXT — Exit
   ------------------------------------------------------------------------- */
.artist-context {
  background: var(--bg-secondary);
  padding-block: var(--space-xl);
}
/* Wider now (Refinement Patch §08A) but still deliberately secondary to
   the copy — this is the Release's exit point to Artist EPK/Social, not
   a second Profile Section (Sample Integration Patch §17: "EPK 전체
   Profile Section처럼 커지지 않아야"). Percentage-based split instead of
   a fixed pixel width so it scales with the wider container. */
.artist-context-layout {
  display: grid;
  grid-template-columns: minmax(0, 0.56fr) minmax(0, 1fr);
  gap: var(--space-xl);
  align-items: start;
}
.artist-context-image {
  width: 100%;
  aspect-ratio: 4 / 5;
  background-color: var(--surface-contrast);
  background-image: var(--media-image, none);
  background-size: cover;
  background-position: center;
  border: 1px solid var(--line);
}
.artist-context-text { min-width: 0; }
.artist-context-name {
  font-size: 1.4rem;
  font-weight: 600;
  margin-bottom: var(--space-xs);
}
.artist-context-copy {
  color: var(--text-secondary);
  max-width: 52ch;
  line-height: 1.7;
  word-break: keep-all;
  margin-bottom: var(--space-lg);
}
.artist-context-links {
  display: flex;
  flex-wrap: nowrap;
  gap: var(--space-lg);
}
.context-link {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  color: var(--text-primary);
  border-bottom: 1px solid var(--line);
  padding-bottom: 0.2em;
  transition: border-color 0.18s ease;
}
.context-link:hover,
.context-link:focus-visible {
  border-color: var(--accent-text);
}
.context-link span { color: var(--accent-text); }

/* -------------------------------------------------------------------------
   15. FOOTER
   ------------------------------------------------------------------------- */
.site-footer {
  background: var(--bg-primary);
  border-top: 1px solid var(--line);
  padding-block: var(--space-lg);
}
.footer-inner {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-xs);
}
.footer-artist,
.footer-signature {
  margin: 0;

  font-family: var(--font-primary);
  font-size: 12px;
  font-weight: 400;
  line-height: 1.4;

  color: var(--text-secondary);
}
.footer-signature {
  text-align: right;
}

/* -------------------------------------------------------------------------
   16. COMPONENTS — CTA / links
   ------------------------------------------------------------------------- */
.cta {
  display: inline-flex;
  align-items: center;
  padding: 0.85em 1.6em;
  font-size: 0.95rem;
  font-weight: 500;
  border: 1px solid transparent;
  min-height: 44px;
}
.cta-primary {
  background: var(--accent-primary);
  color: var(--text-primary);
}
.cta-primary:hover, .cta-primary:focus-visible {
  background: var(--accent-hover);
}
.cta-secondary {
  border-color: var(--line);
  color: var(--text-primary);
}
.cta-secondary:hover, .cta-secondary:focus-visible {
  border-color: var(--text-secondary);
}
.cta-external {
  border-color: var(--line);
  color: var(--text-secondary);
  align-self: start;
}
.cta-external:hover, .cta-external:focus-visible {
  color: var(--text-primary);
  border-color: var(--text-secondary);
}

/* -------------------------------------------------------------------------
   17. UTILITIES
   ------------------------------------------------------------------------- */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* -------------------------------------------------------------------------
   18. RESPONSIVE
   ------------------------------------------------------------------------- */

/* 1024px — tighten wide zone before the mobile stack */
@media (max-width: 1024px) {
  :root { --edge-margin: 5vw; }
  .credits { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* Tablet safeguard — fixed step, not a fluid scale (Master Patch
     20260926_05 §05-3), to prevent overflow at the narrower Tablet
     width without reintroducing viewport-relative growth. */
  .display-release { --title-cap: 3.4rem; }
}

/* 768px and below — mobile stacking begins */
@media (max-width: 768px) {
  .section { padding-block: var(--space-xl); }
  .hero {
    padding-top: calc(var(--nav-height) + var(--space-lg));
    padding-bottom: var(--space-xl);
  }
  /* Fixed Mobile step, not vw-based (Master Patch 20260926_05 §05-4). */
  .display-release { --title-cap: 2.25rem; }
  .hero-inner {
    grid-template-columns: 1fr;
    gap: var(--space-lg);
  }
  /* Refinement Patch §10-2 — Cover width now matches the same content
     edges as Artist/Title/Meta/CTA below it (both are full-width
     children of .hero-inner, which already carries the shared
     --edge-margin padding) instead of an independently centered vw cap.
     Not full-bleed — still inside the same padded content column. */
  .hero-content { text-align: left; }
  .hero-cta-group { flex-direction: column; align-items: stretch; }

  /* Story: alternating Text/Image blocks collapse to a single column,
     source order (Text, then Image, for BOTH blocks — Final UX Polish
     §03/§20: "Desktop alternating 해제 → 단일 컬럼"). Block B's image
     had `order: -1` for the desktop left-position; reset it here so
     mobile doesn't inherit that reordering. */
  .story-block {
    grid-template-columns: 1fr;
    margin-block: var(--space-lg);
  }
  .story-block--b .story-image { order: 0; }

  /* Listen / Listen Everywhere support columns: stack below the main
     content, no longer capped to a narrow desktop reading width. */
  .listen-layout, .platforms-layout {
    grid-template-columns: 1fr;
    gap: var(--space-lg);
  }
  .listen-support {
    max-width: 420px;
    width: 100%;
    margin-inline: auto;
    padding-left: 0;
    text-align: center;
  }
  /* facts stay a dt/dd list; the unit is centered as a whole, rows keep label-left / value-right */
  .listen-facts { max-width: 280px; margin-inline: auto; text-align: left; }
  /* Listen Everywhere support column kept its Desktop 30ch cap (~267px)
     even after stacking to a single column above, which squeezed the
     copy into 4 wrapped lines instead of reading as a short block.
     320px (vs. `.listen-support`'s max-width:none) is a controlled
     widen, not an unset: at this width the copy settles at exactly 3
     lines with clean sentence-boundary breaks ("...platform." /
     "...LOW SIGNAL" / "wherever you listen.") across 360–768px,
     whereas `none` would let it stretch wide enough to fall to 2 lines
     instead. The Copy Release Link button needs no separate
     repositioning — it's a normal-flow sibling directly below the
     copy, so it moves up on its own now that the paragraph is one
     line shorter. */
  /* Listen Everywhere support block (copy + optional share button): centered as a unit on
     mobile; platform rows and the desktop two-column layout are untouched. */
  .platforms-support {
    width: 100%;
    max-width: 360px;
    margin-inline: auto;
    padding-left: 0;
    padding-right: 0;
    text-align: center;
  }

  .credits { grid-template-columns: 1fr; gap: var(--space-md); }

  .tracklist-link {
    /* Refinement Patch §10-3 — same row shape as desktop (No / Title /
       Duration / Arrow all on one row, Note full-width below); only the
       No. column narrows. Previously Duration+Arrow dropped to their
       own row — reverted, duration must sit immediately left of the
       external arrow like Desktop. */
    grid-template-columns: 1.75rem minmax(0, 1fr) auto auto;
    grid-template-areas:
      "no title duration arrow"
      ".  note  note     note";
    column-gap: var(--space-xs);
    row-gap: 0.2em;
  }
  .tracklist[data-release-type="single"] .tracklist-link {
    grid-template-columns: 1fr auto;
  }

  /* Campaign Visual: vertical sequence, Main first, original ratios kept —
     no forced crop. */
  .campaign-visual[data-count="3"] {
    grid-template-columns: 1fr;
    grid-template-rows: auto;
    grid-template-areas:
      "main"
      "detail"
      "portrait";
  }
  /* Scoped to data-count="3" only — Main needs its own aspect-ratio again
     here since it's no longer spanning two rows to inherit its height.
     Count 1/2 keep their own desktop aspect-ratio (16:9 / 4:5) unchanged
     at mobile — this selector must not leak into those. */
  .campaign-visual[data-count="3"] .campaign-item--main { aspect-ratio: 4 / 5; }

  /* data-count="2" — Main → Secondary vertical stack (source order
     already correct, no reordering needed). */
  .campaign-visual[data-count="2"] {
    grid-template-columns: 1fr;
    grid-template-areas:
      "main"
      "secondary";
  }

  /* Artist Context: image above copy. Refinement Patch §18-3 — full
     content-width (the base .artist-context-image rule is already
     width:100%; this mobile breakpoint previously overrode it down to
     a 140px thumbnail, which read as a small profile crop rather than
     an editorial image). 4:5 ratio and object-position are unchanged
     from the base rule, so the image just gets larger, not re-cropped.
     Still not an EPK Profile Section — text/links below stay modest. */
  .artist-context-layout { grid-template-columns: 1fr; }

  .artist-context-links { flex-direction: column; gap: var(--space-sm); }
  .footer-inner { flex-direction: column; }
  /* Footer stacks to a single column on Mobile (rule above), so the
     right-hand signature no longer sits beside the left copyright line
     — it's now its own full-width block. Left-aligning it here (was
     text-align: right, inherited from Desktop's side-by-side layout)
     keeps both lines flush to the same left edge instead of the
     signature's wrapped second line reading ragged-right against the
     copyright line above it. */
  .footer-signature { text-align: left; }

  /* Watch — Play control, mobile-scaled (follow-up, 2026-09-26). Master
     Patch 20260926_01 §05-2 originally kept 48px fixed on Mobile since
     it already clears the WCAG ≥44px tappable target — still true at
     44px, so scaling down to 44px keeps full accessibility compliance
     while sitting better against a full-width Mobile thumbnail. Triangle
     border-width scaled proportionally (48→44 ≈ 92%) so the glyph stays
     visually centered, not just the circle. */
  .video-play-icon {
    width: 44px;
    height: 44px;
  }
  .video-play-icon::before {
    border-width: 6px 0 6px 10px;
  }

  /* Utility Nav — two-zone layout (Refinement Patch §18-1, tightened
     §23). Brand left (flexible, ellipsis if it ever needs it, secured
     FIRST per §23's priority order — nav padding, then brand font-size,
     then letter-spacing, then action gap, ellipsis only as a last
     resort); Listen + Watch form one right-side action group — adjacent
     in the DOM once Story/Tracks/Visual/Platforms are `display:none`
     below, so Listen sits immediately left of Watch with nothing else
     rendered between them, right-aligned, fixed (never shrinks first).
     Grid instead of the desktop flex row so the brand's
     `minmax(0, 1fr)` track can actually go to 0 and trigger the
     ellipsis before the right-hand links are touched — a plain flex
     `space-between` doesn't guarantee that priority. Verified at 360px:
     brand text, "Listen", and "Watch" all fit with margin to spare,
     0 horizontal overflow, right group never touches the viewport
     edge (safe-area padding already on .utility-nav-inner). */
  .utility-nav-inner {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: 8px;
  }
  /* Follow-up (2026-09-26): Master Patch 20260926_05 §06 matched Desktop's
     1.1rem exactly, but at real Mobile widths that crowds "Listen /
     Watch" and leaves the brand no breathing room against the right
     action group and the empty center gap. Scaled back down to a
     size proportioned to the actual available width — still notably
     larger/bolder than the pre-Patch-05 0.72rem, just not full Desktop
     parity. justify-self:start still required — grid items default to
     stretch, which would otherwise re-open the Header hit-area issue
     Master Patch 20260926_03 fixed. */
  .utility-nav-brand {
    font-size: 0.9rem;
    font-weight: 500;
    justify-self: start;

    max-width: 100%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .utility-nav-links { justify-content: flex-end; gap: 6px; }
  .utility-nav-links a:not(.utility-nav-link--listen):not(.utility-nav-link--watch) {
    display: none;
  }
}

/* 430px and below — smallest supported width */
@media (max-width: 430px) {
  :root { --edge-margin: 6vw; }
}

/* Fixed Hero title steps for small Mobile (Master Patch 20260926_05
   §05-4) — breakpoint-specific fixed sizes, not vw/clamp, so the title
   never grows again above these widths. */
@media (max-width: 390px) {
  .display-release { --title-cap: 2.1rem; }
}
@media (max-width: 360px) {
  .display-release { --title-cap: 2rem; }
}

/* -------------------------------------------------------------------------
   19. ACCESSIBILITY / MOTION
   ------------------------------------------------------------------------- */
:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 2px;
}

/* Restrained, one-time reveal — Campaign Visual + Video only (STEP 04
   scope). A page-load moment, not scroll-gated: script.js flips every
   "reveal-pending" element to "is-revealed" within ~2 animation frames
   of running (plus a short timeout safety net), regardless of scroll
   position — so visibility never depends on the user having scrolled,
   or on any observer firing. If JS never runs at all, "reveal-pending"
   is never added in the first place, so content is simply visible. */
.reveal-pending {
  opacity: 0;
  transform: translateY(12px);
}
.reveal-pending.is-revealed {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.5s ease, transform 0.5s ease;
}

/* Body scroll lock while the Video Modal is open */
.has-modal-open {
  overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
  * { transition-duration: 0.001ms !important; animation-duration: 0.001ms !important; }
  html { scroll-behavior: auto; }
}
