/* ══════════════════════════════════════════════════════════════════════════════════════════════
   PIF - Apple-design refinements   (production)

   ADDITIVE ON PURPOSE. It loads AFTER style.css, so every rule is an override. Removing the two
   tags that pull this file and its script reverts the site exactly - that is the rollback.
   Trialled at /next/ before promotion (2026-09-05).

   Source: emilkowalski/skills -> skills/apple-design/SKILL.md (MIT). The section numbers are that
   document's own, and the spring values are Apple's published ones rather than invented.
   ══════════════════════════════════════════════════════════════════════════════════════════════ */

:root {
    /* §4 Behaviour over animation - springs expressed as damping ratio + response time.
       CSS has no spring primitive, but linear() samples an arbitrary curve, so these ARE the
       springs rather than a cubic-bezier that merely resembles one. Generated from
       x(t) = 1-(1+wt)e^-wt for the critically damped case and the underdamped form otherwise,
       with w = 2*pi/response, sampled to the point where motion settles. */

    /* damping 1.0, response 0.4s - move / reposition. No overshoot: the default for most UI. */
    --spring-move-ms: 446ms;
    --spring-move: linear(0.0000, 0.0207, 0.0719, 0.1407, 0.2184, 0.2987, 0.3776, 0.4526, 0.5221, 0.5855, 0.6424, 0.6929, 0.7374, 0.7763, 0.8100, 0.8391, 0.8641, 0.8855, 0.9037, 0.9192, 0.9323, 0.9434, 0.9528, 0.9606, 0.9672, 0.9727, 0.9773, 0.9812, 0.9844, 0.9871, 0.9893, 0.9912, 1.0000);

    /* damping 0.8, response 0.4s - peaks at 1.0152, i.e. a 1.5% overshoot. Reserved for motion a
       gesture actually carried momentum into; using it everywhere is what makes a site feel cheap. */
    --spring-bounce-ms: 557ms;
    --spring-bounce: linear(0.0000, 0.0323, 0.1114, 0.2159, 0.3306, 0.4449, 0.5522, 0.6483, 0.7316, 0.8015, 0.8586, 0.9040, 0.9390, 0.9653, 0.9844, 0.9977, 1.0064, 1.0116, 1.0143, 1.0152, 1.0148, 1.0137, 1.0121, 1.0104, 1.0087, 1.0070, 1.0055, 1.0042, 1.0032, 1.0023, 1.0015, 1.0010, 1.0000);

    /* damping 0.8, response 0.3s - drawers and sheets. */
    --spring-sheet-ms: 418ms;
    --spring-sheet: linear(0.0000, 0.0323, 0.1114, 0.2159, 0.3306, 0.4449, 0.5522, 0.6483, 0.7316, 0.8015, 0.8586, 0.9040, 0.9390, 0.9653, 0.9844, 0.9977, 1.0064, 1.0116, 1.0143, 1.0152, 1.0148, 1.0137, 1.0121, 1.0104, 1.0087, 1.0070, 1.0055, 1.0042, 1.0032, 1.0023, 1.0015, 1.0010, 1.0000);

    --press-ms: 100ms;
}

/* ── §1 RESPONSE - kill latency ────────────────────────────────────────────────────────────────
   "Respond on pointer-down, not release." Measured on the live site: 49 click listeners against 3
   pointerdown. Nothing acknowledges a press until the click handler has already run, so the whole
   event cycle reads as lag. A pressed state costs nothing and removes all of that perceived delay.
   apple-refinements.js adds .pif-pressed on pointerdown and clears it on pointerup/cancel/leave. */
.pif-pressable {
    transition: transform var(--press-ms) ease-out, filter var(--press-ms) ease-out;
    /* The browser's own grey tap flash would double up with this one, and it fires on a delay
       rather than instantly - which is the very thing being removed. */
    -webkit-tap-highlight-color: transparent;
}

/* Strengthened after the first version was reported as invisible. 2.8% and a 12% brightness lift
   are what Apple uses, but Apple pairs them with a light UI where a brightness change is obvious.
   On a near-black surface a brightness multiplier has almost nothing to multiply, so the same
   numbers read as nothing at all. The lift is now additive - an overlay that does not depend on how
   dark the element already is - plus a slightly deeper scale. */
.pif-pressed {
    transform: scale(0.96);
    filter: brightness(1.16) saturate(1.06);
    box-shadow: inset 0 0 0 999px rgba(255, 255, 255, 0.07);
}

/* The same percentage reads as far too much on a large surface - scale the effect to the target. */
/* A large surface needs a smaller percentage to read as the same amount of movement. */
.gig-card.pif-pressed,
.collab-card.pif-pressed,
.item-card.pif-pressed,
.gateway-card.pif-pressed {
    transform: scale(0.985);
    filter: brightness(1.08);
    box-shadow: inset 0 0 0 999px rgba(255, 255, 255, 0.05);
}

/* Touch needs a DIFFERENT press than a mouse. A fingertip covers roughly a 45px circle, so on a
   small control the scale happens underneath it and is simply not seen. What stays visible is the
   area around the finger, so on touch the surface itself brightens and gains a ring instead.
   (hover: none) is safe HERE where (hover: hover) was not, because this only decides which of two
   press treatments to paint - it never gates whether the press happens at all. */
@media (hover: none) {
    .pif-pressed {
        transform: none;
        filter: brightness(1.2);
        box-shadow: inset 0 0 0 999px rgba(255, 255, 255, 0.10),
                    inset 0 0 0 2px rgba(255, 255, 255, 0.30);
    }

    .gig-card.pif-pressed,
    .collab-card.pif-pressed,
    .item-card.pif-pressed {
        transform: scale(0.988);
        filter: brightness(1.1);
    }
}

/* ── §4 BEHAVIOUR - springs where motion is watched to a stop ──────────────────────────────────
   Only transitions a person actually watches resolve are switched. Decorative fades keep their
   existing easing, which keeps this a small and reviewable diff. */
.pif-ui .cvr-layer-b,
.pif-ui .collab-cover-img.collab-cover-next {
    transition: opacity var(--spring-move-ms) var(--spring-move) !important;
}

.pif-ui .event-modal-content,
.pif-ui .modal-content {
    transition: transform var(--spring-sheet-ms) var(--spring-sheet),
                opacity var(--spring-sheet-ms) var(--spring-sheet) !important;
}

/* ── §9 RUBBER-BANDING - soft boundaries ───────────────────────────────────────────────────────
   A strip that stops dead at its end reads as broken rather than finished. Keeping the gesture
   inside the strip lets the browser's own elastic edge do the work. */
.pif-ui .collab-thumbs,
.pif-ui .gigs-grid,
.pif-ui .past-gigs-list {
    overscroll-behavior-x: contain;
}

/* ── §12 MATERIALS & DEPTH - scroll edge effects, not hard dividers ────────────────────────────
   "Use scroll edge effects (fade/gradient mask) instead of hard dividers." Text cut dead at a
   boundary gives no signal that more of it exists below. */
.pif-ui .collab-session-note,
.pif-ui .desc-body {
    -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 14px,
                                        #000 calc(100% - 14px), transparent 100%);
    mask-image: linear-gradient(to bottom, transparent 0, #000 14px,
                                #000 calc(100% - 14px), transparent 100%);
}

/* ── §15 TYPOGRAPHY - optical sizing ───────────────────────────────────────────────────────────
   "Tracking is size-specific, never one value for all sizes", and leading tracks inversely with
   size. Display type needs negative tracking; body copy needs none and wants air. */
.pif-ui h1,
.pif-ui .section-title,
.pif-ui .gateway-title {
    letter-spacing: -0.022em;
    line-height: 1.04;
}

.pif-ui h2,
.pif-ui h3,
.pif-ui .collab-name,
.pif-ui .item-title {
    letter-spacing: -0.012em;
    line-height: 1.18;
}

.pif-ui p,
.pif-ui .collab-desc,
.pif-ui .desc-body,
.pif-ui .collab-session-note {
    letter-spacing: 0;
    line-height: 1.62;
}

/* ── §14 REDUCED MOTION & ACCESSIBILITY ────────────────────────────────────────────────────────
   The live site answers prefers-reduced-motion in 8 places and the other two queries nowhere at
   all. These are not decoration: they are how the operating system reports that this site's glass
   surfaces are unreadable for the person in front of it. */
@media (prefers-reduced-motion: reduce) {
    .pif-ui *,
    .pif-ui *::before,
    .pif-ui *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }

    /* "Drop elastic and overshoot in reduced-motion mode" - but a cross-fade still communicates
       that something changed, so the slide dissolve is kept and merely de-sprung. */
    .pif-ui .cvr-layer-b,
    .pif-ui .collab-cover-img.collab-cover-next {
        transition: opacity 200ms ease !important;
    }
}

@media (prefers-reduced-transparency: reduce) {
    /* "Raise background opacity, drop blur." 29 backdrop-filters on this site are illegible for
       anyone who has asked the OS to stop rendering them. */
    .pif-ui .nav-bar,
    .pif-ui .event-modal-content,
    .pif-ui .modal-content,
    .pif-ui [class*="glass"] {
        backdrop-filter: none !important;
        -webkit-backdrop-filter: none !important;
        background-color: rgba(21, 21, 21, 0.97) !important;
    }

    .pif-ui .cvr-wash {
        opacity: 0.45;
    }
}

@media (prefers-contrast: more) {
    /* "Near-solid backgrounds and defined borders." */
    .pif-ui .nav-bar,
    .pif-ui .event-modal-content,
    .pif-ui .modal-content,
    .pif-ui .gig-card,
    .pif-ui .collab-card {
        background-color: #0b0b0b !important;
        border: 1px solid rgba(255, 255, 255, 0.42) !important;
        backdrop-filter: none !important;
        -webkit-backdrop-filter: none !important;
    }

    .pif-ui a,
    .pif-ui button {
        text-decoration-thickness: 2px;
    }
}


/* ══════════════════════════════════════════════════════════════════════════════════════════════
   SCROLL REVEAL - the visible one
   Cards rise and fade as they enter the viewport, staggered along a row. This is the change that
   actually reads as "this site was made in 2026"; everything above it is craft you feel rather
   than see.

   ⚠️ FAIL-OPEN BY CONSTRUCTION. The hiding rule is gated on `.pif-reveal-on`, a class the SCRIPT
   adds to <html> only once its observer is live, and the script also force-reveals everything
   after a 2s deadline. So a JS error, a blocked file or a browser that lacks
   IntersectionObserver leaves every card fully visible rather than blanking the page. Never write
   this the other way round - `opacity: 0` in a stylesheet that JS is expected to undo is one
   broken request away from an empty site.
   ══════════════════════════════════════════════════════════════════════════════════════════════ */
.pif-reveal-on .pif-reveal {
    opacity: 0;
    transform: translateY(16px);
    /* Long and eased-out: the motion should be finishing as your eye arrives, not starting.
       transform+opacity only, so this stays on the compositor and costs no layout. */
    transition: opacity 620ms cubic-bezier(0.22, 0.61, 0.36, 1),
                transform 620ms cubic-bezier(0.22, 0.61, 0.36, 1);
    will-change: opacity, transform;
}

.pif-reveal-on .pif-reveal.is-in {
    opacity: 1;
    transform: none;
    /* Dropped once the element has arrived - a permanent will-change pins a compositor layer for
       every card on the page, which is exactly the cost this animation is not worth paying. */
    will-change: auto;
}

/* A heading leads its own group, so it rises a little further and lands first. */
.pif-reveal-on .section-title.pif-reveal {
    transform: translateY(22px);
}

/* §14: someone who asked the OS for less motion gets the content, immediately and in place. */
@media (prefers-reduced-motion: reduce) {
    .pif-reveal-on .pif-reveal,
    .pif-reveal-on .pif-reveal.is-in {
        opacity: 1 !important;
        transform: none !important;
        transition: none !important;
    }
}

/* ══════════════════════════════════════════════════════════════════════════════════════════════
   BREATHING ROOM - measured, not guessed
   Two complaints, both correct, both confirmed by measurement at 375x812.
   ══════════════════════════════════════════════════════════════════════════════════════════════ */

/* ── 1. The PARTY grid is stuck to the edges ───────────────────────────────────────────────────
   Measured: `#party` has 12px of side padding and the card lands 13px from the screen edge, while
   a section heading sits just 18px above the poster it introduces. On a 375px phone a 12px gutter
   reads as "the image is falling off the screen", and a 25px-tall heading needs more than 18px
   under it or it looks glued to the content.
   These are the only two numbers changed - the grid's own 24px gap between cards was already right
   and is left alone. */
/* ⚠️ `!important` here is not laziness. The base rule is
   `.content-section { padding: 60px 12px 220px !important }` (style.css:2304), an important
   SHORTHAND - measured: even an inline `padding-left: 18px` on the element still computed 12px.
   There is no way to change one side of that without matching it. Longhands are used so the
   shorthand's top and bottom (60/220) survive untouched; only the gutter moves. */
@media (max-width: 767px) {
    .pif-ui .content-section {
        padding-left: 18px !important;
        padding-right: 18px !important;
    }
}

/* The middle breakpoint sits at 16px, which is fine but a touch mean beside the new 18. */
@media (min-width: 768px) and (max-width: 1024px) {
    .pif-ui .content-section {
        padding-left: 22px !important;
        padding-right: 22px !important;
    }
}

.pif-ui .section-title {
    /* 18px -> 28px. A heading owns the space beneath it; that is what makes it read as a heading
       rather than as the first line of the block. */
    margin-bottom: 28px;
}

/* ── 2. The player bar: four type sizes, ONE weight ────────────────────────────────────────────
   Measured inside a 69px bar: font sizes 8.48px, 9.28px, 12px and 12.48px - and every single one
   at weight 700. So hierarchy was being attempted with size alone, in a band too short for four
   steps to be distinguishable, which is exactly what reads as cluttered.
   §15: "Build hierarchy from weight + size + leading as a unified set."
   Two sizes now, separated by WEIGHT rather than by a third and fourth size. 8.48px also sits
   below any sensible legibility floor - nothing here goes under 11px. */
.pif-ui .pcast-ep-title,
.pif-ui .pcast-marquee-track {
    font-size: 12.5px;
    font-weight: 600;          /* was 700 - the title no longer shouts at the same volume as a timestamp */
    letter-spacing: -0.005em;  /* §15: tighten as size grows, and this is the largest text here */
}

/* The real class is `.pcast-time-display`, and the size lives on the PARENT - the spans inside it
   inherit. Measured: 0.68rem at rest, 0.6rem under the mobile breakpoint = 9.28px, which is below
   any legibility floor. Targeting `.pcast-time` / `time` / `.pcast-dur` matched nothing at all. */
.pif-ui .podcast-player .pcast-time-display {
    font-size: 11px;           /* was 9.28px */
    font-weight: 500;          /* was 700 - a timestamp is reference, not emphasis */
    letter-spacing: 0.02em;    /* digits read better with a hair of air */
    font-variant-numeric: tabular-nums;  /* so the counter stops jittering as the seconds change */
}

.pif-ui .pcast-seek-num {
    font-size: 11px;           /* was 8.48px, which is simply not readable */
    font-weight: 600;
}

/* ⚠️ The info column is only 90px wide on a phone (measured: player 357px = thumb 40 + info 90 +
   controls 110 + actions 67 + padding), and it holds the timestamp AND the scrolling title. At
   11px the "00:00 / 00:00" pair measured 82px, leaving 8px for everything else - so raising the
   size to a comfortable reading floor was, at this width, taking room the title needed.
   10.5px with no extra tracking is the compromise: still far above the 9.28px it was, and back
   inside a width the column can actually give. Wider screens keep the full 11px. */
@media (max-width: 767px) {
    .pif-ui .podcast-player .pcast-time-display {
        font-size: 10.5px;
        letter-spacing: 0;
    }
}

/* The bar had padding: 0. Room on the sides, and clearance from the phone's home indicator. */
.pif-ui .podcast-player {
    padding: 0 14px;
}

@media (max-width: 767px) {
    .pif-ui .podcast-player {
        padding: 0 12px max(0px, env(safe-area-inset-bottom));
    }
}

/* Measured button sizes: 28, 34, 28, 32, 27 - five near-identical values that never quite agree.
   One scale instead: 30 for the secondary controls, 38 for the primary. Colour and fill are NOT
   touched here - the docked bubble pins its own with !important and fighting that is a documented
   dead end. */
.pif-ui .podcast-player .pcast-btn {
    width: 30px;
    height: 30px;
}

.pif-ui .podcast-player .pcast-btn.play-main {
    width: 38px;
    height: 38px;
}

/* ── 접힌 플레이어의 되돌리기 ────────────────────────────────────────────────────────────────
   접힌 상태는 style.css:2865-2879 의 56x56 버블이고, 가운데를 38px 재생 버튼이 채웁니다.
   되돌리는 컨트롤은 style.css:2962-2975 에서 그 위 우측 모서리에 18x18 로 얹혀 있어 손가락으로
   겨냥하기 어려웠습니다.

   시도했다가 버린 두 가지를 남겨 둡니다.
   - 배지를 25px 로 키우기: 재생 버튼을 가립니다. 56px 안에 38px 원과 25px 표적이 겹치지 않게
     들어갈 자리가 없습니다.
   - 가로 알약으로 펼치기: 폭이 357px 로 늘어나 화면을 거의 다 덮었고, 접는 목적 자체가
     사라졌습니다.

   그래서 표적을 버튼 하나에 의존하지 않습니다. 재생 버튼을 제외한 버블 전체(아트워크 레이어)가
   되돌리기로 동작하고(js 의 togglePodcastHeight), 모서리 배지는 그 사실을 알리는 표시로만
   남깁니다. 아트워크는 position:absolute inset 0 이고 재생 버튼이 있는 .pcast-controls 는
   z-index 2 라, 재생을 누른 클릭은 아래로 새지 않습니다.

   배지는 보이는 크기를 키우는 대신 히트 영역만 넓힙니다. 크기는 그대로라 재생 버튼을 가리지
   않으면서, 실제로 눌리는 범위는 24px 을 넘깁니다. */
/* 56px 정사각 안에 36px 원이 있으면, 배지를 어느 모서리에 놔도 원의 호에 닿습니다. 실측에서
   20px 배지도 재생 버튼 우상단을 물었습니다. 그래서 배지를 버블 바깥으로 빼냅니다. 접힌
   플레이어는 overflow:hidden 이었으므로 그것도 함께 풉니다. 아트워크의 둥근 모서리는
   .pcast-thumb-wrap 자신의 border-radius 가 유지하므로 잘림이 필요 없습니다. */
.pif-ui .podcast-player.docked {
    overflow: visible !important;
}

/* .pcast-actions 컨테이너에 오프셋을 주면 그 안에서 버튼이 다시 밀려 실측상 위로만 나가고
   오른쪽으로는 나가지 않았다. 컨테이너를 static 으로 되돌리고 버튼 자체를 버블 모서리에
   절대 배치한다. 접힌 플레이어는 position:fixed 라 그대로 컨테이닝 블록이 된다. */
/* .pcast-compact-bar 는 style.css 가 position:relative + width:100% 로 잡아 둔 56px 박스이고,
   배지는 이걸 기준으로 붙는다. 한때 이걸 static 으로 바꿔 봤더니 폭이 30px 로 줄어 배지가
   버블 안쪽에 갇혔다(실측). 그대로 두는 것이 맞다. */

/* .pcast-actions 를 흐름 안에 두면 배지의 레이아웃 박스가 flex 행에서 폭을 차지해 재생 버튼이
   왼쪽으로 밀린다(실측: 버블 중심 329 대 버튼 중심 325, 4px 어긋남). style.css 가 원래 주던
   absolute 를 그대로 살려 흐름에서 빼고, 재생 버튼이 정사각형 정중앙에 오게 한다. */
.pif-ui .podcast-player.docked .pcast-actions {
    position: absolute !important;
    top: 0 !important;
    right: 0 !important;
}

.pif-ui .podcast-player.docked .pcast-actions .side-dock-btn {
    position: relative !important;
    top: auto !important;
    right: auto !important;
    z-index: 4 !important;
    width: 22px !important;
    height: 22px !important;
    font-size: 0.62rem !important;
    background: #0b0b0b !important;
    border-color: rgba(255, 255, 255, 0.55) !important;
    color: #fff !important;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.9) !important;
}

.pif-ui .podcast-player.docked .pcast-actions .side-dock-btn::before {
    content: '';
    position: absolute;
    inset: -6px;
}

/* 아트워크가 눌리는 면이라는 것을 커서로도 알린다. */
.pif-ui .podcast-player.docked .pcast-thumb-wrap {
    cursor: pointer;
    opacity: 0.45;
}

/* 회전 규칙은 아이콘 교체(js)와 겹쳐 방향을 헷갈리게 하므로 되돌린다. */
.pif-ui .podcast-player.docked #pcast-dock-icon {
    transform: none;
}

/* 배지를 모서리 밖으로 걸치는 마지막 한 걸음은 transform 으로 한다.
   top/right 로는 원하는 자리에 오지 않았다. 이 버튼의 컨테이닝 블록이 .pcast-compact-bar 인데
   접힌 상태에서 그 박스의 계산 폭이 56px 이 아니라 30px 로 나오기 때문이다(실측). 그래서
   right:-7px 이 버블 기준이 아니라 30px 박스 기준으로 걸려 배지가 안쪽에 갇혔다.
   transform 은 배치가 끝난 뒤 그려진 결과를 옮기므로 컨테이닝 블록이 무엇이든 영향을 받지
   않는다. 결과적으로 배지가 우상단을 안팎으로 걸친다. */
.pif-ui .podcast-player.docked .pcast-actions .side-dock-btn {
    transform: translate(21px, -10px);
}
