:root {
    --header-height: 54px;
    --gutter: clamp(16px, 1vw, 32px);
    --case-transition: 28vh;
    --font-size: 14px;
    --font-weight: 500;
    --letter-spacing: -0.03em;
    /* Shared grid so the header tagline and the hub's ДЕЛАЮ column share one
       left axis. The lead (left) column flexes; centre + showreel sit right.
       The 46vw basis keeps ДЕЛАЮ near the screen centre across 1024–1680, and
       the column keeps stretching with the viewport up to the cap. */
    --grid-lead: clamp(300px, 46vw, 760px);
    --grid-gap: clamp(24px, 4vw, 72px);
}

* {
    box-sizing: border-box;
}

/* Show the focus ring only for keyboard navigation. Tap / click / programmatic
   focus (e.g. the close button getting focus when a modal opens) no longer draws
   the blue outline. */
:focus:not(:focus-visible) {
    outline: none;
}

/* Touch devices (phones/tablets) have no keyboard navigation, so suppress the
   focus ring entirely — it only ever appears from a tap / the programmatic focus
   on modal open, which looked off. Desktop keeps :focus-visible above. */
@media (hover: none) {
    :focus {
        outline: none;
    }
}

html {
    scroll-behavior: smooth;
    /* Stop iOS Safari from auto-inflating text when the layout width changes
       (e.g. rotating to landscape for a video, then back) — that bug bumped the
       case description up a size until reload. */
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
}

body {
    margin: 0;
    background-color: #ffffff;
    color: #111;
    font-family: Arial, Helvetica, sans-serif;
    font-size: var(--font-size);
    font-weight: var(--font-weight);
    letter-spacing: var(--letter-spacing);
    line-height: 1.25;
    transition: background-color 450ms ease;
}

main,
.site-footer,
.case-nav {
    transition: filter 180ms ease;
}

body.contact-popup-open main,
body.contact-popup-open .site-footer,
body.contact-popup-open .case-nav {
    filter: blur(40px);
}

a {
    color: inherit;
    text-decoration: none;
}

button {
    color: inherit;
    font: inherit;
}

.site-header {
    position: fixed;
    z-index: 100;
    top: 0;
    left: 0;
    display: grid;
    grid-template-columns: var(--lead-x, 46vw) minmax(0, 1fr) auto;
    column-gap: 0;
    width: 100%;
    height: var(--header-height);
    align-items: center;
    padding: 0 var(--gutter);
    color: #fff;
    mix-blend-mode: difference;
}

.site-header p {
    margin: 0;
}

.wordmark {
    display: inline-flex;
    align-items: baseline;
    justify-self: start;
    white-space: nowrap;
    font-weight: 700;
    text-transform: uppercase;
    font-size: calc(var(--font-size) - 1px);
    letter-spacing: calc(-0.03em + 0.3px);
}

.wm__reveal,
.wm__dash,
.wm__gap {
    display: inline-block;
    overflow: hidden;
    min-width: 0;
    flex: 0 0 auto;
}

.wm__reveal {
    max-width: 0;
    opacity: 0;
    transition: max-width 380ms cubic-bezier(0.22, 0.61, 0.36, 1), opacity 260ms ease;
}

.wm__dash {
    max-width: 3em;
    white-space: pre;
    transition: max-width 320ms ease, opacity 220ms ease;
}

.wm__gap {
    max-width: 0;
    white-space: pre;
    transition: max-width 380ms ease;
}

.wordmark:hover .wm__reveal {
    max-width: 6em;
    opacity: 1;
}

.wordmark:hover .wm__reveal--last {
    transition-delay: 130ms;
}

.wordmark:hover .wm__dash {
    max-width: 0;
    opacity: 0;
}

.wordmark:hover .wm__gap {
    max-width: 0.5em;
}

.header-contacts {
    grid-column: 3;
    justify-self: end;
    display: flex;
    align-items: center;
    gap: 18px;
}

.header-contacts a {
    font-size: calc(var(--font-size) - 1px);
    line-height: 1.1;
    text-transform: uppercase;
    letter-spacing: calc(-0.03em + 0.3px);
    transition: filter 220ms ease;
}

.header-contacts a:hover {
    filter: blur(5px);
}

.header-backtotop {
    grid-column: 3;
    justify-self: end;
    display: none;
    font-size: calc(var(--font-size) - 1px);
    line-height: 1.1;
    text-transform: uppercase;
    letter-spacing: calc(-0.03em + 0.3px);
    transition: filter 220ms ease;
}

.header-backtotop:hover {
    filter: blur(5px);
}

/* On the footer screen the social links become "Back to top". */
body.at-footer .header-contacts {
    display: none;
}

body.at-footer .header-backtotop {
    display: block;
}

.contact-menu {
    justify-self: end;
}

.contact-menu__button {
    padding: 0;
    border: 0;
    background: transparent;
    cursor: pointer;
    text-transform: uppercase;
    font-size: calc(var(--font-size) - 1px);
    transition: filter 220ms ease;
}

.contact-menu__panel {
    position: fixed;
    z-index: 120;
    top: 50%;
    left: 50%;
    display: grid;
    min-width: min(320px, calc(100vw - 40px));
    gap: 14px;
    padding: 24px;
    font-size: var(--font-size);
    background: rgba(255, 255, 255, 1);
    transform: translate(-50%, -50%) scale(1);
    animation: contact-popup-in 580ms ease-out;
}

.contact-menu__panel[hidden] {
    display: none;
}

.contact-menu__panel a {
    white-space: nowrap;
    transition: filter 220ms ease;
}

@keyframes contact-popup-in {
    from {
        opacity: 0;
        transform: translate(-50%, -46%) scale(0.96);
    }

    to {
        opacity: 1;
        transform: translate(-50%, -50%) scale(1);
    }
}

.hero {
    position: relative;
    display: grid;
    /* left = name+intro (flexes/shrinks); then lists, a gap, and the showreel —
       the right cluster glides left as a unit while the left text narrows. */
    grid-template-columns: minmax(0, 1fr) auto clamp(80px, 10vw, 200px) clamp(240px, 19vw, 340px);
    column-gap: 0;
    align-content: end; /* bottom-anchored: as height shrinks, content moves up */
    align-items: start; /* name / ДЕЛАЮ / video top share one line */
    min-height: 100vh;
    /* Bottom padding leaves a gap below the lists equal to the column rhythm. */
    padding: calc(var(--header-height) + 4vh) var(--gutter) calc(var(--hub-gap, 72px) + var(--gutter) + 20px);
    background: transparent;
    /* Rises into place as the preloader wipes up (same timing as the wipe). */
    transition: transform 820ms cubic-bezier(0.76, 0, 0.24, 1);
}

/* Held 100px low while the preloader covers the screen; released on wipe. */
body.hero-hold .hero {
    transform: translateY(100px);
}

/* ---- Hub (the whole single-page landing) ---- */
.hub__intro,
.hub__lists,
.hub__showcase {
    position: relative;
    z-index: 1; /* above the particle canvas */
    align-self: start;
}

.hub__intro {
    /* 440 text + 60 gap up to ~1600px, then widens so the intro wraps to fewer
       lines on large screens. The clamp is offset by the 60px padding so the
       text width is unchanged — only the gap to ДЕЛАЮ grows. */
    max-width: clamp(500px, calc(30vw + 38px), 708px);
    padding-right: 60px; /* gap between the left text and the ДЕЛАЮ column */
}

.hub__name {
    margin: 0; /* bottom offset set by alignHub() to meet the caption row */
    font-size: clamp(34px, 3vw, 44px);
    font-weight: var(--font-weight);
    line-height: 1;
}

.hub__desc {
    margin: 0;
    max-width: none; /* width is governed by .hub__intro (clamp + 32 padding) */
    font-size: var(--font-size);
}

.hub__lists {
    display: flex;
    flex-direction: column;
    gap: var(--hub-gap, 48px); /* equal spacing between blocks, tuned by alignHub() */
}

.hub__label {
    display: block;
    margin-bottom: var(--hub-gap, 48px); /* same gap as between the blocks */
    text-transform: uppercase;
    letter-spacing: calc(-0.03em + 0.3px);
}

.hub__projects,
.hub__skills {
    margin: 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
}

.hub__projects {
    gap: 0; /* spacing comes from the link padding so hit areas stay contiguous */
}

.hub__project {
    display: block;
    width: fit-content;
    /* 1px top+bottom keeps the 2px visual rhythm AND makes adjacent links touch,
       so there's no dead strip between items to miss-click into. */
    padding: 1px 0;
    text-transform: uppercase;
    letter-spacing: calc(-0.03em + 0.3px);
    transition: opacity 220ms ease, filter 220ms ease;
}

/* Blur the others only when an actual project is hovered (not just anywhere in
   the list area). */
.hub__projects:has(.hub__project:hover) .hub__project:not(:hover) {
    opacity: 0.55;
    filter: blur(5px);
}

.hub__skills li {
    position: relative;
    padding-left: 42px; /* gap from the bullet to the text */
    font-size: var(--font-size);
}

.hub__skills li::before {
    content: "•";
    position: absolute;
    left: 0;
}

.hub__showcase {
    grid-column: 4; /* skip the spacer column (3) */
    display: flex;
    flex-direction: column;
    gap: var(--gutter); /* caption sits a gutter below the video */
    width: 100%;
}

.hub__media {
    position: relative;
    width: 100%;
    margin: 0; /* reset default <figure> margin (40px sides / 1em top-bottom) */
    cursor: pointer;
    transition: filter 220ms ease;
}

/* Hidden full-quality showreel, kept in the DOM with its metadata preloaded so
   a phone tap can hand it straight to the OS fullscreen player. Must not be
   display:none (iOS won't fullscreen a detached/undisplayed video). */
.hub__showreel-fs {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    pointer-events: none;
}

/* Hover blurs the showreel — the same hover treatment used across the site. */
.hub__showcase:not(.is-previewing) .hub__media:hover {
    filter: blur(5px);
}

.hub__showreel {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
    background: #000;
    /* Fades in whenever it returns (after a cover is dismissed), not abruptly. */
    animation: hub-showreel-in 640ms ease;
}

@keyframes hub-showreel-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}

/* The hovered cover is small and shares the video's top-left corner. Two
   stacked layers crossfade, so posters replace each other smoothly with no
   flicker or empty frame when moving between projects. */
.hub__cover {
    display: none;
    position: relative;
    width: 60%;
    aspect-ratio: 1 / 1.414;
}

.hub__cover-img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    opacity: 0; /* only layers that have received a poster are shown */
}

.hub__cover-img.is-shown {
    opacity: 1;
}

/* Mask reveal: the incoming poster sits on top and its mask opens downward
   over the previous one — the image itself stays put, only the edge moves. */
.hub__cover-img.is-top {
    z-index: 2;
    animation: hub-cover-reveal 322ms cubic-bezier(0.76, 0, 0.24, 1);
}

@keyframes hub-cover-reveal {
    from { clip-path: inset(0 0 100% 0); }
    to   { clip-path: inset(0 0 0 0); }
}

.hub__showcase.is-previewing .hub__showreel {
    display: none;
}

.hub__showcase.is-previewing .hub__cover {
    display: block;
}

/* Fixed 16px gap from the hover poster to its caption (the video state keeps
   the responsive gutter gap). The caption's alignHub margin-top — which drops it
   onto the intro/list baseline in the video state — must be cancelled here so
   the poster→caption distance is exactly the 16px gap, nothing more. */
.hub__showcase.is-previewing {
    gap: 16px;
}

.hub__showcase.is-previewing .hub__caption {
    margin-top: 0 !important;
}

.hub__caption {
    display: flex;
    justify-content: space-between;
    margin: 0;
    font-size: calc(var(--font-size) - 1px);
}

.hub__about,
.hub__year {
    position: absolute;
    z-index: 1;
    bottom: var(--gutter);
    font-size: calc(var(--font-size) - 1px);
    text-transform: uppercase;
    letter-spacing: calc(-0.03em + 0.3px);
}

.hub__about {
    left: var(--gutter);
    transition: filter 220ms ease;
}

.hub__about:hover {
    filter: blur(5px);
}

.hub__year {
    right: var(--gutter);
}

.hero__particles {
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    /* Multiply so the dotted pattern darkens through the hover background colour
       instead of sitting on top of it as flat grey. */
    mix-blend-mode: multiply;
}

.hero__intro {
    display: flex;
    flex-direction: column;
    transform: translateY(-7vh);
    position: relative;
    z-index: 1;
}

.hero__name {
    margin: 0 0 40px;
    font-size: 44px;
    font-weight: var(--font-weight);
    line-height: 1;
}

.hero__desc {
    max-width: 460px;
    margin: 0 0 clamp(28px, 4vh, 56px);
    font-size: var(--font-size);
}

.hero__skills {
    margin-top: auto;
    display: grid;
    grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.8fr);
    gap: clamp(40px, 3.4vw, 72px);
    padding-right: var(--gutter);
}

.hero__skills ul {
    margin: 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 0;
}

.hero__skills li {
    position: relative;
    padding-left: 14px;
    font-size: var(--font-size);
}

.hero__skills li::before {
    content: "•";
    position: absolute;
    left: 0;
}

.hero__showcase {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: var(--gutter);
    transform: translateY(-7vh);
    position: relative;
    z-index: 1;
}

.hero__meta {
    display: none;
    flex-direction: column;
    justify-content: space-between;
    align-self: stretch;
    white-space: nowrap;
}

.hero__meta-time {
    transform: translateY(3px);
}

@media (max-width: 780px) {
    .hero {
        grid-template-columns: 1fr;
        row-gap: 80px;
        align-content: start;
    }

    .hero__video {
        width: 100%;
        justify-self: stretch;
    }

    .hero__skills {
        margin-top: 0;
    }
}

/* ≤820 (tablet portrait & down): stack the hub — name + intro on top (full
   width), then the ДЕЛАЮ/УМЕЮ lists pinned left and the showreel pinned right. */
@media (max-width: 820px) {
    .hero {
        grid-template-columns: minmax(0, 1fr) clamp(220px, 40vw, 360px);
        grid-template-rows: auto auto;
        column-gap: var(--gutter);
        row-gap: clamp(48px, 8vh, 96px);
        align-content: end;
        align-items: start;
    }

    .hub__intro {
        grid-column: 1 / -1; /* spans the top row */
        grid-row: 1;
        max-width: calc(100vw * 2 / 3); /* text column = 2/3 of the screen */
        padding-right: 0;
    }

    .hub__name {
        margin-bottom: 24px; /* gap between the name and the intro text */
    }

    .hub__lists {
        grid-column: 1; /* left */
        grid-row: 2;
    }

    .hub__showcase {
        grid-column: 2; /* right, pinned to the edge */
        grid-row: 2;
        justify-self: end;
        align-self: start;
        width: 100%;
    }
}

/* The under-name tagline and the mobile project cards are desktop-hidden; the
   mobile homepage (≤500) below switches them on. */
.hub__tagline {
    display: none;
}

.hub__mobile {
    display: none;
}

/* ===== Mobile homepage (≤500): a normal vertical scroll ====================
   First screen = the hero, one viewport tall, showreel pinned to the bottom.
   Below it, the projects become centred poster cards (#hub-mobile). The tagline
   moves under the name; ДЕЛАЮ/УМЕЮ labels are hidden; the particle field is a
   fixed background the cards scroll over. */
@media (max-width: 500px) {
    .site-header p {
        display: none; /* tagline leaves the header (it goes under the name) */
    }

    .hero__particles {
        position: fixed; /* a fixed background behind the scrolling cards */
    }

    .hero {
        display: flex;
        flex-direction: column;
        min-height: auto; /* not the desktop 100vh — content height, so the
                             projects flow right after the showreel (no dead space
                             once the showreel gap is capped) */
        align-content: normal;
        align-items: stretch;
        column-gap: 0;
        row-gap: 0;
        padding: 0 10px 10px; /* 10px gutters; the top gap is the ::before spacer */
    }

    /* Two height-driven spacers shape the first screen; both shrink as the
       viewport shortens, the bottom one first:
       ::after  = gap above the showreel — 150px max (the video never sits more
       than 150px below the skills, then the page just scrolls), down to 50px min;
       ::before = gap above the name — 150px, shrinking to 100px once ::after has
       hit its 50px minimum. */
    .hero::after {
        content: "";
        order: 3;
        flex: 0 0 auto;
        /* 120 max (skills → showreel never more than 120px apart), 50 min at
           ≤775 (725 = everything else: 150 top + 413 text + 152 showcase + 10). */
        height: clamp(50px, calc(100vh - 725px), 120px);
    }

    .hero::before {
        content: "";
        order: 0;
        flex: 0 0 auto;
        /* 150px, shrinking to 100px between viewport 775→725 — i.e. only after
           ::after has already collapsed to its 50px minimum. */
        height: clamp(100px, calc(100vh - 625px), 150px);
    }

    .hub__lists {
        display: contents; /* free the ДЕЛАЮ / УМЕЮ blocks for ordering */
    }

    .hub__label {
        display: none;
    }

    /* projects are shown as cards below the first screen, not here */
    .hub__block:has(#hub-projects) {
        display: none;
    }

    .hub__year {
        display: none; /* the year lives in the mobile footer instead */
    }

    .hub__intro {
        order: 1;
        max-width: none;
        padding-right: 0;
    }

    .hub__name {
        margin: 0 0 16px; /* name → tagline */
    }

    .hub__tagline {
        display: block;
        margin: 0 0 50px; /* tagline → intro text */
    }

    .hub__desc {
        margin: 0;
        max-width: none;
    }

    .hub__block:has(.hub__skills) {
        order: 2;
        margin-top: 50px; /* intro text → skills */
    }

    /* Showreel: a small 220×120 clip pinned to the bottom-right corner, caption
       above it. */
    .hub__showcase {
        order: 4; /* after the ::after spacer that pins it to the bottom */
        width: 220px;
        align-self: flex-end; /* right */
        flex-direction: column-reverse; /* caption above the video */
        gap: 16px; /* caption → video */
    }

    .hub__media {
        width: 220px;
    }

    .hub__showreel {
        width: 220px;
        height: 120px;
        aspect-ratio: auto;
    }

    /* About link: fixed, vertical, 10px from the left edge — stays while scrolling. */
    .hub__about {
        position: fixed;
        left: 10px;
        bottom: 16px;
        z-index: 2; /* above #hub-mobile (z-index 1) so it stays clickable */
        writing-mode: vertical-rl;
        transform: rotate(180deg);
    }

    /* Invisible 10px-larger hit area around the link — easier to tap, no change
       to the text or its position. */
    .hub__about::after {
        content: "";
        position: absolute;
        inset: -10px;
    }

    /* No hover-blur on mobile — a tap triggers sticky :hover and leaves the
       element blurred. Kill it for the About link, the header contacts, and the
       showreel. */
    .hub__about:hover,
    .header-contacts a:hover,
    .hub__showcase:not(.is-previewing) .hub__media:hover {
        filter: none;
    }

    /* Header items 10px from the screen edges (top + sides). */
    .site-header {
        height: auto;
        align-items: start;
        padding: 10px;
    }

    /* ---- Project cards below the first screen ---- */
    .hub__mobile {
        display: block;
        padding: 0 10px;
        position: relative;
        z-index: 1; /* above the fixed particle pattern (z-index 0) */
    }

    .hub__cards {
        display: flex;
        flex-direction: column;
        align-items: center;
        text-align: center;
        /* trailing space so the last poster sits centred at max scroll: half a
           viewport minus half the poster (164) minus the footer below (~84). */
        padding-bottom: calc(50svh - 248px);
    }

    .hub__card {
        display: flex;
        flex-direction: column;
        align-items: center;
        color: inherit;
        margin-top: 70px; /* gap between cards */
    }

    .hub__card:first-child {
        margin-top: 110px; /* + the 10px video bottom gap = 120 showreel → project */
    }

    .hub__card-title {
        text-transform: uppercase;
        font-weight: 700;
        letter-spacing: calc(-0.03em + 0.3px);
    }

    .hub__card-type {
        margin-top: 20px; /* title → type */
        color: #111;
    }

    .hub__card-poster {
        margin-top: 12px; /* type → poster */
        height: 328px;
        width: auto;
        display: block;
    }

    .hub__foot {
        position: relative;
        display: flex;
        align-items: flex-end; /* arrow sits on the bottom line */
        justify-content: center;
        min-height: 60px;
        margin-top: 24px;
        padding-bottom: 10px; /* bottom line = 10px from screen */
    }

    .hub__to-top {
        border: 0;
        background: transparent;
        cursor: pointer;
        display: grid;
        place-items: center;
        padding: 8px;
        color: #111;
        line-height: 0;
    }

    .hub__foot-year {
        position: absolute;
        right: 0;
        bottom: 10px; /* bottom-aligned with the arrow + "Обо мне" link */
        text-transform: uppercase;
        letter-spacing: calc(-0.03em + 0.3px);
    }
}

.hero__video {
    position: relative;
    grid-column: 2;
    align-self: start;
    justify-self: end;
    width: 80%;
    aspect-ratio: 16 / 9;
    margin: 0;
    overflow: hidden;
    background: #000;
    cursor: pointer;
}

.hero__video-el {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.hero__play {
    position: absolute;
    inset: 0;
    margin: auto;
    display: grid;
    place-items: center;
    width: 64px;
    height: 64px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.1);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    color: #fff;
    cursor: pointer;
    transition: background 220ms ease;
}

.hero__play svg {
    margin-left: 3px;
}

.hero__video:hover .hero__play {
    background: rgba(255, 255, 255, 0.2);
}

.hero__scroll {
    position: absolute;
    left: 50%;
    bottom: 18px;
    z-index: 1;
    transform: translateX(-50%);
    display: grid;
    place-items: center;
    transition: filter 220ms ease, transform 220ms ease;
}

.hero__scroll:hover {
    transform: translate(-50%, 4px);
}

.hero__cv {
    position: absolute;
    right: var(--gutter);
    bottom: 18px;
    z-index: 1;
    padding: 0;
    border: 0;
    background: transparent;
    color: inherit;
    cursor: pointer;
    font-size: calc(var(--font-size) - 1px);
    text-transform: uppercase;
    letter-spacing: calc(-0.03em + 0.3px);
    transition: filter 220ms ease;
}

.hero__cv:hover {
    filter: blur(5px);
}

.video-overlay {
    position: fixed;
    inset: 0;
    z-index: 200;
    display: grid;
    place-items: center;
    padding: clamp(24px, 6vh, 80px) clamp(24px, 6vw, 96px);
    /* Same backdrop as the project modal: 20% black + blur 20. */
    background: rgba(0, 0, 0, 0.2);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    opacity: 0;
    transition: opacity 280ms ease;
}

.video-overlay.is-open {
    opacity: 1;
}

.video-overlay[hidden] {
    display: none;
}

.video-overlay__frame {
    position: relative;
    width: 100%;
    max-width: calc(90vh * 16 / 9);
    max-height: 90vh;
    aspect-ratio: 16 / 9;
    overflow: hidden;
    background: #000;
}

.video-overlay__video {
    display: block;
    width: 100%;
    height: 100%;
    background: #000;
    object-fit: contain;
}

/* "Uncovering" reveal: a panel covers the frame, then slides off to expose
   the video left-to-right (and slides back to re-cover on close). */
.video-overlay__cover {
    position: absolute;
    inset: 0;
    z-index: 2;
    background: #fff;
    transform: translateY(0);
    transition: transform 410ms cubic-bezier(0.76, 0, 0.24, 1);
    pointer-events: none;
}

.video-overlay.is-open .video-overlay__cover {
    transform: translateY(-101%);
    transition-delay: 40ms;
}

.video-overlay__close {
    position: absolute;
    top: 18px;
    right: 24px;
    z-index: 3; /* above the frame, which fills the screen in zoom mode */
    display: grid;
    place-items: center;
    width: 64px;
    height: 64px;
    padding: 0;
    border: 0;
    background: transparent;
    color: #fff; /* the overlay backdrop is dark — white X stays visible */
    line-height: 0;
    cursor: pointer;
    transition: transform 260ms cubic-bezier(0.2, 0.7, 0.2, 1);
}

.video-overlay__close:hover {
    transform: rotate(90deg);
}

/* Mobile: the showreel/trailer overlay opens full-bleed — the 16:9 video fills
   the screen width edge-to-edge (no side padding) instead of a small centred
   clip. Desktop keeps the padded, centred frame. */
@media (max-width: 500px) {
    .video-overlay {
        padding: 0;
        background: #000; /* solid black behind the full-bleed video on mobile */
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
    }

    .video-overlay__frame {
        width: 100vw;
        max-width: 100vw;
        max-height: none;
    }
}

.case {
    display: grid;
    grid-template-columns: 50% 50%;
    min-height: 620px;
    padding-top: var(--header-height);
    background: transparent;
}

.case__intro {
    position: sticky;
    top: calc(var(--header-height) + 10vh);
    align-self: start;
    display: grid;
    grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.8fr);
    gap: clamp(40px, 3.4vw, 72px);
    min-height: 0;
    padding: 0 var(--gutter);
    align-items: stretch;
}

.case__copy {
    display: flex;
    flex-direction: column;
    gap: calc(clamp(14px, 1.6vh, 22px) + 5px);
    width: 100%;
}

.case__title {
    margin: 0;
    font-size: calc(var(--font-size) - 1px);
    font-weight: 700;
    text-transform: uppercase;
    line-height: 1.1;
    letter-spacing: calc(-0.03em + 0.3px);
}

.case__roles {
    margin: 0;
    color: #9a9a9a;
}

.case__desc {
    max-width: 460px;
    margin: 0;
    font-size: var(--font-size);
}

.case__year {
    margin-top: auto;
    transform: translateY(3px);
}

.case__media-col {
    display: flex;
    flex-direction: column;
    gap: calc(clamp(14px, 1.6vh, 22px) + 5px);
    align-items: flex-start;
    min-width: 0;
}

.case__type {
    margin: 0;
    line-height: 1.1;
}

.poster {
    width: 82%;
    height: auto;
    aspect-ratio: 1 / 1.414;
    overflow: hidden;
    margin: 0;
    background: rgba(17, 17, 17, 0.05);
}

.poster img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}


.case__content {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: calc(var(--gutter) / 2);
    align-content: start;
    padding-right: var(--gutter);
}

.content-block {
    position: relative;
    display: flex;
    width: 100%;
    max-height: 70vh;
    align-items: center;
    justify-content: center;
    margin: 0;
    background: #000;
}

.content-block img,
.content-block video {
    display: block;
    width: auto;
    max-width: 100%;
    height: auto;
    max-height: 70vh;
}

.content-block--trailer {
    cursor: pointer;
}

.content-block__play {
    position: absolute;
    inset: 0;
    margin: auto;
    display: grid;
    place-items: center;
    width: 64px;
    height: 64px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.1);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    color: #fff;
    cursor: pointer;
    transition: background 220ms ease;
}

.content-block__play svg {
    margin-left: 3px;
}

.content-block--trailer:hover .content-block__play {
    background: rgba(255, 255, 255, 0.2);
}

.site-footer {
    position: relative;
    min-height: 100vh;
    background: transparent;
}

.footer__top {
    position: absolute;
    left: var(--gutter);
    right: var(--gutter);
    bottom: 60%;
    display: grid;
    grid-template-columns: 50% 1fr auto;
    align-items: end;
}

.footer__contacts {
    grid-column: 2;
    display: flex;
    flex-direction: column;
    gap: 2px;
    align-self: end;
}

.footer__contacts a {
    display: grid;
    grid-template-columns: 5.5em 1fr;
    column-gap: var(--gutter);
    width: fit-content;
    color: #111;
    font-size: calc(var(--font-size) - 1px);
    line-height: 1.4;
    text-transform: uppercase;
    letter-spacing: calc(-0.03em + 0.3px);
    white-space: nowrap;
    transition: filter 220ms ease;
}

.footer__contacts a span:last-child {
    transition: filter 220ms ease;
}

/* hover anywhere on the row, but only the address blurs — the label stays sharp */
.footer__contacts a:hover span:last-child {
    filter: blur(5px);
}

.footer__photo {
    grid-column: 3;
    justify-self: end;
    width: clamp(150px, 14vw, 210px);
    aspect-ratio: 1 / 1;
    overflow: hidden;
    background: rgba(17, 17, 17, 0.05);
}

.footer__photo img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.footer__logo {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    line-height: 0;
    /* Decorative wordmark — its full-width box must not intercept clicks on the
       contact links it overlaps at some viewport sizes. */
    pointer-events: none;
}

.footer__logo img {
    display: block;
    width: 100%;
}

.case-nav {
    position: fixed;
    z-index: 110;
    left: var(--gutter);
    bottom: 18px;
    display: flex;
    flex-direction: column;
    gap: 2px;
    transition: opacity 180ms ease, filter 180ms ease;
}

.case-nav.is-hidden {
    visibility: hidden;
    opacity: 0;
    pointer-events: none;
}

.case-nav a {
    width: fit-content;
    color: #111;
    font-size: calc(var(--font-size) - 1px);
    line-height: 1.1;
    text-transform: uppercase;
    letter-spacing: calc(-0.03em + 0.3px);
    transition: opacity 220ms ease, filter 220ms ease;
}

/* While scrolling the cases — and only while the cursor isn't near the menu —
   the active project stays black and the rest fade to 30%. On the first/last
   screens (no active case) or when the cursor approaches, every item is black. */
.case-nav.in-cases:not(.is-near) a {
    opacity: 0.3;
}

.case-nav.in-cases:not(.is-near) a.is-active {
    opacity: 1;
}

.case-nav a:hover {
    filter: blur(5px);
}

@media (max-width: 780px) {
    .site-header {
        grid-template-columns: 1fr auto;
    }

    .site-header p {
        display: none;
    }

    .case {
        display: block;
        height: auto;
        min-height: 0;
    }

    .case__intro {
        position: relative;
        top: auto;
        display: grid;
        grid-template-columns: 1fr;
        grid-template-rows: auto minmax(0, 1fr);
        height: auto;
        
        padding: 18px;
    }

    .case__content {
        padding: 18px;
        overflow: visible;
    }

    .site-footer {
        flex-direction: column;
        justify-content: flex-end;
        align-items: center;
        gap: 12px;
        min-height: 50svh;
        padding: 18px;
        text-align: center;
        min-height: 20vh;
    }

    .site-footer a {
        white-space: normal;
    }

    .poster {
        width: 100%;

        
    }

}

/* ---- Project modal ---- */
html.is-locked,
html.is-locked body {
    overflow: hidden;
}

/* While any modal is open, blur the page content itself (header + hub — both
   frozen, so this rasterises once) instead of using a backdrop-filter on the
   modal. A full-screen backdrop-filter leaves stale grey compositor tiles in
   Chromium/Yandex when the layer behind it is re-tiled (e.g. hovering the
   browser chrome). The slight scale hides the blur's fade at the viewport
   edges. The modals themselves are siblings, so they stay sharp. */
.site-header,
main {
    transition: filter 360ms ease, transform 360ms ease;
}

body.modal-open .site-header,
body.modal-open main {
    filter: blur(20px);
    transform: scale(1.06);
}

.project-modal {
    position: fixed;
    inset: 0;
    z-index: 150;
    overflow: hidden; /* clip the next-card's off-screen tail */
    background: rgba(0, 0, 0, 0.2); /* 20% black scrim */
    /* The blur lives on <main> (see body.modal-open main) rather than a
       backdrop-filter here — a full-screen backdrop-filter leaves stale grey
       compositor tiles in Chromium/Yandex when the layer behind it is
       re-tiled (e.g. hovering the browser chrome). The hub is frozen while the
       modal is up, so blurring it directly is cheap and artefact-free. */
    opacity: 0;
    transition: opacity 360ms ease;
}

.project-modal.is-open {
    opacity: 1;
}

.project-modal[hidden] {
    display: none;
}

/* The whole card scrolls over the (fixed, blurred) hub — driving up from below,
   rather than scrolling inside a pinned window. */
.project-modal__scroll {
    position: absolute;
    inset: 0;
    overflow-y: auto;
    overflow-x: hidden;
    overscroll-behavior: contain;
    /* no scroll indicator */
    scrollbar-width: none;
    -ms-overflow-style: none;
}

.project-modal__scroll::-webkit-scrollbar {
    display: none;
}

/* Holds the current page + the next teaser; the first card starts 120px down,
   the teaser sits 120px below it. The track is transformed during a hand-off. */
.pm__track {
    width: min(800px, 100%);
    margin: 0 auto;
    padding-top: 120px;
    will-change: transform;
}

/* The current project: an 800px white card. min-height reserves a full screen
   so the next teaser stays below the fold while media (0-height until loaded)
   is still streaming in on first open — otherwise the teaser flashes into view
   and the open animation jerks. Real content always exceeds this, so no gap. */
.pm__current {
    min-height: calc(100vh - 120px);
    padding: var(--gutter); /* same inset as the main-screen edge gutter */
    background: #fff;
    /* Mask reveal: the card wipes in bottom-to-top, like the showreel. */
    clip-path: inset(100% 0 0 0);
    transition: clip-path 560ms cubic-bezier(0.76, 0, 0.24, 1);
}

.project-modal.is-open .pm__current {
    clip-path: inset(0 0 0 0);
}

/* Next-project hand-off: the header text wipes in under a bottom-to-top mask as
   the page rides up, instead of snapping in when the teaser is swapped out. The
   explicit inset(0) default is what the masked inset(100%) animates back to. */
.pm__current .pm__title,
.pm__current .pm__type,
.pm__current .pm__text {
    clip-path: inset(0 0 0 0);
    transition: clip-path 620ms cubic-bezier(0.76, 0, 0.24, 1);
}

.pm__current.pm__entering .pm__title,
.pm__current.pm__entering .pm__type,
.pm__current.pm__entering .pm__text {
    clip-path: inset(100% 0 0 0);
    transition: none;
}

/* Next project: a compact teaser card that scrolls up with the page (120px
   below the current card). Clicking it rides the next project up into place.
   A white box-shadow runs 100vh below the card so a fast flick / overscroll
   stays continuous as it rises — box-shadow doesn't affect the scroll height,
   and the clip-path trims the shadow's sideways spread to the card's width. */
.pm__next {
    display: block;
    width: 100%;
    margin-top: 120px; /* gap from the current card's last media */
    padding: var(--gutter); /* same insets as the current card */
    border: 0;
    background: #fff;
    color: inherit;
    text-align: left;
    cursor: pointer;
    clip-path: inset(0 0 -100vh 0);
    box-shadow: 0 50vh 0 50vh #fff;
}

.project-modal__close {
    position: fixed;
    top: 18px;
    right: 24px;
    z-index: 1;
    display: grid;
    place-items: center;
    width: 64px;
    height: 64px;
    padding: 0;
    border: 0;
    background: transparent;
    color: #111;
    line-height: 0;
    cursor: pointer;
    transition: transform 260ms cubic-bezier(0.2, 0.7, 0.2, 1);
}

.project-modal__close:hover {
    transform: rotate(90deg);
}

/* ≤820: inset the case-modal card 20px from the screen edges. (After the base
   .pm__track rule so it wins — a media query adds no specificity.) */
@media (max-width: 820px) {
    .pm__track {
        width: calc(100% - 40px);
        padding-top: 20px;
    }
}

/* Head: title + type on the top row; poster (144×200) and the text column
   (at the modal's central axis) bottom-aligned below. */
.pm__head {
    display: grid;
    grid-template-columns: 1fr 1fr;
    /* Title row, a fixed breathing gap, then the poster/text row. The gap is a
       real grid track (not slack from a min-height + 1fr) so it can NEVER
       collapse: a long description grows the head downward while the poster/text
       stay bottom-aligned with the same gap above, instead of the text creeping
       up and sticking to the title (which happened in the 1441–1679 band, where
       the old min-height left only ~19px of give). */
    grid-template-rows: auto var(--pm-head-gap) auto;
    column-gap: 0;
    --pm-head-gap: 69px; /* breathing room between the title row and poster/text */
}

.pm__title {
    grid-column: 1;
    grid-row: 1;
    margin: 0;
    font-size: calc(var(--font-size) - 1px);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: calc(-0.03em + 0.3px);
}

.pm__type {
    grid-column: 2;
    grid-row: 1;
    margin: 0;
}

/* Poster + text reveal together with the card's own mask (no separate staggered
   wipe — that double clip made the open animation look jerky). */
.pm__poster {
    grid-column: 1;
    grid-row: 3;
    align-self: end;
    width: 144px;
    height: 200px;
    margin: 0;
    overflow: hidden;
    background: rgba(17, 17, 17, 0.05);
}

.pm__poster img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.pm__text {
    grid-column: 2;
    grid-row: 3;
    align-self: end;
    margin-bottom: 14px; /* text sits 24px above the trailer (poster sits 10px) */
    padding-right: 20px; /* extra breathing room to the modal's right edge */
    display: flex;
    flex-direction: column;
    gap: 24px; /* equal spacing between text paragraphs */
}

.pm__roles {
    margin: 0;
    color: #9a9a9a;
}

.pm__desc {
    margin: 0;
}

.pm__media {
    margin-top: var(--gutter); /* same gap as between media items */
    display: grid;
    gap: var(--gutter); /* between media items */
}

.pm__media .content-block {
    max-height: none;
    /* Transparent while loading so the block blends with the white modal — no
       black flash before the media decodes (which janked the open animation).
       Letterboxed media (portrait / video) sets its own backdrop below. */
    background: transparent;
}

/* --- Skeleton shimmer while a modal media block loads --------------------
   Each block reserves its height up front; until the img/video inside is
   ready, a soft grey shimmer fills that space so the modal reads as "loading"
   instead of an empty white void. buildMedia() adds .is-loaded once the media
   decodes (or a lazy video starts buffering), fading the shimmer to reveal
   the media beneath. pointer-events:none so it never eats a play-button tap. */
.pm__media .content-block::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 3;
    pointer-events: none;
    background:
        linear-gradient(100deg,
            rgba(255, 255, 255, 0) 38%,
            rgba(255, 255, 255, 0.6) 50%,
            rgba(255, 255, 255, 0) 62%),
        #e9e9e9;
    background-size: 200% 100%, 100% 100%;
    background-repeat: no-repeat;
    animation: skeleton-shimmer 1.5s ease-in-out infinite;
    opacity: 1;
    transition: opacity 400ms ease;
}

.pm__media .content-block.is-loaded::after {
    opacity: 0;
    animation: none;
}

@keyframes skeleton-shimmer {
    from { background-position: 120% 0, 0 0; }
    to   { background-position: -20% 0, 0 0; }
}

.pm__media .content-block img,
.pm__media .content-block video {
    display: block;
    width: 100%;
    max-width: 100%;
    max-height: none;
    height: auto;
}

/* When the block has a reserved aspect-ratio, fill it exactly so the media
   doesn't nudge the layout by a pixel as it loads. */
.pm__media .content-block[style*="aspect-ratio"] img,
.pm__media .content-block[style*="aspect-ratio"] video {
    height: 100%;
    object-fit: cover;
}

/* Vertical media: a fixed 780px-tall block (reserves height, no reflow). The
   clip is height-capped and centred (object-fit: contain), so portrait footage
   can be dropped in as-is and gets manual pillarbox "ears" on the sides. */
.pm__media .content-block--portrait {
    height: 780px;
    background: #000; /* letterbox ears (overridden by inline item.bg) */
}

.pm__media .content-block--portrait img,
.pm__media .content-block--portrait video {
    width: 100%;
    height: 100%;
    object-fit: contain;
}

.pm__media .content-block--trailer {
    cursor: pointer; /* click the video (or play button) to open it with sound */
    background: #000; /* letterbox bars for trailers wider than the 16:9 block */
}

/* Trailer fills a 16:9 block; contain (not cover) so an ultra-wide clip shows
   in full, letterboxed top/bottom, matching the height of the content below.
   A 16:9 trailer fills exactly, so the bars never show for those. */
.pm__media .content-block--trailer[style*="aspect-ratio"] video {
    object-fit: contain;
}

/* Photo row: N images side by side in equal columns. The grid gap is the same
   --gutter as between stacked blocks, so the spacing between the photos matches
   the spacing above/below them. All cells share one aspect-ratio (--row-ar, the
   first photo's) so their heights line up; cover fills each cell. */
.pm__media .content-block--row {
    display: grid;
    grid-template-columns: repeat(var(--cols, 2), 1fr);
    gap: var(--gutter);
    background: transparent;
}

.content-block--row .row-cell {
    aspect-ratio: var(--row-ar, 1);
    overflow: hidden;
    background: #000;
}

.pm__media .content-block--row .row-cell img,
.pm__media .content-block--row .row-cell video {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* Swipeable gallery: a horizontal scroll-snap track contained within the card
   (the modal clips the neighbours — they don't bleed past it). The active slide
   is a bit narrower than the card so the edge of the next photo peeks in, and
   slides are separated by the standard --gutter — which makes the gallery (and
   that it swipes) read clearly. Height is reserved up front from the tallest
   slide's aspect-ratio (--slide-ar). --slide-w is the active slide width: card
   content width minus a gutter gap + a peek of the neighbour on each side. */
.pm__media .content-block--gallery {
    position: relative;
    overflow: hidden;
    display: block; /* not flex (base .content-block) — .gallery just fills it */
    /* as a grid item, min-width:auto would expand the block to its scroll
       content's width (breaking the scroll) — pin it to 0 */
    min-width: 0;
    --slide-ar: 1.5;
    --gallery-peek: clamp(20px, 2.5vw, 48px); /* visible sliver of the next photo */
    --slide-w: calc(min(800px, 100vw) - 2 * var(--gutter) - 2 * (var(--gutter) + var(--gallery-peek)));
    height: calc(var(--slide-w) / var(--slide-ar));
}

@media (min-width: 1680px) {
    .pm__media .content-block--gallery {
        --slide-w: calc(min(900px, 100vw) - 2 * var(--gutter) - 2 * (var(--gutter) + var(--gallery-peek)));
    }
}

/* Vertical gallery: capped at the portrait height like single vertical media;
   the JS sets --slide-w to the matching width. */
.pm__media .content-block--gallery.content-block--portrait {
    height: 780px;
}

/* A single-slide gallery is just a normal block — no peek, full card width. */
.pm__media .content-block--gallery.gallery--solo {
    height: auto;
    aspect-ratio: var(--slide-ar);
    --slide-w: 100%;
}

.content-block--gallery .gallery {
    position: relative;
    height: 100%;
    outline: none;
}

.content-block--gallery .gallery__track {
    display: flex;
    height: 100%;
    gap: var(--gutter);
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none; /* Firefox */
}

.content-block--gallery .gallery__track::-webkit-scrollbar {
    display: none; /* WebKit */
}

.content-block--gallery .gallery__slide {
    flex: 0 0 var(--slide-w);
    height: 100%;
    scroll-snap-align: center;
    scroll-snap-stop: always;
    display: grid;
    place-items: center;
    background: #000;
}

/* Each slide is letterboxed onto black (contain), regardless of the block's own
   aspect-ratio cover rule above. */
.pm__media .content-block--gallery .gallery__slide img,
.pm__media .content-block--gallery .gallery__slide video {
    width: 100%;
    height: 100%;
    object-fit: contain;
}

/* ===== Mobile project modal (≤500): a centred column with a floating close pill.
   Placed after the base .pm__* rules so it overrides them. ===== */
@media (max-width: 500px) {
    /* Scrim matches desktop (base rgba(0,0,0,0.2)). While open, the page colour
       under the blur is forced to white in JS (openProject); on close it returns
       to the viewed project's tint. */

    /* Card inset 10px from the screen edges (overrides the ≤820 20px rule). */
    .pm__track {
        width: calc(100% - 20px);
        padding-top: 100px;
    }

    .pm__current {
        min-height: 0;
        padding-top: 32px; /* card top → title */
        padding-bottom: 10px; /* last media → card bottom edge */
    }

    /* Close: a centred 60×40 white pill, fixed, floating above the card. */
    .project-modal__close {
        top: 50px;
        right: auto;
        left: 50%;
        transform: translateX(-50%);
        width: 60px;
        height: 40px;
        border-radius: 20px;
        background: #fff;
        color: #111;
        box-shadow: 0 4px 13px rgba(0, 0, 0, 0.15);
        transition: none; /* no hover rotate on mobile */
    }

    .project-modal__close:hover {
        transform: translateX(-50%); /* stay centred, no rotation */
    }

    /* The crosshair renders at 24px: the X path spans 36/64 of the 64px viewBox,
       so a 43px svg gives a ~24px cross. The svg is taller (43) than the pill (40),
       so grid place-items mis-centres it — pin it dead-centre with absolute
       positioning instead. */
    .project-modal__close svg {
        position: absolute;
        top: 50%;
        left: 50%;
        width: 43px;
        height: 43px;
        transform: translate(-50%, -50%);
    }

    /* Stroke ~1.38px on screen (2.05 user units × 43/64) to match the footer
       up-arrow's line weight. */
    .project-modal__close svg path {
        stroke-width: 2.05;
    }

    /* Gap from the case card down to the next-case teaser; 10px padding from the
       teaser's own edges. */
    .pm__next {
        margin-top: 100px;
        padding: 10px;
    }

    /* Head: a single centred column — title, type, poster, roles, description. */
    .pm__head {
        display: flex;
        flex-direction: column;
        align-items: center;
        text-align: center;
    }

    .pm__type {
        margin-top: 24px; /* title → type */
    }

    .pm__poster {
        align-self: center;
        margin-top: 16px; /* type → poster */
        width: auto;
        height: 328px;
    }

    .pm__poster img {
        width: auto; /* natural width at the fixed 328px height */
    }

    .pm__text {
        align-self: stretch;
        width: 100%;
        margin: 32px 0 0; /* poster → roles */
        padding-right: 0;
        gap: 32px; /* roles → description, description → description */
        text-align: center;
    }

    /* Description paragraphs sit 10px tighter on each side than the roles. */
    .pm__desc {
        padding-left: 10px;
        padding-right: 10px;
    }

    /* Media: 32px below the text, 10px between blocks, and 10px from the card's
       side edges (partly pulled out of the card's wider padding) — the centred
       text keeps its wider padding. */
    .pm__media {
        margin-top: 32px;
        gap: 10px;
        margin-left: calc(10px - var(--gutter));
        margin-right: calc(10px - var(--gutter));
    }

    /* Gallery: 5px between slides. */
    .content-block--gallery .gallery__track {
        gap: 5px;
    }

    /* Photo row: 10px between side-by-side photos. */
    .pm__media .content-block--row {
        gap: 10px;
    }

    /* Vertical media: drop the desktop 780px height cap + black letterbox — on a
       narrow phone the width already limits the height, so the clip just fills the
       width at its own ratio with no bars. */
    .pm__media .content-block--portrait {
        height: auto;
        background: transparent;
    }

    .pm__media .content-block--portrait img,
    .pm__media .content-block--portrait video {
        height: auto;
    }
}

.content-block--gallery .gallery__nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 2;
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.1);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    color: #fff;
    cursor: pointer;
    transition: background 220ms ease, opacity 220ms ease;
}

.content-block--gallery .gallery__nav:hover {
    background: rgba(255, 255, 255, 0.2);
}

.content-block--gallery .gallery__nav--prev {
    left: var(--gutter);
}

.content-block--gallery .gallery__nav--next {
    right: var(--gutter);
}

.content-block--gallery .gallery__nav:disabled {
    opacity: 0;
    pointer-events: none;
}

.content-block--gallery .gallery__nav[hidden] {
    display: none;
}

.content-block--gallery .gallery__counter {
    position: absolute;
    bottom: 14px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 2;
    padding: 4px 10px;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.4);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    color: #fff;
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: calc(-0.03em + 0.3px);
}

.content-block--gallery .gallery__counter[hidden] {
    display: none;
}

/* Next-project teaser — a compact card; poster + title/label on the left,
   roles on the right. Only its top ~120px peeks above the bottom edge. */
.pm__next-inner {
    display: grid;
    grid-template-columns: 63px 1fr;
    grid-template-rows: auto 1fr;
    column-gap: var(--gutter);
    align-items: start;
    width: 100%;
}

.pm__next-poster {
    grid-column: 1;
    grid-row: 1 / 3;
    width: 63px;
    height: 88px;
    margin: 0;
    overflow: hidden;
    background: rgba(17, 17, 17, 0.05);
}

.pm__next-poster img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.pm__next-title {
    grid-column: 2;
    grid-row: 1;
    margin: 0;
    font-size: calc(var(--font-size) - 1px);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: calc(-0.03em + 0.3px);
    transition: filter 220ms ease;
}

.pm__next-label {
    grid-column: 2;
    grid-row: 2;
    align-self: end; /* bottom-aligned to the poster */
    margin: 0;
    color: #9a9a9a;
    transition: filter 220ms ease;
}

.pm__next:hover .pm__next-title,
.pm__next:hover .pm__next-label {
    filter: blur(5px);
}

/* The fresh teaser's content wipes in under the same mask after a hand-off,
   instead of snapping in. The resting clip extends 20px past the top/bottom edges
   so the hover blur's halo isn't clipped; only the top inset animates on reveal. */
.pm__next .pm__next-inner {
    clip-path: inset(-20px 0 -20px 0);
    transition: clip-path 620ms cubic-bezier(0.76, 0, 0.24, 1);
}

.pm__next.pm__entering .pm__next-inner {
    clip-path: inset(100% 0 -20px 0);
    transition: none;
}

/* Hand-off: a ghost of the clicked teaser's content rides up with the page and
   wipes out under the same mask, simultaneously — so there's no pause between
   the click and the project moving. */
.pm__next-ghost {
    z-index: 2;
    margin: 0;
    pointer-events: none;
    will-change: transform, clip-path;
}

.content-block__expand {
    position: absolute;
    top: 12px;
    right: 12px;
    z-index: 2;
    display: grid;
    place-items: center;
    width: 40px;
    height: 40px;
    padding: 0;
    border: 0;
    background: rgba(0, 0, 0, 0.4);
    color: #fff;
    cursor: pointer;
    transition: background 220ms ease;
}

.content-block__expand:hover {
    background: rgba(0, 0, 0, 0.65);
}

/* Zoom mode (a trailer): no mask cover — the frame zooms out to fill the whole
   screen (edge-to-edge, black letterbox), not a centred modal. */
.video-overlay.is-zoom .video-overlay__cover {
    display: none;
}

.video-overlay.is-zoom {
    padding: 0;
    background: #000;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
}

.video-overlay.is-zoom .video-overlay__frame {
    width: 100vw;
    height: 100vh;
    max-width: none;
    max-height: none;
    aspect-ratio: auto;
}

.video-overlay.is-zoom .video-overlay__close {
    color: #fff; /* visible on the black fullscreen backdrop */
}

/* ---- About modal ("Обо мне + CV") ----
   A left-anchored 900px white panel over the blurred hub. Opens with a
   left-to-right mask reveal; content scrolls vertically inside the panel. */
.about-modal {
    position: fixed;
    inset: 0;
    z-index: 150;
    overflow: hidden;
    background: rgba(0, 0, 0, 0.2);
    /* Blur lives on <main> (body.modal-open main), not a backdrop-filter here —
       see the note on .project-modal. */
    opacity: 0;
    transition: opacity 360ms ease;
}

.about-modal.is-open {
    opacity: 1;
}

.about-modal[hidden] {
    display: none;
}

.about-modal__scroll {
    position: absolute;
    left: 0;
    top: 0;
    width: min(900px, 100%);
    height: 100vh; /* full height — flush to the screen top and bottom */
    overflow-y: auto;
    overflow-x: hidden;
    overscroll-behavior: none; /* no rubber-band bounce revealing the backdrop */
    background: #fff;
    /* Mask reveal: wipes in left-to-right (right edge opens rightward). */
    clip-path: inset(0 100% 0 0);
    transition: clip-path 560ms cubic-bezier(0.76, 0, 0.24, 1);
    scrollbar-width: none;
    -ms-overflow-style: none;
}

.about-modal__scroll::-webkit-scrollbar {
    display: none;
}

.about-modal.is-open .about-modal__scroll {
    clip-path: inset(0 0 0 0);
}

.about-modal__close {
    position: fixed;
    top: 18px;
    right: 24px;
    z-index: 1;
    display: grid;
    place-items: center;
    width: 64px;
    height: 64px;
    padding: 0;
    border: 0;
    background: transparent;
    color: #111;
    line-height: 0;
    cursor: pointer;
    transition: transform 260ms cubic-bezier(0.2, 0.7, 0.2, 1);
}

.about-modal__close:hover {
    transform: rotate(90deg);
}

/* ≤820: move the "About me" modal's close button to the bottom-right corner so
   it doesn't sit over the full-width content. (After the base rule so it wins —
   a media query adds no specificity.) */
@media (max-width: 820px) {
    .about-modal__close {
        top: auto;
        bottom: 20px;
        right: 20px;
    }
}

.about__page {
    --about-photo: 192px; /* grows at the same breakpoint as the text (1680) */
    position: relative;
    padding: 0;
    color: #111;
}

/* Photo: pinned to the panel's top-right corner and kept there on scroll
   (sticky). The head is pulled up by the photo's height so it shares its row. */
.about__photo {
    position: sticky;
    top: 0;
    z-index: 1;
    width: var(--about-photo);
    height: var(--about-photo);
    margin: 0 0 0 auto; /* right-align within the full-width page */
    overflow: hidden;
    background: rgba(17, 17, 17, 0.05);
}

.about__photo img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* Head: name (top-left, 16px in) + intro (under the name, its bottom on the
   photo's bottom line). */
.about__head {
    position: relative;
    /* pull up by the photo's height so the head shares the photo's row */
    margin-top: calc(-1 * var(--about-photo));
    min-height: var(--about-photo);
    padding: 16px calc(var(--about-photo) + 48px) 0 16px; /* 16 top/left; clear photo */
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    margin-bottom: 140px; /* gap down to the first dated section */
}

.about__name {
    margin: 0;
    font-size: 48px;
    letter-spacing: -2px;
    font-weight: var(--font-weight);
    line-height: 1;
}

.about__intro {
    margin: 0;
    max-width: 530px;
    font-size: var(--font-size);
}

/* Sections: dates (left, 16px in) | body starting at 240px from the left edge;
   body's right edge sits 96px in (16 + 80) while CV reaches to 16px. */
.about__section {
    position: relative;
    display: grid;
    grid-template-columns: 240px 1fr;
    margin-top: 50px;
}

.about__head + .about__section {
    margin-top: 0; /* head's margin-bottom owns that gap */
}

.about__period {
    grid-column: 1;
    padding-left: 16px;
    text-transform: uppercase;
    letter-spacing: calc(-0.03em + 0.3px);
}

.about__body {
    grid-column: 2;
    padding-right: 270px; /* room on the right of the work column (≤1440) */
}

/* Stage names share the dates' font — same size and weight (not the h3 default). */
.about__title {
    margin: 0;
    font-size: inherit;
    font-weight: var(--font-weight);
    text-transform: uppercase;
    letter-spacing: calc(-0.03em + 0.3px);
}

.about__role {
    margin: 24px 0 0;
    color: #9a9a9a;
}

.about__list {
    margin: 24px 0 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/* Software / languages: tight line-height spacing like the home skills list. */
.about__section--nodate .about__list {
    margin-top: 24px;
    gap: 0;
}

.about__list li {
    position: relative;
    padding-left: 40px;
}

.about__list li::before {
    content: "•";
    position: absolute;
    left: 0;
    top: 0;
}

.about__cv {
    position: absolute;
    right: 16px; /* 16px from the panel's right edge */
    bottom: 0;
    color: #111;
    text-decoration: none;
    text-transform: uppercase;
    letter-spacing: calc(-0.03em + 0.3px);
    transition: filter 220ms ease;
    /* TEMP: no CV file yet — dimmed + not clickable. Remove these two lines and
       restore the :hover blur below once assets/about/cv.pdf exists. */
    opacity: 0.3;
    pointer-events: none;
}

.about__cv:hover {
    /* TEMP: hover disabled while the CV is missing (restore: filter: blur(4px)) */
    filter: none;
}

.about__logo {
    margin: 120px 0 0;
}

.about__logo img {
    display: block;
    width: 100%;
    height: auto;
}

/* Photo grows to 212×212 at the same breakpoint as the base type (1680); the
   head/min-height/padding follow automatically via --about-photo. */
@media (min-width: 1680px) {
    .about__page {
        --about-photo: 212px;
    }
}

/* ===== Mobile "About" modal (≤500): same shell as the case modal — a centred
   white card inset 10px from the screen edges, with a floating close pill on top.
   The photo no longer sticks on scroll; the intro flows full-width below it. ===== */
@media (max-width: 500px) {
    /* Scroll container goes full-screen + transparent (the card is the white
       surface). No left-to-right wipe — the modal just fades in. */
    .about-modal__scroll {
        width: 100%;
        height: 100%;
        background: transparent;
        clip-path: none;
    }

    .about-modal.is-open .about-modal__scroll {
        clip-path: none;
    }

    /* Close: a centred 60×40 white pill on top (overrides the ≤820 bottom-right),
       matching the case modal — 24px cross, no hover rotate. */
    .about-modal__close {
        top: 50px;
        bottom: auto;
        right: auto;
        left: 50%;
        transform: translateX(-50%);
        width: 60px;
        height: 40px;
        border-radius: 20px;
        background: #fff;
        color: #111;
        box-shadow: 0 4px 13px rgba(0, 0, 0, 0.15);
        transition: none;
    }

    .about-modal__close:hover {
        transform: translateX(-50%);
    }

    .about-modal__close svg {
        position: absolute;
        top: 50%;
        left: 50%;
        width: 43px;
        height: 43px;
        transform: translate(-50%, -50%);
    }

    .about-modal__close svg path {
        stroke-width: 2.05;
    }

    /* The card: centred, 10px from the screen edges, starts 100px down (below the
       close pill); 10px above the name, 10px gap to the screen bottom. */
    .about__page {
        width: calc(100% - 20px);
        margin: 100px auto 10px;
        padding: 10px 16px 0 10px; /* 10px left (name + years), 16px right */
        background: #fff;
        /* Same open animation as the case modal: the card wipes in bottom-to-top
           under a mask while the modal fades in. */
        clip-path: inset(100% 0 0 0);
        transition: clip-path 560ms cubic-bezier(0.76, 0, 0.24, 1);
    }

    .about-modal.is-open .about__page {
        clip-path: inset(0 0 0 0);
    }

    /* Head: name top-left, photo 120×120 flush to the card's top-right corner
       (negative margins cancel the card padding), intro flows full-width 24px
       below the photo. */
    .about__photo {
        position: static;
        float: right;
        width: 120px;
        height: 120px;
        margin: -10px -16px 24px 12px; /* pull to the card's outer top-right corner */
    }

    .about__head {
        display: block;
        min-height: 0;
        margin: 0 0 50px; /* head → first section */
        padding: 0;
    }

    .about__name {
        font-size: 28px;
        letter-spacing: -1px;
    }

    .about__intro {
        clear: right; /* drop below the photo */
        max-width: none;
        margin: 0;
        padding-right: 24px; /* right edge 40px from the card edge (16 padding + 24) */
    }

    /* Sections: vertical year on the left, content to its right. No-date sections
       align to the same content column (the year cell just stays empty). */
    .about__section {
        grid-template-columns: 80px 1fr;
        margin-top: 50px;
    }

    .about__period {
        writing-mode: vertical-rl;
        transform: rotate(180deg);
        align-self: start;
        justify-self: start;
        padding-left: 0;
    }

    .about__body {
        padding-right: 0;
    }

    /* Spacing per spec: title → role 20, role → list 20, list items 12 apart,
       no bullets. */
    .about__role {
        margin-top: 20px;
    }

    .about__list {
        margin-top: 20px;
        gap: 12px;
    }

    /* Software / languages: tight, no gap between lines. */
    .about__section--nodate .about__list {
        margin-top: 20px;
        gap: 0;
    }

    .about__list li {
        padding-left: 0;
    }

    .about__list li::before {
        display: none;
    }

    /* CV link: in flow under the languages list (not pinned bottom-right). */
    .about__cv {
        position: static;
        grid-column: 2;
        justify-self: start;
        margin-top: 50px; /* gap from the text above */
    }

    /* Logo: 170px tall, scaled proportionally (so wider than the card — it bleeds
       past the edges and is clipped). Centred and pinned at the card bottom. The
       70px top gap makes CV-bottom → card-bottom (logo included) = 240px
       (240 − 170 logo height). */
    .about__logo {
        display: flex;
        justify-content: center;
        width: calc(100% + 26px); /* span both paddings (10 left + 16 right) */
        margin: 70px -16px 0 -10px;
        overflow: hidden; /* clip the (wider-than-card) logo to the card edges */
    }

    .about__logo img {
        flex: none;
        height: 170px;
        width: auto;
        max-width: none;
    }
}

/* From 1200px up, pin the ДЕЛАЮ column's left edge to the horizontal centre;
   the spacer (col 3) absorbs the extra width so the showreel drifts toward the
   right edge. Below 1200 the base grid lets the right cluster slide left. */
@media (min-width: 1200px) {
    .hero {
        grid-template-columns: 50% auto minmax(0, 1fr) clamp(240px, 18vw, 380px);
    }
}

/* Above 1440 the modal poster scales up 15%. The head gap is unchanged, so the
   poster/text keep the same breathing room above them as below 1440 (the old
   shorter min-height here is what left the description nowhere to grow). */
@media (min-width: 1441px) {
    .pm__poster {
        width: 166px;  /* 144 × 1.15 */
        height: 230px; /* 200 × 1.15 */
    }
}

/* From 1680px up the base type grows and the columns keep stretching. */
@media (min-width: 1680px) {
    :root {
        --font-size: 16px;
    }

    /* Bigger type → the project card widens too (800 → 900). */
    .pm__track {
        width: min(900px, 100%);
    }

    /* Bigger type → more breathing room above the poster/text so the larger
       description doesn't crowd the first media. */
    .pm__head {
        --pm-head-gap: 104px; /* base 69 +40%-ish, for the larger type */
    }
}

@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }
}

/* Preloader: S ——— D with a line that fills left-to-right as media loads,
   then the white panel wipes upward (same easing as the video reveal). */
/* Lock the page to the top while the preloader is up, so the hero always
   reveals from vertical 0 (no leftover scroll offset, no overlap with case-01). */
html.is-preloading,
html.is-preloading body {
    overflow: hidden;
}

/* A 20% black scrim sits just under the white preloader panel and fades to 0
   as the panel wipes up — it gives the all-white panel a visible edge against
   the all-white hero behind it. */
.preloader-dim {
    position: fixed;
    inset: 0;
    z-index: 299;
    background: rgba(0, 0, 0, 0.2);
    pointer-events: none;
    transition: opacity 820ms cubic-bezier(0.76, 0, 0.24, 1);
}

.preloader-dim.is-done {
    opacity: 0;
}

.preloader {
    position: fixed;
    inset: 0;
    z-index: 300;
    display: flex;
    align-items: center;
    overflow: hidden;
    background: #fff;
    transform: translateY(0);
    /* Same upward wipe as the video reveal, at half the speed. */
    transition: transform 820ms cubic-bezier(0.76, 0, 0.24, 1);
}

.preloader.is-done {
    transform: translateY(-101%);
}

/* Mask reveal: the white panel slides up while the bar (letters + line) is
   counter-translated by the same distance, so it stays pinned to the viewport
   centre and is simply wiped away by the panel's rising bottom edge. */
.preloader__bar {
    display: flex;
    width: 100%;
    align-items: center;
    gap: 12px;
    padding: 0 var(--gutter);
    transition: transform 820ms cubic-bezier(0.76, 0, 0.24, 1);
}

.preloader.is-done .preloader__bar {
    transform: translateY(101vh);
}

.preloader__letter {
    font-weight: 700;
    text-transform: uppercase;
    font-size: calc(var(--font-size) - 1px);
    letter-spacing: calc(-0.03em + 0.3px);
}

.preloader__track {
    position: relative;
    flex: 1;
    height: 1.5px;
}

.preloader__fill {
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    width: 0;
    background: #111;
}

/* iOS in-app browsers (Telegram/Safari) let descendants of a transformed
   element escape the preloader's stacking: .hero carries a transform during
   hero-hold, so its fixed children (the vertical About link, the particle
   canvas) AND its composited <video> (the showreel + its caption) can paint ON
   TOP of the preloader on phones and peek out past it. Hide them until the
   preloader is gone; they render normally the instant is-preloading is removed. */
@media (max-width: 500px) {
    html.is-preloading .hub__about,
    html.is-preloading .hero__particles,
    html.is-preloading .hub__showcase {
        visibility: hidden;
    }
}
