/* ==========================================================================
   scroll.css — continuous-scroll behaviour for the games index.
   Loaded LAST so it can layer over style_desktop / _mobile / _tablet.
   Everything here is additive: delete this file and the site falls back to
   a plain stacked page that still works.
   ========================================================================== */

:root {
    /* #header is position:fixed at 70px tall + 10px padding = 80px, dropping
       to 70px and 60px at the two breakpoints below. These leave a little air
       above the section a click lands on, and keep the screenshot overlay
       clear of the header. */
    --headerOffset: 92px;

    /* How faint a section goes once you scroll away from it. 0 is fully
       invisible; raise it if you'd rather see a ghost of the next game. */
    --dimOpacity: 0;

    /* How long a fade takes. Fixed, so it's the same whether you nudge the
       wheel or fling the page. */
    --fadeDuration: 500ms;

    /* Where the fades trigger, as a percentage inset from the top and bottom
       of the screen.

       A section LIGHTS UP once it reaches the middle 40% of the screen
       (100 - 2 x 30) — about a third of it is showing by then. It DIMS once
       it leaves the middle 60% (100 - 2 x 20), with roughly a fifth of it
       still on screen, so you see the fade rather than losing it off the edge.

       The two numbers differ on purpose. Matching them would make a section
       sitting exactly on the boundary flicker as you inch past it.

       Raise --fadeInActivate to make games light up later, nearer the centre.
       Lower --fadeOutDeactivate to keep them lit further toward the edge. */
    --fadeInActivate: 30;
    --fadeOutDeactivate: 20;

    /* Extra gap between games, on top of the 100px margin .content already
       carries. */
    --sectionGap: 2.5rem;

    /* Screenshot overlay chrome, pulled from the site's own palette rather
       than invented. */
    --overlayAccent: #22d5dc;
    --overlayMuted: rgba(255, 255, 255, 0.5);
    --overlayLine: rgba(255, 255, 255, 0.18);
}

@media only screen and (max-width: 900px) {
    :root { --headerOffset: 82px; }
}

@media only screen and (max-width: 600px) {
    :root { --headerOffset: 72px; }
}

/* style_desktop.css sets scroll-behavior: smooth globally. On a page this
   long that turns a click on the last game into ~14,000px of animated travel
   past 17 other games. Jumps are instant now; scroll.js asks for smooth
   explicitly on the nav's prev/next, where the distance is one section and
   the animation reads as continuity rather than a journey. */

html {
    scroll-behavior: auto;
}

/* --------------------------------------------------------------------------
   Sections
   -------------------------------------------------------------------------- */

.gameSection {
    scroll-margin-top: var(--headerOffset);
    padding-bottom: var(--sectionGap);

    /* .content was locked to height: 700px because only one section was ever
       visible, so overflow didn't matter. Stacked, a long write-up spills into
       the next game. Let the box grow instead. */
    height: auto;

    /* Positioning context for the screenshot overlay below. */
    position: relative;
}

/* Keep the even rhythm the fixed height was giving on desktop, without
   capping it. */
@media only screen and (min-width: 901px) {
    .gameSection {
        min-height: 700px;
    }
}

/* style_desktop.css caps .rightMenu at a flat max-height: 660px with
   overflow-y: scroll — a leftover from when .content was locked to
   height: 700px (only one section ever visible, so the rail scrolled
   internally rather than growing past the fold).

   Dropping that cap outright (an earlier version of this rule did)
   traded one problem for another: a screenshot-heavy rail would then
   stretch the whole section — and everyone's scroll distance — past
   whatever the write-up actually needed. The rail shouldn't be what
   decides how tall a section is.

   So the cap stays as the no-JS fallback, and scroll.js (see "Right
   rail height") replaces it per-section with an inline max-height sized
   to that section's own left-column content — taller than 660px for a
   long write-up, no shorter — falling back to this flat 660px/scroll
   if JS doesn't run. */

/* The old layout floated .rightMenu. Now that sections are stacked they have
   to contain their own floats or they'll overlap each other. */
.gameSection::after {
    content: "";
    display: block;
    clear: both;
}

/* The fade only switches on once scroll.js has run. If JS is off or errors,
   every section stays fully readable. */
.scrollFade .gameSection {
    opacity: var(--dimOpacity);
    transition: opacity var(--fadeDuration) cubic-bezier(0.4, 0, 0.2, 1);

    /* At --dimOpacity: 0 a section is invisible but still occupies its space,
       so without this you could click a screenshot or a link in the strip of
       blank page above and below the game you're reading. */
    pointer-events: none;
}

/* scroll.js adds and removes this as sections cross the thresholds above.
   The transition, not the scroll position, controls the speed. */
.scrollFade .gameSection.isActive {
    opacity: 1;
    pointer-events: auto;
}

/* Jumping to a game should just put it there. scroll.js adds this for one
   frame alongside .isActive so the destination arrives lit rather than
   fading up over half a second after you land. */
.scrollFade .gameSection.isInstant {
    transition: none;
}

/* A section with an open screenshot gallery is pinned lit, with no transition.
   Any opacity below 1 makes the section a stacking context, which would trap
   the fullscreen overlay behind the fixed header on small screens. Snapping
   straight to 1 avoids a 500ms window where that could happen. */
.scrollFade .gameSection.isZoomed {
    opacity: 1 !important;
    transition: none;
    pointer-events: auto;
}

/* --------------------------------------------------------------------------
   Divider between shipped work and jam / older work
   -------------------------------------------------------------------------- */

.scrollDivider {
    text-align: center;
    padding: 2rem 0 calc(var(--sectionGap) / 2);
    opacity: 0.5;
}

.scrollDivider img {
    width: 40px;
    height: auto;
}

/* --------------------------------------------------------------------------
   Inline link rows

   A row of linked thumbnails dropped into the write-up copy (e.g. Vibes'
   list of public games), rather than the usual .rightMenu / .slidesContainer
   rail. Nothing else on the site puts images inside .content's flow, so this
   is new rather than reused from elsewhere.

   style_desktop.css floats THREE things inside .content at once:
   .headerDesktop (left, 60%), .rightMenu (right, 40%), and — easy to miss,
   since it's not scoped past .content — every <p>, via ".content p { float:
   left; width: 57.5%; margin-left: 20px }". Whatever width happens to be
   left over between them at a given scroll position is what a non-floated
   box like .linkRow would otherwise have to negotiate for, which is exactly
   why this looked fine in isolation and broke once .rightMenu had real
   height: there wasn't reliably enough leftover width beside all three
   floats for the row to land in, so it got squeezed into whatever sliver
   was open.

   The fix doesn't try to win that negotiation. clear: left drops the row
   below the two LEFT floats (.headerDesktop and the intro <p>) unconditionally,
   and width/margin-left below copy .content p's own box model exactly
   (57.5% / 20px above 900px, 91% below it, the same breakpoint
   style_tablet.css uses to override it) so the row is exactly as wide as
   that paragraph was. Given that, it already clears .rightMenu on the right
   without being told to: same width as the text beside it, so it just never
   reaches that column. clear: both was tried first and rejected too — it
   also clears .rightMenu itself, so the row waits for that much taller float
   to finish before it can appear at all, leaving a gap under the paragraph
   the whole time .rightMenu still has height left to clear. */

.linkRow {
    clear: left;
    float: none;
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-start;
    align-items: flex-start;
    gap: 1rem;
    margin: 1rem 0 0 20px;
    width: 57.5%;
}

@media only screen and (max-width: 900px) {
    .linkRow {
        width: 91%;
    }
}

/* Every other section keeps its whole write-up — intro copy AND the "Work
   Highlights:" label — in one <p>, which floats as a single box. A <p> can't
   contain a block-level .linkRow, so here that paragraph has to close before
   .linkRow and a second one reopens after it for the "Work Highlights:"
   label. .content p floats every <p> though, so that second, much shorter
   paragraph becomes its OWN float, and the list right after it wraps its
   first item beside that float instead of below it. Cancel the float on
   just this one, rather than on every <p> a game write-up might use.

   clear: left, not both, for the same reason as .linkRow above — clearing
   .rightMenu too just reintroduces the gap under the image row, one
   paragraph later. This label is short and unfloated, so it doesn't need to
   avoid .rightMenu the way a wide box would: it just wraps to the true left
   edge on its own. */
.linkRow + p {
    float: none;
    width: auto;
    clear: left;
}

.linkRow a {
    display: block;
    float: none;
    flex: 0 1 110px;
}

.linkRow img {
    display: block;
    float: none;
    width: 100%;
    max-width: 100%;
    height: auto;
    border: 1px solid #535353;
    border-radius: 7px;
    transition: transform 140ms ease, opacity 140ms ease;
}

.linkRow a:hover img,
.linkRow a:focus-visible img {
    transform: translateY(-3px);
    opacity: 0.9;
}

.linkRow a:focus-visible {
    outline: 2px solid var(--overlayAccent);
    outline-offset: 3px;
    border-radius: 7px;
}

@media only screen and (max-width: 600px) {
    .linkRow a {
        flex-basis: 84px;
    }
}

@media (prefers-reduced-motion: reduce) {
    .linkRow img {
        transition: none;
    }
}

/* --------------------------------------------------------------------------
   Deferred video embeds
   -------------------------------------------------------------------------- */

/* Holds the slot open so nothing jumps when the embed loads in. */
.deferredEmbed:not([src]) {
    background: rgba(0, 0, 0, 0.25);
    border: 0;
}

/* --------------------------------------------------------------------------
   Accessibility
   -------------------------------------------------------------------------- */

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

    .scrollFade .gameSection,
    .scrollFade .gameSection.isActive {
        opacity: 1 !important;
        transition: none;
    }

}

/* body uses background-attachment: fixed, which forces a repaint of the tiled
   background on every scroll frame. That was cheap on a short page and is not
   on a long one — touch devices take the worst of it. */
@media (hover: none) and (pointer: coarse) {
    body {
        background-attachment: scroll;
    }
}

@media print {
    .scrollFade .gameSection,
    .scrollFade .gameSection.isActive {
        opacity: 1 !important;
    }
}

/* ==========================================================================
   Screenshot zoom

   Click a screenshot and it fills the game's content area. Scoped to the
   section on desktop, where .content is a comfortable 1100px box; fullscreen
   below 900px, where sections run taller than the screen and a scoped panel
   would centre the image somewhere off-view.
   ========================================================================== */

.gameSection img.isZoomable {
    cursor: zoom-in;
}

.gameSection img.isZoomable:focus-visible {
    outline: 2px solid var(--overlayAccent);
    outline-offset: 3px;
}

.shotZoom {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: var(--sectionGap);

    /* Deliberately low. The section is unpositioned, so 1 is enough to sit
       above the rail and the copy, while staying under #header at z-index 2
       as the page scrolls past. */
    z-index: 1;

    display: block;
    background: rgba(0, 0, 0, 0.94);
    border-radius: 12px;
    cursor: zoom-out;

    opacity: 0;
    visibility: hidden;
    transition: opacity 240ms ease, visibility 240ms;
}

.shotZoom.isOpen {
    opacity: 1;
    visibility: visible;
}

/* Sections grow to fit their copy, so some run taller than the window. A
   backdrop pinned to the section would then centre the image somewhere you
   can't see. The stage sticks to the top of the viewport instead, carrying
   the controls with it, so the image stays put however tall the game is. */
.shotZoom__stage {
    position: sticky;
    top: var(--headerOffset);
    height: min(100%, calc(100vh - var(--headerOffset) - 1.5rem));
    display: flex;
    align-items: center;
    justify-content: center;
}

.shotZoom__image {
    display: block;
    width: auto;
    height: auto;
    max-width: calc(100% - 7rem);
    max-height: calc(100% - 5rem);
    object-fit: contain;
    border: 1px solid #535353;
    border-radius: 7px;
    cursor: default;
}

.shotZoom button {
    position: absolute;
    appearance: none;
    -webkit-appearance: none;
    background: rgba(0, 0, 0, 0.55);
    border: 1px solid var(--overlayLine);
    border-radius: 999px;
    color: #fff;
    font-family: Nunito, system-ui, sans-serif;
    line-height: 1;
    cursor: pointer;
    transition: background-color 140ms ease;
}

.shotZoom button:hover {
    background: rgba(255, 255, 255, 0.16);
}

.shotZoom button:focus-visible {
    outline: 2px solid var(--overlayAccent);
    outline-offset: 2px;
}

.shotZoom__close {
    top: 0.9rem;
    right: 0.9rem;
    width: 2.4rem;
    height: 2.4rem;
    font-size: 1.3rem;
}

.shotZoom__step {
    top: 50%;
    transform: translateY(-50%);
    width: 2.8rem;
    height: 2.8rem;
    font-size: 1.5rem;
}

.shotZoom__step[data-step="-1"] { left: 0.9rem; }
.shotZoom__step[data-step="1"]  { right: 0.9rem; }

.shotZoom__count {
    position: absolute;
    bottom: 1rem;
    left: 50%;
    transform: translateX(-50%);
    color: var(--overlayMuted);
    font-family: Nunito, system-ui, sans-serif;
    font-size: 0.8rem;
    font-variant-numeric: tabular-nums;
}

/* Only one screenshot in a section: nothing to step through. */
.shotZoom.isSingle .shotZoom__step,
.shotZoom.isSingle .shotZoom__count {
    display: none;
}

/* Below 900px the whole overlay is fixed, so the stage has nothing to stick
   to and simply fills it. */
@media only screen and (max-width: 900px) {
    .shotZoom__stage {
        position: static;
        top: auto;
        height: 100%;
    }

    .shotZoom {
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        z-index: 1000;
        border-radius: 0;
        background: rgba(0, 0, 0, 0.97);
    }

    .shotZoom__image {
        max-width: calc(100% - 2rem);
        max-height: calc(100% - 7rem);
    }

    .shotZoom__step {
        top: auto;
        bottom: 1rem;
        transform: none;
    }

    .shotZoom__step[data-step="-1"] { left: 1.5rem; }
    .shotZoom__step[data-step="1"]  { right: 1.5rem; }

    body.isZooming {
        overflow: hidden;
    }
}

@media (prefers-reduced-motion: reduce) {
    .shotZoom {
        transition: none;
    }
}

@media print {
    .shotZoom {
        display: none;
    }
}
