.application-body.application-images {
    /* Styling for this app. Compiled into the per-app block in */
    /* assets/base/applications/_registry.scss by */
    /* scripts/compile_sass.py:generate_registry_scss(). The CSS field */
    /* on the configuration App record is appended on top of this file */
    /* at compile time, so DB-stored overrides still apply. */

    & {
        --app-colour: var(--app-colour-images, #E43A83);
    }

    /* Compact / personal-cloud mode: the Map view's leaflet canvas is fixed
     * full-viewport by default (inline style) so it fills the hub chrome. In
     * Compact mode the app sits beside the section sidebar, so pin it to its
     * positioned ancestor (the document body) instead of the viewport. */
    body[data-global-nav="compact"] & .pat-leaflet {
        position: absolute !important;
    }

    /* Compact / personal-cloud photo detail (image_page / pexels_favourite_page /
     * personal_photo_page): the letterbox stage + caption pill + absolute
     * .pat-picture moved to the SHARED static/libraries/kikaron/film-hover/film-detail.css — the
     * Films app's detail page renders the identical media-detail layout, so the
     * rules live once, un-scoped from this app. Only the Live-Photo overlay
     * (photos-only) stays here; it positions against that shared stage. */
    body[data-global-nav="compact"] & .quaive-document {
        /* Live Photo detail: the paired motion clip laid over the large still,
         * faded in on hover / press-and-hold by kik-live-photo.js (which appends
         * ONE shared <video class="film-roll-motion"> into the gallery anchor).
         *
         * The insets deliberately REPEAT .pat-picture's above rather than being
         * inherited: the clip is that picture's sibling, so identical absolute
         * insets resolve against the identical containing block and the video
         * lands exactly on the photo — whichever ancestor turns out to be the
         * positioned one.
         *
         * object-fit is CONTAIN here, not the grid tiles' cover: the detail still
         * is letterboxed (see the img rule above), so a cover clip would crop
         * wider than the photo it is meant to be replacing pixel for pixel.
         *
         * Lives in the app stylesheet, not an inline <style> in the template:
         * this page arrives via pat-inject, and an injected fragment's inline
         * <style> only takes effect after a full reload. */
        .film-roll-motion {
            display: block;
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            z-index: 1;
            width: 100%;
            height: 100%;
            object-fit: contain;
            opacity: 0;
            pointer-events: none;
            transition: opacity 0.25s ease-in-out;
        }

        .film-roll-live.live-playing .film-roll-motion {
            opacity: 1;
        }

        /* A press-and-hold must not raise iOS's "Save Image…" callout. */
        .film-roll-live {
            -webkit-touch-callout: none;
        }
    }

    @media (prefers-reduced-motion: reduce) {
        body[data-global-nav="compact"] & .quaive-document .film-roll-motion {
            transition: none;
        }
    }

    /* Live Photo detail, everywhere BUT compact: the geometry that makes the clip
     * sit exactly on the still is tied to the compact layout's absolutely-
     * positioned .pat-picture (above). Without that layout there is nothing to
     * align to, so keep the injected <video> completely inert rather than let it
     * stretch across the page the moment a hover adds .live-playing. The compact
     * rule re-enables it with display: block. */
    & .quaive-document .film-roll-motion {
        display: none;
    }

    /* Compact / personal-cloud Collections landing ("Verzamelingen"): the framed
     * collection tiles (.item.collection) inset their figure a FIXED 29px on every
     * side — geometry tuned for the hub's wide ~224px cells (leaving a ~166px
     * square). In the narrower personal-cloud column the base .col-6 grid gives
     * ~106px cells, so that same 29px inset leaves only a ~48px square — the tiles
     * read tiny. Lay the grid out at a comfortable minimum cell size instead (an
     * auto-fill track floor, independent of the responsive column count), so the
     * cells are hub-sized again and the inset geometry lands where it was designed
     * to. ~166px cells → ~108px squares, 4 across in the personal cloud. */
    body[data-global-nav="compact"] & #directory-images-collections {
        grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    }

    /* Year separation headers span the full grid row — see the inline <style> in
     * images/collections.html for the live rule + rationale (shipped inline
     * because the assets bundle isn't recompiled on a main-repo push). Keep this
     * as the canonical home for when the bundle build is healthy again. */
    & #directory-images-collections .canvas-separation-header {
        grid-column: 1 / -1;
    }

    /* Gecko/Firefox (incl. Zen) grid auto-placement quirk: the tile right after a
     * full-width header (both nested in the display:contents .years-batch wrapper)
     * is flung to the end of the row instead of starting a new one — pin it to
     * column 1 so each year's first collection leads its row on Firefox. Harmless
     * in Blink. */
    & #directory-images-collections .canvas-separation-header + * {
        grid-column-start: 1;
    }

    /* No-hover (touch) devices — e.g. iPhone: the collection tiles carry hover
     * affordances (the framed figure lightens; the preview skimmer swaps to the
     * image under the pointer). With no real hover a tap leaves those states
     * "stuck" until the user taps elsewhere, so freeze the tiles at their resting
     * look on hover:none. */
    @media (hover: none) {
        & #directory-images-collections {
            .item.collection:hover figure {
                background-color: rgba(0, 0, 0, 0.1);
            }

            .photo-skimmer .image:hover img {
                opacity: 0;
            }

            .photo-skimmer .image:first-child:hover img {
                opacity: 1;
            }
        }
    }

    /* Collection tiles (.item.collection): make the framed figure a bit larger by
     * shrinking the fixed inset (29px in the assets submodule's _image-selector.scss
     * base rule), and slightly increase the gap between the figure and its
     * figcaption. Overridden here — not at the canonical submodule home — because
     * the assets submodule CSS isn't shipped by a main-repo push. */
    & .image-selector .item.collection figure {
        top:    22px;
        right:  22px;
        bottom: 22px;
        left:   22px;
    }

    & .image-selector .item.collection figure figcaption {
        bottom: -38px;
    }

    /* Personal-photo / video detail page (personal_photo_page.html): the
     * prev/next-arrow-disabled and <video> player rules moved to the SHARED
     * static/libraries/kikaron/film-hover/film-detail.css — the Films app renders the identical
     * detail (partials/quaive_film_figure.html), so the look lives once. */

    /* The collection-video player rules moved to the SHARED
     * static/libraries/kikaron/video-tile/video-tile.css: the image-detail modal
     * lives in #pat-modal-panel-space, a SIBLING of #app-space, so nothing in
     * this file — which the registry wraps in .application-body.application-images
     * — can ever reach it. That is why the player was rendering at its intrinsic
     * size and a big portrait clip pushed the panel off screen. */

    /* Per-tile actions in a collection grid — the ⋯ menu (Delete / Move /
     * Download) revealed over the top-left of a thumbnail on hover. Markup in
     * images/partials/image_tile.html, behind its ``tile_actions`` opt-in.
     *
     * App-scoped here (unlike the video player, which had to move to a global
     * stylesheet) because the grid really does live inside
     * .application-body.application-images. The popover the pattern builds mounts
     * at that same app body, so it is styled by the app's own menu rules. */
    & .image-selector .item.image {
        position: relative;
    }

    & .image-tile-actions {
        position: absolute;
        /* Clear of the corner, the way the app tile's kebab sits in off its
         * glyph plate rather than straddling the rounded edge. It has to beat the
         * tile's own corner radius or it reads as stuck to the crop. */
        top: 10px;
        left: 10px;
        z-index: 2;
        /* THE GRID TURNS POINTER EVENTS OFF. ``.image-selector.mode-follow label``
         * sets pointer-events: none and gives them back to ``.follow`` alone — so
         * the whole tile is inert except the link that opens the photo. Inherited,
         * that made this menu unclickable: visible, hover-revealed, and swallowing
         * nothing. Take them back for the actions, exactly as .follow does. */
        pointer-events: auto;
        opacity: 0;
        transition: opacity 0.12s ease-in-out;
    }

    /* The trigger is the SAME control the app tiles use — apps-grid.css
     * ``.kik-apps-grid .tile > a.more``: a 26px frosted plate with three CSS dots.
     * Deliberately mimicked rather than invented, so the kebab over a photo and
     * the kebab over an app glyph are one affordance.
     *
     * Copied rather than shared because the two differ in exactly one respect —
     * placement. The app tile pins itself off the glyph box with
     * --kik-app-icon-box / --kik-apps-more-inset, neither of which exists here;
     * a photo tile has no glyph, just a thumbnail, so the wrapper above does the
     * placement and this only draws the control. */
    & .image-tile-actions > .pat-context-menu {
        /* The dots are an absolutely-positioned ::before; the app tile's own
         * trigger is absolute so they resolve against it, this one is not — so
         * say relative rather than let them escape to the tile. */
        position: relative;
        display: flex;
        align-items: center;
        justify-content: center;
        box-sizing: border-box;
        /* 28px, not the app tile's 26: the closest analogue is the link-list card
         * kebab (components/link-list.css), which is 28 precisely because it
         * floats over a preview picture rather than a flat plate. A photo tile is
         * also much larger than a 90px app cell, so 26 read undersized on it. */
        width: 28px;
        height: 28px;
        min-width: 0;
        margin: 0;
        padding: 0;
        border: 0;
        border-radius: var(--border-radii);
        background-color: var(--pat-toolbar-icon-frost-background);
        -webkit-backdrop-filter: var(--pat-toolbar-icon-backdrop-filter);
        backdrop-filter: var(--pat-toolbar-icon-backdrop-filter);
        /* A hairline ring plus a soft drop shadow so the plate reads as a control
         * against the photo behind it. Spread-only, so it can't change the 26px
         * box. */
        box-shadow:
            0 0 0 1px rgba(0, 0, 0, 0.06),
            0 1px 2px rgba(0, 0, 0, 0.12);
        /* Hides the "More…" label — the dots are drawn on ::before. Without this
         * the word renders in the plate, which is what it was doing. */
        font-size: 0;
        line-height: 1;
        text-decoration: none;
        /* An action trigger, not a navigation link. */
        cursor: default;
    }

    /* Three dots in CSS rather than the fontello ellipsis glyph — the glyph's
     * position in its em-box drifts across font bumps, while a flex-centred
     * centre dot plus two box-shadow siblings is pixel-centred by construction. */
    & .image-tile-actions > .pat-context-menu:before {
        content: "";
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        width: 3px;
        height: 3px;
        margin: 0;
        padding: 0;
        border: 0;
        border-radius: 50%;
        aspect-ratio: auto;
        background: currentColor;
        box-shadow: -5px 0 currentColor, 5px 0 currentColor;
        color: var(--body-font-colour);
    }

    @media (hover: hover) {
        & .image-tile-actions > .pat-context-menu:hover {
            background-color: var(--pat-toolbar-icon-background-colour-hover);
            text-decoration: none;
        }
    }

    /* Menu OPEN: solid accent, so it reads as the active control rather than a
     * hovered one. All three dots are currentColor, so recolouring the ::before
     * recolours the lot. */
    & .image-tile-actions > .pat-context-menu.tooltip-active-click,
    & .image-tile-actions > .pat-context-menu[aria-expanded="true"] {
        background-color: var(--colour-accent);
        text-decoration: none;
    }

    & .image-tile-actions > .pat-context-menu.tooltip-active-click:before,
    & .image-tile-actions > .pat-context-menu[aria-expanded="true"]:before {
        color: white;
    }

    /* Reveal only where there is a real pointer: on touch a "hover" is the tap
     * that already opens the photo, so the control would flicker in and out
     * under the finger. Touch gets it permanently instead (below). */
    @media (hover: hover) {
        & .image-selector .item.image:hover .image-tile-actions {
            opacity: 1;
        }
    }

    @media (hover: none) {
        & .image-tile-actions {
            opacity: 1;
        }
    }

    /* Keyboard reach, and stay visible while the menu is actually open —
     * otherwise moving the cursor off the tile to the popover hides the trigger
     * out from under it. */
    & .image-tile-actions:focus-within,
    & .image-tile-actions:has(.pat-context-menu[aria-expanded="true"]) {
        opacity: 1;
    }

    /* The photo detail toolbar's Slideshow button hosts the page's pat-gallery
     * (the button is the gallery's first item; the photos after it ride along as
     * hidden anchors). The host has to be a wrapper, because pat_toolbar owns the
     * .toolbar-section.quick-functions the partial renders inside — so the wrapper
     * is display: contents and the button lays out as a direct child of the
     * section, exactly as it does on the Film roll, where the section itself is
     * the gallery.
     *
     * The prefetched neighbours are no longer hidden here: they are the swipe
     * strip's parked frames (libraries/kikaron/photo-carousel). */
    & .toolbar-section .toolbar-slideshow {
        display: contents;
    }

    /* The photo grids fill the width they are given, rather than the width the
     * SCREEN has (Daniel, 2026-09-22: "automatically fit more photos on the grid
     * when there is space, for instance when the sidebar closes").
     *
     * .image-selector picks its column count from screen-size style queries
     * (libraries/kikaron/image-selector), so closing the sidebar — or any other
     * change that widens the pane without widening the screen — left the same few
     * big tiles with more air around them. An auto-fill track reads the GRID's own
     * width instead: the cells keep a comfortable floor and the count follows the
     * space, which is what a photo grid should do.
     *
     * The floor is a custom property so a page can tune it; 150px keeps a phone at
     * the two-across it has today (a 380-420px pane divides into two), and gives a
     * wide desk eight or more instead of six.
     *
     * Photo grids only — the image SELECTOR panels (picking a picture for a
     * document, the Pexels/attachment tabs) keep their fixed counts: those sit in
     * modals whose width is already chosen for them. */
    & .film-roll-grid,
    & #image-collection-images,
    & .quaive-document .image-selector:not(.mode-select) {
        grid-template-columns: repeat(auto-fill, minmax(var(--kik-photo-cell-min, 150px), 1fr));
    }

    /* Phones and tablets: tiles of about one size, whatever the device
     * (Daniel, 2026-09-22 — "four across on an iPhone mini, five on a Fold, and
     * more again on a tablet so they stay about the same size").
     *
     * The floor and the gap are what decide the count against the pane's width
     * (columns = floor((W + gap) / (min + gap))), so one pair of numbers covers
     * every size. 70px with a 6px gap lands:
     *
     *   iPhone mini (~345px pane)    4 across, ~82px tiles
     *   Fold cover  (~382px)         5 across, ~72px
     *   iPhone 16   (~363px)         4 across, ~86px
     *   Fold inner  (~810px)        10 across, ~76px
     *   tablet      (~994px)        13 across, ~71px
     *
     * The tighter gap is part of it: at 15px the gaps alone would eat a quarter
     * of a phone's width. Above 1024px the desk keeps the roomier 150px floor —
     * a photo on a big screen is something you look at, not something you scan. */
    @media (width <= 1024px) {
        & .film-roll-grid,
        & #image-collection-images,
        & .quaive-document .image-selector:not(.mode-select) {
            --kik-photo-cell-min: 70px;
            --image-selector-gap: 6px;
        }

        /* …and the grid runs from edge to edge inside the phone's own margin
           (Daniel, 2026-09-22). The document pane pads 6% a side, which is a
           thumb's width of nothing on a tablet: at these sizes the pictures are
           the page, so they get the same 15px margin everything else on a small
           screen gets, and the grid fills the rest. */
        & .document-content,
        & #document-content-images {
            /* The pane's side room is a SHORTHAND on the app body's content
               element — ``padding: 60px var(--quaive-document-padding-left)`` —
               so the token to move is that one; the --document-content-padding-*
               pair below covers the panes that use the long form. The top stays
               as it is: the toolbar sits in it. */
            --quaive-document-padding-left: var(--small-screen-padding);
            --document-content-padding-left: var(--small-screen-padding);
            --document-content-padding-right: var(--small-screen-padding);
            /* The same margin above the first row. The shorthand's own top is
               60px — twice the gap the sides have, which read as a band of
               nothing under the toolbar (Daniel, 2026-09-22). The toolbar's own
               seat is not this: .document-body reserves that separately. */
            padding-top: var(--small-screen-padding);
            --document-content-padding-top: var(--small-screen-padding);
        }

        /* Fetch the next batch well before the user arrives at it. The band that
           triggers the load was 80px — about a third of a row here — so the grid
           ran out of pictures and then waited on the server (a batch is ~1.6s).
           A screen and a half of lead turns that wait into something that has
           already happened. */
        & .film-roll-grid .pat-lazy-load.infinite-scrolling,
        & #image-collection-images .pat-lazy-load.infinite-scrolling {
            /* A lead ALONE is dead space: the band is a real box below the grid,
               and the throbber sits at its far end — a screen and a half under
               the last row (Daniel, 2026-09-22). The pattern's own idiom is a
               lead with a PULL that lifts the band back over the rows above, so
               the trigger arrives early while nothing moves on screen.

               The three tokens say where the throbber sits: it lands
               (lead - pull) under the last row and keeps `reserve` under itself.
               40vh each — a little under a screen height together, the same gap
               above and below (Daniel, 2026-09-22) — and the pull still starts
               the fetch a screen before the reader arrives. */
            --pat-lazy-lead: 140vh;
            --pat-lazy-pull: 100vh;
            --pat-lazy-reserve: 40vh;
        }
    }

    /* NO RIM AROUND THE PHOTOS on a tablet or in a small container (Daniel,
     * 2026-09-26), in every Photos grid — the roll, a collection, an album. The
     * stock tile is a card: the image-selector's .item padding, the
     * base-background fill and the default shadow's hairline ring, with the photo
     * inside it. At these sizes the tiles are small enough that the card read as
     * a white rim around every picture, so it goes and the photo IS the tile; the
     * gutter still separates them. The phone already had this for the roll
     * (libraries/kikaron/film-roll). Selection is the checkbox glyph over the
     * picture, not the card, so nothing is lost with it. */
    @media (width <= 1024px) {
        & .film-roll-grid .item.image,
        & #image-collection-images .item.image,
        & .quaive-document .image-selector:not(.mode-select) .item.image {
            padding: 0;
            background: none;
            box-shadow: none;
        }
    }

    @container main (width <= 768px) {
        & .film-roll-grid .item.image,
        & #image-collection-images .item.image,
        & .quaive-document .image-selector:not(.mode-select) .item.image {
            padding: 0;
            background: none;
            box-shadow: none;
        }
    }

    /* AN EMPTY ROLL OR COLLECTION HAS NO CAMERA OR UPLOAD ICON IN ITS TOOLBAR:
     * there both are real labelled buttons under the empty message, so a first
     * photo does not depend on spotting an icon. The icons are in the markup all
     * the same — the first photo re-injects the grid, not the toolbar — and this
     * is what holds them back while the labelled pair is what is on screen: the
     * pair is rendered (only an empty grid renders it) and no tile has arrived. */
    & #document-content-images:has(#directory-images > .kik-photo-capture-form.pat-form):not(:has(#image-collection-images > .item)) #images-collection-toolbar :is(.icon-camera, .icon.upload) {
        display: none;
    }
}
