/* ══════════════════════════════════════════════════════════════════════════════════════════════
   casepeat.com - 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 .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. */
/* ── §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. */
/* ── §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 .hero-title,
.pif-ui .section-title {
    letter-spacing: -0.022em;
    line-height: 1.04;
}

.pif-ui h2,
.pif-ui h3,
.pif-ui .card-title {
    letter-spacing: -0.012em;
    line-height: 1.18;
}

.pif-ui p,
.pif-ui .bio,
.pif-ui .desc {
    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. */
}

@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;
    }

}

@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
   Ported from PIF, minus everything that does not exist here: this site has no `.content-section`
   and no player bar, so only the heading rhythm applies. Shipping the rest would be dead CSS.
   ══════════════════════════════════════════════════════════════════════════════════════════════ */
.pif-ui .section-title,
.pif-ui h2 {
    /* 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;
}
