.gallery-justified {
    position: relative;
    width: 100%;
}

.gallery-justified-tile {
    position: absolute;
    display: block;
    overflow: hidden;
    border-radius: 4px;
    background: #eee;
}

.gallery-justified-tile img {
    width: 100%;
    height: 100%;
    display: block;
    /*
     * `contain`, not `cover`. The box usually does match the photo's ratio, and then
     * the two behave identically. The exception is justified-layout's row-height
     * clamp: completeLayout() forces a row's natural height into
     * [0.5 x targetRowHeight, 2 x targetRowHeight] — hardcoded at construction, not
     * exposed as an option — and when the clamp bites it rescales the row, so every
     * box in it ends up at the photo's ratio multiplied by the clamp factor. A 3:1
     * panorama on a 360px viewport at target 260 lands in a box of ratio 2.77, which
     * `cover` would silently crop by ~8%; a row crowded enough to hit the floor crops
     * far more. `contain` letterboxes against the #eee above instead, which is the
     * trade the design already made when it rejected the CSS flex-grow layout
     * precisely because that one aligns rows by cropping.
     */
    object-fit: contain;
}

/*
 * The no-library fallback, switched on by the controller when justified-layout itself
 * fails to load. The tiles keep their own class and their inline width/height, so only
 * the positioning changes: out of the absolute coordinate space the library would have
 * supplied, into a plain wrapping row. `display`/`flex-wrap` collide with nothing on
 * `.gallery-justified`; the `position` override carries one class more than
 * `.gallery-justified-tile` (0,2,0 against 0,1,0) and so wins without !important.
 */
.gallery-justified-fallback {
    display: flex;
    flex-wrap: wrap;
}

.gallery-justified-fallback .gallery-justified-tile {
    position: static;
}

.gallery-justified-pagination {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 1rem;
    margin-top: 1.5rem;
}

.gallery-justified-page-status {
    font-variant-numeric: tabular-nums;
}

.gallery-justified-pagination button {
    min-width: 36px;
    padding: 6px 10px;
    border: 1px solid #ccc;
    background: #fff;
    cursor: pointer;
    font: inherit;
}

.gallery-justified-pagination button:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

/*
 * The lightbox's "original download" button: a 42x42 bordered square around a
 * three-part download arrow (see gallery_justified_controller.js for the glyph's
 * geometry), where stock PhotoSwipe draws a borderless 50x60 button around a
 * 32px icon. This file is linked before the vendored photoswipe.css, so equal
 * specificity loses the cascade — every rule here outranks the stock rule it
 * overrides by one selector part.
 *
 * .pswp__button (0,1,0) sets width/height/padding/border/overflow; beaten by
 * .pswp__button.pswp__button--download (0,2,0).
 */
.pswp__button.pswp__button--download {
    box-sizing: border-box;
    width: 42px;
    height: 42px;
    border: 2px solid var(--pswp-icon-color);
    border-radius: 4px;
    /*
     * .pswp__top-bar is a flex row with no align-items, so its default is
     * stretch. Stock buttons are 60px tall and fill that stretch; this one
     * declares a definite 42px height, which stretch does not touch, so
     * without this it would sit flush against the bar's top edge instead of
     * centred on it. Centring here lands this button's middle at 30px,
     * matching where the close button's 32px icon is centred inside its own
     * 60px button (top: 14px, spanning 14-46).
     */
    align-self: center;
}

/*
 * .pswp__button:hover, :active, :focus (0,2,0) re-zeroes border; beaten by
 * .pswp__button.pswp__button--download:hover etc. (0,3,0), or the rim set
 * above would disappear on hover and on keyboard focus.
 */
.pswp__button.pswp__button--download:hover,
.pswp__button.pswp__button--download:active,
.pswp__button.pswp__button--download:focus {
    border: 2px solid var(--pswp-icon-color);
}

/*
 * .pswp__icn (0,1,0) positions a 32px glyph inside a 50x60 button (top: 14px,
 * left: 9px); beaten by .pswp__button--download .pswp__icn (0,2,0). These
 * absolute offsets resolve against the button's padding box — the 38x38 area
 * inside the 2px rim, since the button is position: relative, box-sizing:
 * border-box — so the icon fills it exactly, with no offset of its own.
 */
.pswp__button--download .pswp__icn {
    top: 0;
    left: 0;
    width: 38px;
    height: 38px;
}

/*
 * The lightbox backdrop: 25% black over a 15px blur of the page, the treatment the
 * old layout's lightbox gives it (.gallery-overlay--medium in gallery.css, where the
 * black is 30%). Both halves sit on .pswp__bg, whose opacity PhotoSwipe's zoom
 * animation takes up from nothing to the bgOpacity option on open and back down on
 * close, so the darkening and the blur fade in and out together.
 *
 * That holds only for the zoom. Without thumbnail bounds PhotoSwipe fades the .pswp
 * root instead, and an ancestor with opacity below 1 is a backdrop root: the blur
 * would sample nothing until the fade ends, then snap in late. gallery_justified_
 * controller.js supplies the bounds from the clicked tile for that reason.
 *
 * That shared opacity is also why the darkening is the colour's alpha rather than
 * bgOpacity: as an element opacity it would composite the blurred layer at 25% over
 * the still-sharp page and leave the blur all but invisible. gallery_justified_
 * controller.js pins bgOpacity at 1 for that reason.
 *
 * Dropping will-change is what makes the blur visible at all. backdrop-filter
 * samples the page only as far back as the nearest ancestor forming a backdrop root,
 * and `will-change: opacity` forms one, so while stock `.pswp, .pswp__bg` carries it
 * the blur here would sample an empty backdrop. The root has no use for the hint —
 * with showHideOpacity off PhotoSwipe writes its opacity once, to 1, and animates
 * .pswp__bg's instead, which keeps its own hint (that one bounds only descendants,
 * and it has none that blur).
 *
 * As with the download button above, this file is linked before the vendored
 * photoswipe.css, so both overrides of a stock .pswp rule (0,1,0) carry one selector
 * part more. pswp--open is on the root for its whole lifetime, close animation
 * included: PhotoSwipe writes it into className when it builds the element and
 * removes the element itself on destroy.
 */
.pswp.pswp--open {
    --pswp-bg: rgba(0, 0, 0, 0.25);
    will-change: auto;
}

.pswp__bg {
    backdrop-filter: blur(15px);
    -webkit-backdrop-filter: blur(15px);
}
