/* Music Moment — landing page.
 * Tokens live in tokens.css; this file is layout and sections only.
 * Dark only. Visual direction: the layered radial wash the store screenshots and the Play
 * feature graphic already sit on, so the page and the store gallery read as one piece of art
 * direction rather than two. */

.wrap { width: 100%; max-width: var(--max); margin: 0 auto; padding: 0 var(--gutter); }

/* ── header ─────────────────────────────────────────────────────────────────────────── */
.hdr {
  position: sticky; top: 0; z-index: 50;
  background: rgba(10, 22, 34, .72);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--border);
}
.hdr .wrap { display: flex; align-items: center; gap: 16px; height: 64px; }
.hdr nav { margin-left: auto; display: flex; align-items: center; gap: 8px; }

.lang {
  display: inline-flex; align-items: center;
  border: 1px solid var(--border); border-radius: var(--r-pill);
  overflow: hidden;
}
.lang a {
  padding: 7px 12px;
  font-family: var(--fd); font-size: 12px; font-weight: 600;
  color: var(--tx2); text-decoration: none;
  transition: color var(--t-fast) var(--std), background var(--t-fast) var(--std);
}
/* Each language is labelled in its own script, never in the current UI language — the Korean
 * convention, and the only version that is legible to someone who cannot read the page. */
.lang a[aria-current='true'] { color: var(--tx); background: var(--s2); }
.lang a:hover { color: var(--tx); }

/* ── download cluster ───────────────────────────────────────────────────────────────── */
.dl { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.badges { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
.badges img { height: 44px; width: auto; }
.badges a { display: block; border-radius: 8px; transition: transform var(--t-fast) var(--std); }
.badges a:active { transform: scale(.96); }
/* Both badges are always in the markup and both stay at full strength. Dimming the one that
 * does not match the visitor's phone read as the page refusing them the other store, and it
 * told them nothing they did not already know from the badge itself. */



/* ── hero ───────────────────────────────────────────────────────────────────────────── */
.hero {
  position: relative; overflow: hidden;
  background:
    radial-gradient(88% 48% at 76% -2%,  rgba(8,179,239,.50),  rgba(8,179,239,0) 68%),
    radial-gradient(80% 46% at -4% 24%,  rgba(25,195,201,.34), rgba(25,195,201,0) 70%),
    radial-gradient(120% 58% at 50% 98%, rgba(27,90,175,.62),  rgba(27,90,175,0) 76%),
    radial-gradient(90% 40% at 12% 100%, rgba(12,140,190,.26), rgba(12,140,190,0) 72%),
    var(--ground);
}
/* Vignette, then a fade into the page ground so the hero has no hard bottom edge. */
.hero::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background:
    radial-gradient(125% 85% at 50% 22%, rgba(0,0,0,0) 62%, rgba(0,0,0,.28) 100%),
    linear-gradient(180deg, rgba(11,13,18,0) 74%, var(--bg) 100%);
}
.hero .wrap {
  position: relative; z-index: 1;
  display: grid; gap: clamp(40px, 6vw, 72px); align-items: center;
  grid-template-columns: 1fr;
  padding-top: clamp(52px, 7vw, 88px); padding-bottom: clamp(60px, 8vw, 104px);
}
@media (min-width: 940px) { .hero .wrap { grid-template-columns: 1.05fr .95fr; } }
/* Single column, so every pixel above the phone pushes it further out of the first screen. The
   stacked spacing here is tuned against a 660px-tall viewport — an iPhone with Safari's address
   bar and toolbar showing — where the stock desktop rhythm left the device starting at 401px
   and only its top third in view. */
@media (max-width: 939px) {
  .hero .wrap { gap: 44px; padding-top: 28px; padding-bottom: clamp(44px, 8vw, 104px); }
  .hero h1 { margin-top: 10px; }
  .hero-sub { margin-top: 16px; }
  .hero .dl { margin-top: 24px; }
}

.hero h1 {
  margin-top: 18px;
  font-family: var(--fd); font-weight: 800;
  font-size: clamp(38px, 6.6vw, 64px); line-height: 1.06; letter-spacing: -.03em;
  text-shadow: 0 2px 28px rgba(0, 0, 0, .45);
}
.hero-sub {
  /* Balanced, because this line has no <br> of its own any more. The English runs to two lines
     at phone width and the natural break left "music" alone on the second one. */
  text-wrap: balance;
  margin-top: 20px; max-width: 30ch;
  font-size: clamp(16px, 2.1vw, 19px); line-height: 1.55; color: var(--tx2);
}
.hero .dl { margin-top: 32px; }

/* relative so the overlay control has something to sit on — the hero video needs the same
   stop-the-motion affordance the section below it has. */
.hero-device { position: relative; display: flex; justify-content: center; }
/* Tilted so the hero is not a straight-on product shot — the least memorable hero treatment in
 * the reference set. Straightened below the breakpoint, where the tilt costs width the phone
 * needs more than the page needs the angle. */
.hero-device .device { --fw: min(330px, 74vw); transform: rotate(6deg); }
@media (max-width: 939px) { .hero-device .device { transform: none; } }

/* ── screenshot strip ───────────────────────────────────────────────────────────────── */
.strip {
  padding: clamp(24px, 4vw, 44px) 0 clamp(56px, 8vw, 88px);
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.strip::-webkit-scrollbar { display: none; }
.strip-row {
  display: flex; gap: clamp(14px, 2vw, 26px); align-items: flex-end;
  width: max-content;
  padding: 0 max(var(--gutter), calc((100vw - var(--max)) / 2));
}
/* A spotlight walks the row left to right and starts over, and on a narrow screen the row
   scrolls itself so the lit screen is the one you can see. Driven from app.js rather than
   keyframes: the scroll has to land on the same beat as the highlight, and syncing script to a
   CSS animation's phase is guesswork. */
.strip img {
  width: clamp(158px, 20vw, 232px); height: auto;
  opacity: .5;
  filter: drop-shadow(0 30px 44px rgba(0, 0, 0, .5));
  transition: opacity var(--t-slow) var(--std), transform var(--t-slow) var(--std);
  will-change: opacity, transform;
}
.strip img.lit { opacity: 1; transform: translateY(-16px); }
.strip img:hover { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  /* Show the whole row at full strength rather than dimming four of five screens; app.js
     stops the rotation and the auto-scroll to match. */
  .strip img { opacity: 1; transform: none; transition: none; }
}

/* ── sections ───────────────────────────────────────────────────────────────────────── */
.sec { padding: clamp(76px, 11vw, 136px) 0; position: relative; }
.sec h2 {
  font-family: var(--fd); font-weight: 700;
  font-size: clamp(28px, 4.2vw, 44px); line-height: 1.16; letter-spacing: -.022em;
}
.sec p.body { margin-top: 22px; font-size: 16px; line-height: 1.75; color: var(--tx2); max-width: 44ch; }
.sec p.sec-s { margin-top: 16px; font-size: clamp(16px, 2vw, 19px); line-height: 1.5; color: var(--tx2); }
.sec .eyebrow { display: block; margin-bottom: 16px; }

.split { display: grid; gap: clamp(36px, 6vw, 80px); grid-template-columns: 1fr; align-items: center; }
@media (min-width: 900px) {
  .split { grid-template-columns: 1fr 1fr; }
  .split.rev > :first-child { order: 2; }
}

.shot { display: flex; justify-content: center; }
.shot img { width: min(320px, 78vw); height: auto; filter: drop-shadow(0 40px 60px rgba(0, 0, 0, .55)); }

.term {
  display: inline-flex; align-items: center; gap: 10px;
  margin-top: 26px; padding: 11px 16px;
  border: 1px solid var(--border); border-radius: var(--r-md);
  background: var(--s1); font-size: 14px; color: var(--tx2);
}
.term b { color: var(--tx); font-weight: 600; }

/* A line lifted off a real card in the screenshots. The product's own voice beats a sentence
 * written for the occasion. */
.quote {
  margin-top: 26px; padding: 16px 18px;
  border-left: 2px solid var(--primary); border-radius: 0 var(--r-md) var(--r-md) 0;
  background: rgba(8, 179, 239, .06);
  font-size: 16px; line-height: 1.6; color: var(--tx);
}
.quote cite { display: block; margin-top: 8px; font-size: 13px; font-style: normal; color: var(--tx2); }

.glow-r::before, .glow-l::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
}
.glow-r::before { background: radial-gradient(760px 520px at 80% 45%, rgba(8, 179, 239, .12), transparent 62%); }
.glow-l::before { background: radial-gradient(760px 520px at 20% 45%, rgba(25, 195, 201, .10), transparent 62%); }
.sec > .wrap { position: relative; }

/* ── real profiles ──────────────────────────────────────────────────────────────────── */
.profiles .head { max-width: 46ch; margin-bottom: clamp(32px, 4vw, 48px); }
.pcards { display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }
.pcard {
  display: flex; flex-direction: column; gap: 12px;
  padding: 20px;
  border: 1px solid var(--border); border-radius: var(--r-lg);
  background: var(--s1);
  text-decoration: none;
  transition: transform var(--t-base) var(--std), border-color var(--t-base) var(--std);
}
.pcard:hover { transform: translateY(-3px); border-color: rgba(255, 255, 255, .18); }
.pcard .handle { font-family: var(--fd); font-size: 17px; font-weight: 700; color: var(--tx); }
.pcard .meta { font-size: 13px; color: var(--tx2); }
.pcard .go { margin-top: auto; font-size: 13px; font-weight: 600; color: var(--primary); }

/* ── video block ────────────────────────────────────────────────────────────────────── */
.vid { position: relative; display: flex; justify-content: center; }
.vid .device { --fw: min(320px, 78vw); }

/* A control that is not there until it is wanted.
 *
 * A button parked under the screen was visual noise on a section whose whole job is to show the
 * app moving. But WCAG 2.2.2 asks for a way to stop motion that starts by itself and runs past
 * five seconds, and this loop runs 22. Both hold if the control lives ON the video and shows
 * itself in exactly the three cases where someone wants it: the pointer is over the video, it
 * has keyboard focus, or the loop is not playing at all — which is also how a reader whose
 * autoplay was refused, or who has reduced motion turned on, gets a way to start it. */
[data-vbox] .vtog {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  z-index: 2; width: 54px; height: 54px; padding: 0;
  display: grid; place-items: center;
  border: 1px solid rgba(255, 255, 255, .24); border-radius: var(--r-pill);
  background: rgba(11, 13, 18, .55);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  color: var(--tx); cursor: pointer;
  opacity: 0; pointer-events: none;
  transition: opacity var(--t-base) var(--std);
}
[data-vbox]:hover .vtog,
[data-vbox] .vtog:focus-visible,
[data-vbox][data-playing='false'] .vtog { opacity: 1; pointer-events: auto; }
[data-vbox] .vtog:hover { background: rgba(11, 13, 18, .75); border-color: rgba(255, 255, 255, .4); }
/* One icon at a time, chosen by what the video is actually doing rather than by what was
   clicked — the loop also stops on its own when it scrolls out of view. */
[data-vbox][data-playing='true'] .i-play,
[data-vbox][data-playing='false'] .i-pause { display: none; }

/* ── closing band ───────────────────────────────────────────────────────────────────── */
/* The glow behind this section lives on .finale, not here — see the finale block below. */
.closing { text-align: center; }
.closing .wrap { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; }
.closing h2 { font-size: clamp(30px, 4.6vw, 48px); }
.closing .slogan { margin-top: 18px; font-size: clamp(16px, 2vw, 19px); color: var(--tx2); }
.closing .dl { margin-top: 34px; justify-content: center; }


/* ── finale glow ────────────────────────────────────────────────────────────────────
 * The hero's background, copied verbatim — same four radials, same positions, same opacities,
 * same ground, same vignette. Nothing here is a second interpretation of it.
 *
 * It spans the profiles section AND the closing band rather than sitting on the closing alone.
 * Every radial is sized in percentages of its own box, so on a single short section the whole
 * light field compressed into a stripe: the right colours, the wrong shape. Given two sections
 * to cover it renders at roughly the hero's proportions, which is what makes it read as the
 * same glow rather than a smaller copy of it.
 *
 * The hero only fades at the bottom because the header covers its top. This one has page
 * ground above it and the footer below, so the same fade runs at both ends. */
.finale {
  position: relative; overflow: hidden;
  background:
    radial-gradient(88% 48% at 76% -2%,  rgba(8,179,239,.50),  rgba(8,179,239,0) 68%),
    radial-gradient(80% 46% at -4% 24%,  rgba(25,195,201,.34), rgba(25,195,201,0) 70%),
    radial-gradient(120% 58% at 50% 98%, rgba(27,90,175,.62),  rgba(27,90,175,0) 76%),
    radial-gradient(90% 40% at 12% 100%, rgba(12,140,190,.26), rgba(12,140,190,0) 72%),
    var(--ground);
}
/* One gradient for both ends, not two stacked layers — stacked, each one dims the full height
   a little and the glow between them goes grey. */
.finale::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background:
    radial-gradient(125% 85% at 50% 22%, rgba(0,0,0,0) 62%, rgba(0,0,0,.28) 100%),
    linear-gradient(180deg,
      var(--bg) 0%, rgba(11,13,18,0) 15%,
      rgba(11,13,18,0) 85%, var(--bg) 100%);
}
/* .sec is already position:relative; this lifts both sections over the fade layer, which as a
   pseudo-element is the last child and would otherwise paint on top of them. */
.finale > .sec { z-index: 1; }

/* ── footer ─────────────────────────────────────────────────────────────────────────── */
/* No rule above the footer: the glow now fades to the page ground right where the footer
   starts, and a hairline there re-drew the seam the fade exists to remove. */
.ft { padding: clamp(48px, 7vw, 72px) 0 40px; }
.ft-top { display: grid; gap: 36px; grid-template-columns: 1fr; margin-bottom: 40px; }
@media (min-width: 760px) { .ft-top { grid-template-columns: 1.4fr 1fr 1fr; } }
.ft h3 {
  font-family: var(--fd); font-size: 12px; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase; color: var(--tx3);
  margin-bottom: 14px;
}
.ft ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.ft a { color: var(--tx2); text-decoration: none; font-size: 14px; }
.ft a:hover { color: var(--tx); text-decoration: underline; }
/* --tx3 measures 4.14:1 on the page ground, under the 4.5:1 body-text minimum, so neither of
   these lines uses it: the copyright sits on --tx2 (8.55:1) and the attribution below it too. */
.ft-bottom {
  display: flex; flex-wrap: wrap; align-items: center; gap: 16px;
  padding-top: 26px; border-top: 1px solid var(--border);
}
.ft-bottom .copy { font-size: 12px; color: var(--tx2); }
.ft .tm { margin-top: 18px; font-size: 11px; line-height: 1.7; color: var(--tx2); }
.ft-bottom .lang { margin-left: auto; }

/* ── sticky mobile download bar ─────────────────────────────────────────────────────── */
.sticky {
  position: fixed; left: 12px; right: 12px; bottom: 12px; z-index: 60;
  display: none; align-items: center; gap: 12px;
  padding: 10px 12px;
  border: 1px solid var(--border); border-radius: var(--r-md);
  background: rgba(20, 22, 29, .95);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  box-shadow: var(--shadow-raised);
  transform: translateY(140%);
  transition: transform var(--t-slow) var(--std);
}
.sticky[data-on='true'] { transform: none; }
.sticky img.icon { width: 38px; height: 38px; border-radius: 9px; }
/* min-width:0 is what lets a flex child shrink below its content width; without it the title
   pushes the CTA off the bar and both wrap. The ellipsis then does the trimming. */
.sticky .t { flex: 1; min-width: 0; }
.sticky .t b,
.sticky .t span { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sticky .t b { font-family: var(--fd); font-size: 14px; font-weight: 700; }
.sticky .t span { font-size: 12px; color: var(--tx2); }
.sticky .cta { padding: 9px 14px; font-size: 13px; white-space: nowrap; flex-shrink: 0; }
.sticky .x {
  border: 0; background: none; color: var(--tx2);
  font-size: 18px; line-height: 1; padding: 6px; cursor: pointer;
}
@media (max-width: 760px) { .sticky { display: flex; } }
/* At 320px the bar had 83px for a 93px wordmark, so "music moment" itself ellipsised. Trimming
   the gaps, the icon and the two buttons buys back ~28px, which clears both lines with room to
   spare. The subtitle is the app's price rather than the slogan for the same reason — the
   slogan needs 110px and would have been living on a 1px margin. */
@media (max-width: 400px) {
  .sticky { gap: 8px; padding: 10px 10px; }
  .sticky img.icon { width: 32px; height: 32px; border-radius: 8px; }
  .sticky .cta { padding: 9px 11px; }
  .sticky .x { padding: 6px 2px; }
}
/* Measured: the slogan needs 110px and the bar gives the text 119px at 320px but 192px at the
   narrowest iPhone in use. Only the 320px class is tight enough that a fallback Korean face
   could push it into the ellipsis, so only that class steps down. */
@media (max-width: 340px) { .sticky .t span { font-size: 11px; } }
/* The media query's display:flex outranks the UA stylesheet's [hidden] rule, so without this
   the bar would appear on a phone whose JavaScript never ran — the one case where nothing has
   pointed its CTA at the right store yet. */
.sticky[hidden] { display: none !important; }

/* ── narrow ─────────────────────────────────────────────────────────────────────────── */
@media (max-width: 560px) {
  .hdr .wrap { gap: 10px; }
  .hdr .wm { width: 124px; height: 18px; }
  /* Redundant on a phone: the badges are already in the hero and the sticky bar covers the
     rest of the page. Dropping it is what keeps the header on one line at 375 px. */
  .hdr nav > .cta-ghost { display: none; }
  .lang a { padding: 6px 10px; font-size: 11px; }
  .ft-bottom .lang { margin-left: 0; }
}

/* ── scroll reveal ─────────────────────────────────────────────────────────────────────
 * Every animated group carries [data-anim]; app.js adds .in when it scrolls into view and
 * stops watching it. Nothing here runs on load except the hero, which is already in view.
 *
 * Order within a group is fixed: heading types, subline fades, the screen rises, the store
 * badges arrive last. The badges are last on purpose — they are the thing the page is for, and
 * arriving after everything else is what makes them register rather than blend in.
 *
 * Everything starts from a visible state and is hidden by these rules, so a visitor with no
 * JavaScript sees the finished page rather than a blank one: .in is only ever added, and the
 * no-JS fallback at the end of this block restores the resting state. */

/* Typewriter. app.js splits each heading on <br> and wraps the lines; --n is that line's
 * character count. The wipe is a clip-path rather than an animated width so the text still
 * wraps normally and layout never shifts, and steps(--n) quantises the reveal into one jump
 * per character instead of a smooth slide.
 *
 * --tw-ms is the per-character speed and is the ONE place it is written down: app.js reads it
 * back off the root element to compute each line's start time, so the two can't drift. */
:root { --tw-ms: 66; }
/* The English headline is 29 characters to the Korean one's 11, so the same per-character
   speed makes it type for two and a half times as long. 66 -> 53 took 20% off, then 53 -> 37
   another 30%, which lands it at roughly the Korean heading's total. */
html[lang="en"] .hero { --tw-ms: 37; }
/* width:fit-content is load-bearing, not tidiness. As a plain block each line filled the
 * heading's full width, so the clip swept the whole column while the text only occupied part
 * of it: a short line finished revealing early and the animation then ran out its remaining
 * time over empty space. That dead tail is what read as a stutter at every line break, and no
 * amount of adjusting the gap between lines could remove it — the pause was inside the line.
 * Hugging the text makes the sweep and the text the same length, so one step really is one
 * character and the lines hand over with no seam. */
.tw {
  display: block;
  width: fit-content;
  /* The tracking on these headings is negative, so the content box sits tighter than the last
     glyph's ink and a clip that stops at the box edge shaves the final character. .07em was
     not enough once the fallback Korean face was in play. */
  padding-right: .12em;
}
/* THE RESTING STATE IS UNCLIPPED, and the animation only ever fills BACKWARDS.
 *
 * It used to be the other way round: the rest state was the hidden frame and the animation
 * carried it open with fill-mode:both. That makes the finished headline depend on the last
 * frame of a steps() animation actually landing and staying applied — and on iOS Safari it
 * does not, which is why "연결되는 공간" kept arriving with the 간 shaved off. Two earlier
 * attempts treated the symptom (unit mismatch in the keyframes, then more padding) and the
 * character came back both times.
 *
 * Now nothing about the finished state is animated. When the animation is over the element
 * falls back to the rule above, which has no clip at all, so there is no final frame left to
 * go missing. The hidden start is a separate rule that only applies before .in lands. */
/* The vertical insets are NEGATIVE on purpose. line-height on these headings is 1.06, tighter
   than the font's own line box, so a descender — the g in "through music" — hangs below the
   element's border box. Overflow is visible, so it paints fine; a clip-path stopping at the
   box edge is what sliced it off, and only while the animation was running, which is why the
   tail of the g came back the moment the heading finished. The wipe is horizontal, so the top
   and bottom edges have no business being at the box edge at all. */
html:not(.no-js) [data-anim]:not(.in) .tw { clip-path: inset(-.35em 100% -.35em 0%); }
/* fit-content opts the box out of text-align, so centred headings need the box centred too —
   and the right-hand padding mirrored, or the line sits off-centre by that much. */
.closing .tw { margin-inline: auto; padding-left: .12em; }
[data-anim].in .tw {
  animation: type calc(var(--n) * var(--tw-ms) * 1ms) steps(var(--n)) backwards;
  animation-delay: calc(var(--td) * 1ms);
}
/* Both frames keep every position in one kind of unit — em for the vertical pair, percent for
   the horizontal — so there is nothing here for an engine to interpolate inconsistently. */
@keyframes type {
  from { clip-path: inset(-.35em 100% -.35em 0%); }
  to   { clip-path: inset(-.35em 0%   -.35em 0%); }
}

/* Opacity only, and held in place — a subline that also moves reads as a second event after
 * the heading rather than the same sentence settling. Slow on purpose. */
[data-anim] .fade { opacity: 0; }
[data-anim].in .fade {
  animation: fadein 1365ms var(--std) both;
  animation-delay: calc(var(--fd, 0) * 1ms);
}
@keyframes fadein { to { opacity: 1; } }
@keyframes fadeup { to { opacity: 1; transform: none; } }

/* The screens travel further than the text does — the same move the current musicmoment.com
 * makes, where content starts well below the fold line and lifts into place.
 *
 * Gated on its OWN class, not the section's. The section reveals as soon as its top edge
 * appears, which for a screen that is taller than half the viewport meant the rise finished
 * while the reader was still scrolling toward it — the animation played to nobody. app.js
 * adds .rise-in only once the element has climbed past the middle of the screen.
 *
 * The easing is deliberately NOT --std. That curve puts ~80% of the distance into the first
 * fifth of the duration, so the screen appears to arrive already in place and only the last
 * few pixels read as motion. This one spreads the travel out. */
.rise { opacity: 0; transform: translateY(110px); }
.rise.rise-in {
  animation: fadeup 820ms cubic-bezier(.22, .61, .36, 1) both;
}

[data-anim] .last { opacity: 0; transform: translateY(28px); }
[data-anim].in .last {
  animation: fadeup var(--t-slow) var(--std) both;
  animation-delay: calc(var(--ld, 700) * 1ms);
}

/* No JavaScript, or motion turned down: show everything in its resting state immediately. */
.no-js .tw, .no-js .fade, .no-js .rise, .no-js .last {
  clip-path: none; opacity: 1; transform: none; animation: none;
}
@media (prefers-reduced-motion: reduce) {
  .tw, .fade, .rise, .rise.rise-in, .last,
  [data-anim].in .tw, [data-anim].in .fade, [data-anim].in .last {
    clip-path: none !important; opacity: 1 !important; transform: none !important;
    animation: none !important;
  }
}

/* ── profile card avatars ─────────────────────────────────────────────────────────────── */
.pcard .top { display: flex; align-items: center; gap: 12px; }
/* Circular everywhere, as in the app. The gradient ring is the app's own active-avatar
 * treatment; the inner border keeps the photo off the ring so the two do not blend. */
.pcard .av {
  width: 44px; height: 44px; flex-shrink: 0;
  border-radius: 50%;
  background: var(--gradient);
  padding: 2px;
}
.pcard .av img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; display: block; }
.pcard .handle { font-family: var(--fd); font-size: 17px; font-weight: 700; color: var(--tx); }


/* ── 모먼트 월 ───────────────────────────────────────────────────────────────────────── */
.wall h2 { margin-bottom: clamp(34px, 4.6vw, 52px); }

.wall-grid {
  display: grid; gap: 14px;
  grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 720px) { .wall-grid { grid-template-columns: repeat(4, 1fr); } }

/* The card is the still, with the sentence written on it — the same shape the app gives a
   moment, rather than a metadata row with a picture attached. 4:5 because a portrait holds a
   two-line comment without the type having to shrink. */
.moment {
  position: relative;
  display: block;
  aspect-ratio: 4 / 5;
  border-radius: var(--r-lg);
  overflow: hidden;
  text-decoration: none; color: inherit;
  background: var(--s1);
  transition: transform var(--t-base) var(--std);
}
.moment:hover { transform: translateY(-4px); }

.m-art {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover;
  /* Pulled back so the sentence, not the still, is what the eye lands on. */
  filter: saturate(.85) brightness(.72);
  transition: transform var(--t-expressive) var(--std), filter var(--t-base) var(--std);
}
.moment:hover .m-art { transform: scale(1.04); filter: saturate(1) brightness(.8); }

/* Text now sits at BOTH ends of the card, so the scrim darkens both and clears the middle —
   the same shape the app uses over a playing video (design.md §2.5 overlay.scrim). A
   bottom-weighted gradient would have left the quote at the top sitting on raw artwork. */
.m-veil {
  position: absolute; inset: 0;
  background: linear-gradient(180deg,
    rgba(10,13,18,.72) 0%, rgba(10,13,18,.34) 26%, rgba(10,13,18,.10) 46%,
    rgba(10,13,18,.52) 74%, rgba(10,13,18,.92) 100%);
}
.moment::after {
  content: ''; position: absolute; inset: 0; border-radius: var(--r-lg);
  box-shadow: 0 0 0 1px rgba(255,255,255,.09) inset;
  pointer-events: none;
}

.m-text {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; justify-content: space-between;
  gap: 14px;
  padding: 18px;
}

/* A second, local pane of shade under the sentence only.
 *
 * The card-wide veil above has to lift back to .10 across the middle of the card, or the still
 * stops being a picture. But a three-line comment reaches into exactly that band, so its last
 * line was sitting on raw artwork while its first line sat on .72. This layer is shaped to even
 * that out rather than to pile on: it adds almost nothing at the very top, where the veil is
 * already dark, and the most in the 30–50% band, where the veil is at its weakest. It is gone
 * well before the credit. */
.m-text::before {
  content: '';
  position: absolute; inset: 0 0 auto 0;
  height: 66%;
  pointer-events: none;
  background: linear-gradient(180deg,
    rgba(8,11,16,.22) 0%, rgba(8,11,16,.34) 30%,
    rgba(8,11,16,.30) 50%, rgba(8,11,16,0) 100%);
}
/* Both blocks are positioned so they paint above that pane. The pseudo-element is first in DOM
   order, so without this the shade would land on top of the text it exists to support. */
.m-comment, .m-credit { position: relative; }

.m-comment {
  text-align: center;
  font-family: var(--fd); font-weight: 700;
  font-size: clamp(15px, 1.5vw, 18px); line-height: 1.42; letter-spacing: -.012em;
  color: #fff;
  text-shadow: 0 2px 14px rgba(0,0,0,.65);
  word-break: keep-all; line-break: strict;
  /* <q> would otherwise add the browser's locale quotes on top of ours. */
  quotes: none;
}
.m-comment::before { content: '\201C'; }
.m-comment::after { content: '\201D'; }

/* Right-aligned against the card edge, and a fifth larger than the comment's supporting text
   was before — the credit is what tells you whose sentence you just read. */
.m-credit { display: flex; flex-direction: column; gap: 2px; text-align: right; }
.m-credit b { font-family: var(--fd); font-weight: 600; font-size: 15.5px; color: #fff; }
.m-credit i { font-style: normal; font-size: 15px; color: rgba(255,255,255,.74); }
.m-credit em {
  font-style: normal; margin-top: 7px;
  font-family: var(--fd); font-size: 14px; font-weight: 600; letter-spacing: .01em;
  color: var(--primary);
}

/* Below the 4-column breakpoint a card is about 160px wide, where the credit's desktop size
   breaks song titles onto two lines and swamps the quote. The larger type is a desktop
   decision; here it steps back down and the padding gives the text its width back. */
@media (max-width: 719px) {
  .m-text { padding: 13px; gap: 10px; }
  .m-comment { font-size: 13.5px; line-height: 1.38; }
  .m-credit b { font-size: 13px; }
  .m-credit i { font-size: 12.5px; }
  .m-credit em { font-size: 12px; margin-top: 5px; }
}

@media (prefers-reduced-motion: reduce) {
  .moment:hover { transform: none; }
  .moment:hover .m-art { transform: none; }
}

/* ── 게시물 수치 ───────────────────────────────────────────────────────────────────────
 * One figure, undecorated, on its own. Posts by real users is the only headline number this
 * service can state honestly: 511 of 1,180 accounts are operational helpers, and the
 * `activation` schedulers make 93% of all likes — so likes and views are unusable here.
 * scripts/content.py recounts it from the analytics cache on every build. */
.figure { padding-block: clamp(56px, 8vw, 92px); }
.stat { text-align: center; display: grid; justify-items: center; gap: 6px; }
.stat .pre { font-size: clamp(14px, 1.6vw, 16px); color: var(--tx2); }
/* Empty on the English page, where the sentence needs no lead-in. */
.stat .pre:empty { display: none; }
.stat b {
  font-family: var(--fd); font-weight: 800;
  font-size: clamp(44px, 8vw, 84px); line-height: 1.05; letter-spacing: -.035em;
  font-variant-numeric: tabular-nums;
  background: var(--gradient);
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
}
.stat .post {
  margin-top: 4px;
  font-size: clamp(15px, 1.8vw, 17px); color: var(--tx2);
  word-break: keep-all; line-break: strict;
}

