@charset "UTF-8";

/*
 * <mxw-gallery> / <mxw-photo> — light-DOM styles.
 * Copyright (c) 2001-2026 Stephane D'Alu — http://www.sdalu.com/
 *
 * Pairs with mxw-gallery.js (shadow chrome: lightbox dialog,
 * resizer slider, filmstrip). This file only styles the light DOM:
 * the photos, layouts, captions and overlays. Design + class mapping:
 * /web/MoXoW/doc/GALLERY.md.
 *
 * Everything lives in the single layer `mxw.gallery` — one sublayer per
 * component, as mxw-back-to-top, mxw-print, mxw-scm, mxw-map and
 * mxw-climate-chart do — so ANY site rule in a layer `mxw` precedes
 * (theme, site…) overrides it without !important, and an unlayered one
 * always does. The order is MoXoW's, declared inline and first.
 *
 * Unlike its siblings this sheet is not injected by its script: the
 * gallery add-on emits the <link> itself (g:css-path in
 * MoXoW/xsl/add-on/gallery.xsl), so a page that has the gallery has the
 * stylesheet. To pull it into a layer of your own instead:
 *   @import url("/moxow/components/mxw-gallery.css") layer(components);
 *
 * Theming knobs (set them on mxw-gallery or any ancestor):
 *   --mxw-gallery-frame-width    grid frame width             (default 130px)
 *   --mxw-gallery-frame-height   grid frame height            (default: the
 *                        frame width — set it for non-square frames)
 *   --mxw-gallery-gap            gap between photos           (default 1em)
 *   --mxw-gallery-column-width   masonry column width         (default 250px)
 *   --mxw-gallery-row-height     justified target row height  (default 200px)
 *   --mxw-gallery-strip-height   strip thumbnail height       (default 120px)
 *   --mxw-gallery-wall-min       wall min column width         (default 150px)
 *   Deck (layout="deck") — one photograph at a time:
 *   --mxw-gallery-deck-height    tallest a picture may be     (default 60svh)
 *                        The picture's WIDTH is derived from it and the
 *                        picture's own ratio, so nothing is letterboxed.
 *   --mxw-gallery-deck-gap       rows of the NO-SCRIPT column (default: the gap)
 *                        With the script running the deck is one stacked
 *                        row and the placard is flush against it, so this
 *                        only spaces the column the deck degrades to.
 *   --mxw-gallery-deck-fade      cross-fade between photographs (default 800ms;
 *                        the fade="" attribute overrides it per deck)
 *   --mxw-gallery-deck-hold      how long a photograph stays up under
 *                        [autoplay] (default 10000ms; the hold=""
 *                        attribute overrides it per deck)
 *   The PLACARD: the deck's chrome is one strip flush with the picture's
 *   bottom edge, the width of the picture and not of the gallery — a
 *   label under a print, not a toolbar under a form. Its top rule IS the
 *   progress bar (filled part / hairline track); below it, three zones on
 *   a 1fr auto 1fr grid: the counter at the inline start, the dots
 *   optically centred, prev/pause/next at the inline end.
 *   The defaults are deliberately QUIET and theme-neutral — a ground
 *   mixed out of currentcolor, a rule derived from it. A site theme
 *   (mat board, brass rule…) sets these; the framework ships no palette.
 *   --mxw-gallery-deck-placard-background  the strip's ground  (5% of the ink)
 *   --mxw-gallery-deck-placard-color       the strip's ink   (default currentcolor)
 *   --mxw-gallery-deck-placard-padding-inline  strip side padding    (0.6em)
 *   --mxw-gallery-deck-placard-padding-block   below the rule, and under
 *                        the zones                                  (0.45em)
 *   --mxw-gallery-deck-placard-gap         between the three zones   (0.75em)
 *   --mxw-gallery-deck-placard-radius      the strip's two BOTTOM corners
 *                        (default 0: the top edge is the picture's)
 *   --mxw-gallery-deck-placard-rule-thickness  the top rule           (2px)
 *   --mxw-gallery-deck-placard-rule-color  the filled part   (default: the accent)
 *   --mxw-gallery-deck-placard-rule-track  the unfilled hairline (20% ink)
 *   --mxw-gallery-deck-control-size        prev/next/pause box, floored at
 *                        the 24px pointer target                    (2.2rem)
 *   --mxw-gallery-deck-control-gap         gap between the controls   (0.5em)
 *   --mxw-gallery-deck-control-color       glyph ink         (default currentcolor)
 *   --mxw-gallery-deck-control-background  glyph ground      (default none: the
 *                        controls are bare marks, with no box of their own)
 *   --mxw-gallery-deck-dot-size            pager dot diameter         (0.6em)
 *   --mxw-gallery-deck-dot-color           pager dot ink     (default currentcolor)
 *   --mxw-gallery-deck-dot-current-color   the current dot   (default: the accent)
 *   --mxw-gallery-deck-count-size          "3 / 15" counter type size (0.85rem)
 *   --mxw-gallery-deck-count-color         counter ink       (default currentcolor)
 *   The deck's three corner marks: the licence and the author a
 *   photograph carries, along the picture's bottom, and the gallery's
 *   own logo (the .mxw-gallery-logo child) at its top. Deck only: no
 *   other layout shows the logo, and in the others the licence and the
 *   author are 14px thumbnail badges revealed on hover.
 *   --mxw-gallery-deck-mark-size          height of all three, floored
 *                        at the 24px pointer target                   (7%)
 *   --mxw-gallery-deck-mark-inset         distance from the picture's
 *                        bottom corners                            (0.5%)
 *   --mxw-gallery-deck-logo-size          the logo alone, if it should
 *                        differ                     (the mark size above)
 *   --mxw-gallery-deck-logo-inset-inline  distance from the picture's
 *                        inline start                               (0.5%)
 *   --mxw-gallery-deck-logo-inset-block   distance from its top         (1%)
 *   (the percentages are of the PICTURE, as mxw-hero's marks were of the
 *    hero frame, so the mark keeps its proportions on any screen)
 *   --mxw-gallery-caption-gap    gap between a thumb and its caption (1ex)
 *   Popover caption placement (captions="popover"):
 *   --mxw-gallery-caption-width        caption box width      (default max-content)
 *   --mxw-gallery-caption-height       caption box height     (default auto)
 *   --mxw-gallery-caption-top          top inset              (default anchor(bottom))
 *   --mxw-gallery-caption-bottom       bottom inset           (default auto)
 *   --mxw-gallery-caption-translate    self-offset            (default -50% 0)
 *   --mxw-gallery-caption-margin       caption margin box     (default: gap on top)
 *   --mxw-gallery-caption-place        place-content of text  (default start)
 *   --mxw-gallery-caption-lines        with-caption-centered ellipsis clamp (5)
 *   --mxw-gallery-caption-anchor       popover anchor: --mxw-gallery-pic
 *                        (the picture, default) or --mxw-gallery-frame (frame box)
 *   Ready-made modifiers (add the class on the gallery):
 *     with-caption-inside    photo-width band at the picture bottom
 *     with-caption-centered  fills the picture, text centred in the middle
 *     with-caption-none      no caption (wins over any caption default)
 *     with-caption-on-picture  anchor the popover to the picture (default)
 *     with-caption-on-frame    anchor the popover to the frame / layout box
 *   --mxw-gallery-frame-background  grid frame background
 *   --mxw-gallery-grid-border       grid picture border shorthand
 *   --mxw-gallery-hover-color       hover accent
 *   (the theme's mat ribbon uses its own --mxw-gallery-border-width)
 *
 * Per-photo: --mxw-gallery-ratio holds the picture aspect ratio (e.g. 3/2).
 * The XSL emits it inline (style="--mxw-gallery-ratio: 3/2") so justified and
 * masonry layouts reserve space with no JS and no layout shift; the
 * component fills it from the ratio="" attribute as a fallback.
 * (An inline custom property, not typed attr(): attr(type()) is still
 * Chrome-only.)
 */

@layer mxw.gallery {

  mxw-gallery {
    display: flex;
    flex-wrap: wrap;
    gap: var(--mxw-gallery-gap, 1em);
  }

  mxw-photo {
    position: relative;
    display: block;

    /* the picture wrapper: an <a> when the thumb links, an inert <div>
       otherwise (see add-on/gallery.xsl). Both carry class="picture", so
       every layout / decoration / anchor rule targets the wrapper and the
       <img>, never the tag — no bare-<img> special-casing. */
    & > .mxw-picture {
      display: block;
      text-decoration: none;
    }

    & img {
      display: block;
      max-inline-size: 100%;
      max-block-size: 100%;
      aspect-ratio: var(--mxw-gallery-ratio, auto);
      /* safety net: if a layout ever hands the img a box that does not
       * match the picture, letterbox rather than stretch */
      object-fit: contain;
    }
  }

  /*== Layouts =======================================================*/

  /* Grid: fixed square frames, picture letter-boxed inside — replaces
     .gallery.default / .exhibition / .imgbrowse (theme via the knobs). */
  mxw-gallery[layout="grid"] mxw-photo {
    & > .mxw-picture {
      display: flex;
      align-items: center;
      justify-content: center;
      /* A frame is a fixed box by default. A fluid layout -- one that
         hands the gallery a grid and lets each frame fill its track --
         cannot state a height, since it does not know the width; it sets
         --mxw-gallery-frame-ratio and --mxw-gallery-frame-height: auto
         instead, and the height follows the width. */
      inline-size: var(--mxw-gallery-frame-width, 130px);
      block-size: var(--mxw-gallery-frame-height, var(--mxw-gallery-frame-width, 130px));
      aspect-ratio: var(--mxw-gallery-frame-ratio, auto);
      background: var(--mxw-gallery-frame-background, transparent);
    }

    /* the frame border decorates the picture itself */
    & img {
      /* Fill the cell in BOTH axes so object-fit does the
       * letterboxing. Leaving the block size to aspect-ratio looks
       * equivalent until a theme borders the picture: aspect-ratio
       * sizes the BORDER box, so the mat is counted inside the ratio
       * and the content box comes out off-ratio — contain then
       * letterboxes against the mat itself, showing a sliver of it
       * down the long edges. Two definite sizes take the ratio out
       * of the equation; the cell is the frame, contain fits to it. */
      inline-size: 100%;
      block-size: 100%;
      border: var(--mxw-gallery-grid-border, none);
    }

    &:hover img {
      border-color: var(--mxw-gallery-hover-color, currentcolor);
    }
  }

  /* Masonry: CSS multi-columns (native masonry layout is not yet
     cross-browser) — replaces #gallery-travel-log and .waterfall. */
  mxw-gallery[layout="masonry"] {
    display: block;
    column-width: var(--mxw-gallery-column-width, 250px);
    column-gap: var(--mxw-gallery-gap, 1em);

    & mxw-photo {
      margin-block-end: var(--mxw-gallery-gap, 1em);
      break-inside: avoid;

      /* Same trap as the strip: aspect-ratio governs the BORDER box,
         so with a mat drawn on the picture the ratio counts the mat
         in and the content left inside comes out off-ratio, 1.524
         against a 1.501 photograph here, which contain shows as
         1.95px of mat down each long edge. The column already gives
         a definite width; letting the picture set its own height
         against it keeps the content box true. */
      & img {
        inline-size: 100%;
        aspect-ratio: auto;
      }
    }
  }

  /* Justified rows — replaces .stretched-board. Each photo's flex
     values derive from its aspect ratio, so a row of mixed portrait/
     landscape pictures shares one height and fills the line. */
  mxw-gallery[layout="justified"] {
    & mxw-photo {
      flex-grow: calc(var(--mxw-gallery-ratio, 1) * 100);
      flex-basis: calc(var(--mxw-gallery-ratio, 1) * var(--mxw-gallery-row-height, 200px));
      /* pack lines by flex-basis alone: the min-width:auto floor would
         re-inflate an item to its image's natural width as each lazy
         image loads, re-wrapping the rows under a scroll in progress. */
      min-inline-size: 0;

      & img {
        inline-size: 100%;
        /* height comes from inline-size + the aspect ratio; a percentage
           block-size here would depend on the row height, which depends on
           this image — a circular height that browsers resolve to a wrong
           (short) value, capping the page scroll. */
        block-size: auto;
        object-fit: cover;
      }
    }

    /* Oversized spacer keeps the last, incomplete row left-aligned
       instead of stretching its photos to fill the line. */
    &::after {
      content: "";
      flex-grow: 1000000;
    }
  }

  /* Inline strip — replaces .gallery.inline and .g-blog-pics. A centred
     flex row with gap. (This works now that the srcset carries no
     sizes="auto": with the picture's true ratio restored, a flex item
     sizes to max-block-size x ratio, not the 300px default object width
     that used to blow every cell out.) */
  mxw-gallery[layout="strip"] {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: var(--mxw-gallery-gap, 1em);

    & mxw-photo {
      flex: 0 1 auto;
      min-inline-size: 0;       /* may shrink below content on a tight line */
    }

    & mxw-photo img {
      /* height caps at the strip height and width follows the ratio;
       * when the line is too narrow the base max-inline-size:100%
       * shrinks the width and the height scales DOWN with it */
      inline-size: auto;
      block-size: auto;
      max-block-size: var(--mxw-gallery-strip-height, 120px);

      /* The same trap the grid cell fell into, and it needs the opposite
       * remedy. aspect-ratio governs the BORDER box, so with a theme
       * that draws a mat on the picture the ratio counted the mat in:
       * 163.2 by 108.8 outside came to 1.500 while the content left
       * inside was 1.540 against a 1.501 photograph, and contain fitted
       * to the short axis and left 2px of mat showing down each long
       * edge.
       *
       * The grid answered with two definite sizes, which a strip cannot
       * have: its width has to stay auto so a narrow line can shrink it.
       * Dropping the ratio does the same job here. It is only there to
       * reserve space before the picture loads, which is the justified
       * and masonry layouts' problem rather than this one, and with it
       * gone the picture's own ratio governs its content box, which is
       * what contain measures against. */
      aspect-ratio: auto;
    }
  }

  /* Full-bleed wall — replaces .g-picture-wall. */
  mxw-gallery[layout="wall"] {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(var(--mxw-gallery-wall-min, 150px), 1fr));
    gap: 0;

    & mxw-photo img {
      inline-size: 100%;
      block-size: 100%;
      object-fit: cover;
    }
  }

  /* List: captions only — replaces .gallery-as-list and the old
     caption-only .compact-view. */
  mxw-gallery[layout="list"] {
    display: block;

    & mxw-photo {
      & > .mxw-picture,
      & [slot="description"] { display: none; }

      & [slot="headline"] { display: block; }
    }
  }

  /* Deck: one photograph at a time, stepped through — the arrangement is
     a STACK, not a row, so the layout is a one-column grid whose photos
     all sit in row 1 and whose chrome (progress bar, control bar) takes
     the rows below. The component appends that chrome to the LIGHT DOM,
     as it already appends the credit marks and the [selectable] button,
     which is why every rule for it is here rather than in the script's
     shadow sheet — and why every rule is scoped under
     mxw-gallery[layout="deck"] and can reach no other layout.

     Two halves, deliberately:

     - the SIZING half is unconditional. With no javascript nothing
       stacks and nothing is hidden: the grid puts each photograph on its
       own row, at the same generous size, and the deck degrades to a
       column of big pictures with working links. Nothing is empty and
       nothing is dead.
     - the STACKING half hangs off :defined, which is true exactly when
       the element has upgraded, i.e. when there is a script to drive it.
       CSS cannot tell "javascript is off" from "the module has not run
       yet", and there is no third state to test, so a load paints the
       no-JS column until the module runs. Measured on a real page over
       several loads: the upgrade lands 24ms, 52ms and 87ms after first
       contentful paint — one to five frames of column before the deck
       closes up, the worst of it on a cold cache. It is the price of
       degrading to something real rather than to an empty box.

     The photo box is sized to the picture, not stretched to the row:
     its width is the height cap multiplied by the picture's own ratio,
     clamped to the row. That way the height cap never has to letterbox
     (the trap documented on the grid and strip layouts above), the box
     is definite before the picture loads (no layout shift), and the
     absolutely-positioned licence / author marks land on the picture's
     corners rather than out at the edges of an empty band. */
  mxw-gallery[layout="deck"] {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    gap: var(--mxw-gallery-deck-gap, var(--mxw-gallery-gap, 1em));

    & > mxw-photo {
      inline-size: min(100%, calc(var(--mxw-gallery-deck-height, 60svh)
                                  * var(--mxw-gallery-ratio, 1)));
      text-align: center;
    }

    & mxw-photo img {
      inline-size: 100%;
      block-size: auto;
      aspect-ratio: var(--mxw-gallery-ratio, auto);
    }
  }

  /* The stack, and the cross-fade across it. visibility, not display:
     it animates as a discrete step at the end of the fade, it keeps the
     box in the grid so the deck's height is the tallest photograph and
     never jumps, and it takes the hidden photographs out of the
     accessibility tree AND out of the tab order — so the deck offers the
     keyboard exactly one photograph, the one on screen. They stay in the
     light DOM either way, which is what a crawler reads.

     The stack is aligned to its BOTTOM, and the row gap is closed: the
     placard below it is attached to the picture's bottom edge, so print
     and label have to read as one object. With one orientation that is
     what centring already gave (every picture is the height cap); with a
     deck mixing portrait and landscape it is what keeps the label
     touching the picture instead of floating under the tallest one, and
     the slack all goes above. --mxw-gallery-deck-gap keeps its meaning
     for the no-script column, which is a real stack of rows. */
  mxw-gallery[layout="deck"]:defined {
    row-gap: 0;

    & > mxw-photo {
      grid-area: 1 / 1;
      align-self: end;
      opacity: 0;
      visibility: hidden;
      transition: opacity var(--mxw-gallery-deck-fade, 800ms) ease,
                  visibility 0s linear var(--mxw-gallery-deck-fade, 800ms);
    }

    & > mxw-photo.is-current {
      opacity: 1;
      visibility: visible;
      transition: opacity var(--mxw-gallery-deck-fade, 800ms) ease,
                  visibility 0s;
    }
  }

  /*-- The deck's logo mark ------------------------------------------*
     The container's own credit — the site's logo, in the picture's TOP
     corner, where the licence and the author a photograph carries sit
     along its bottom. <mxw-hero> had the same three marks and this is
     the third of them; unlike the other two it belongs to the gallery,
     not to any one picture, so the page writes it as a child of the
     gallery: <a class="mxw-gallery-logo"> with whatever inside.

     DECK ONLY. A grid, a wall or a row of thumbnails has no single
     picture to mark and no corner that means anything, so every rule
     here is scoped under mxw-gallery[layout="deck"] and the mark is not
     displayed at all in another layout.

     The corner it has to hug is the PICTURE's, and the picture is not
     the grid cell: it is centred in it, sized min(100%, height x its own
     ratio). So the script frames the mark in a .mxw-gallery-deck-logo box given
     that same size, that same ratio and the same centring in the same
     grid area — the frame is then exactly the photograph on top,
     whatever its proportions, and the mark hangs off its corner with the
     insets mxw-hero used. The frame takes no pointer events: it covers
     the picture, which is usually a link of its own.

     No frame without javascript, and none is wanted: nothing stacks
     there either, so the mark is simply the link the page wrote, at the
     head of the column the deck degrades to. */

  mxw-gallery:not([layout="deck"]) > .mxw-gallery-logo { display: none; }

  mxw-gallery[layout="deck"] {
    & > .mxw-gallery-deck-logo {
      position: relative;      /* the mark is placed against the picture */
      z-index: 5;              /* over the photograph and its own marks */
      box-sizing: border-box;
      grid-area: 1 / 1;
      place-self: end center;    /* the picture's alignment, exactly */
      inline-size: min(100%, calc(var(--mxw-gallery-deck-height, 60svh)
                                  * var(--mxw-gallery-deck-ratio, 1)));
      aspect-ratio: var(--mxw-gallery-deck-ratio, 1);
      pointer-events: none;
    }

    & > .mxw-gallery-deck-logo > .mxw-gallery-logo {
      position: absolute;
      inset-block-start:  var(--mxw-gallery-deck-logo-inset-block,  1%);
      inset-inline-start: var(--mxw-gallery-deck-logo-inset-inline, 0.5%);
      display: block;
      /* A proportion of the picture, as in mxw-hero, but never below
         the 24px pointer-target floor (WCAG 2.5.8) — 7% of a
         phone-height photograph is 17px of link. */
      block-size: max(var(--mxw-gallery-deck-logo-size,
                          var(--mxw-gallery-deck-mark-size, 7%)), 24px);
      inline-size: fit-content;
      pointer-events: auto;      /* the frame gave them up; take them back */
    }

    & > .mxw-gallery-deck-logo img {
      block-size: 100%;
      inline-size: auto;
      object-fit: contain;       /* a badge is never cropped */
    }

    /* No badge: the mark is the site's name in words, on a plate dark
       enough to sit on any photograph (mxw-hero's fallback, verbatim). */
    & > .mxw-gallery-deck-logo > .mxw-gallery-logo:not(:has(img)) {
      block-size: auto;
      padding: 0.35em 0.6em;
      color: #f2f2f2;
      background: rgb(26 26 26 / 0.75);
      border-radius: 0.25em;
      font-size: 0.75rem;
      line-height: 1.2;
      text-decoration: none;
      white-space: nowrap;
    }

    /* Two-tone, because the backdrop is a photograph: the white ring
       shows against a dark picture, the dark one against a light one. */
    & > .mxw-gallery-deck-logo > .mxw-gallery-logo:focus-visible {
      outline: 2px solid #1a1a1a;
      outline-offset: 2px;
      box-shadow: 0 0 0 2px #fff;
    }

    @media (forced-colors: active) {
      & > .mxw-gallery-deck-logo > .mxw-gallery-logo:focus-visible {
        outline-color: Highlight;
      }
    }
  }

  /*-- Deck chrome: the placard --------------------------------------*
     One strip, flush with the picture's bottom edge, the width of the
     PICTURE and not of the gallery — the label under a print, in the
     register the rest of this component is written in (frame, mat,
     filmstrip, wall). It replaces what the deck first grew: a centred row
     of boxed buttons, a dot pager, and a separate full-width progress bar
     under them. Three competing objects, and all three read as a form
     toolbar.

     Anatomy, top to bottom:

     - the RULE along the placard's top IS the progress bar. Filled part
       in the accent, the rest a hairline track; it is drawn whether or
       not the deck rotates, because it is also the placard's own top
       edge — with no [autoplay] (or no [progress]) there is simply no
       fill inside it, and the strip keeps its line. Still driven by the
       one Animation object that IS the hold's clock, so a pause freezes
       it exactly where the countdown froze.
     - three ZONES on a `1fr auto 1fr` grid: the counter at the inline
       start, the dots in the middle, prev/pause/next at the inline end.
       A grid and not a flex row with space-between, so the middle zone
       stays OPTICALLY centred however wide the two sides grow (measured
       at 1200px and at 390px: the dot rail's centre and the placard's
       are the same pixel). Below 26em of placard the rail takes a line
       of its own and the two sides keep the one under it.

     The width trick is the .mxw-gallery-deck-logo frame's, not a second one: the
     picture is centred in the grid cell and sized min(100%, height x its
     own ratio), so the placard is given that same expression against
     --mxw-gallery-deck-ratio, which the script keeps at the ratio of the
     photograph on top.

     Everything here is quiet and theme-neutral on purpose. The ground is
     a few per cent of the ink and the rule is derived from it, so the
     placard sits under any photograph on any site; a theme that wants
     mat board and a brass rule sets the --mxw-gallery-deck-placard-*
     knobs from its own palette. The framework ships no palette. */

  mxw-gallery[layout="deck"] {
    & > .mxw-gallery-deck-placard {
      box-sizing: border-box;
      /* the picture's box, by the picture's own expression */
      inline-size: min(100%, calc(var(--mxw-gallery-deck-height, 60svh)
                                  * var(--mxw-gallery-deck-ratio, 1)));
      /* The placard is its own container so that the zones can reflow
         against the PICTURE's width — which is what they are laid out
         against — and not against the viewport's. A portrait photograph
         on a wide page has a narrow placard, and a viewport media query
         would never hear about it. */
      container-type: inline-size;
      display: grid;
      /* 1fr auto 1fr keeps the middle zone optically centred whatever
         the two sides weigh — the sides are equal tracks, not the
         leftovers of a space-between row. The max-content floor is what
         stops a long dot rail from starving them: without it the middle
         zone takes its full width first and the controls stack into a
         vertical column (measured: 3 rows at 390px with 12 dots). */
      grid-template-columns:
          minmax(max-content, 1fr) auto minmax(max-content, 1fr);
      align-items: center;
      column-gap: var(--mxw-gallery-deck-placard-gap, 0.75em);
      /* the rule sits ON the top edge: no padding above it, and the row
         gap below it is the same measure as the padding under the zones */
      row-gap: var(--mxw-gallery-deck-placard-padding-block, 0.45em);
      padding: 0 var(--mxw-gallery-deck-placard-padding-inline, 0.6em)
                 var(--mxw-gallery-deck-placard-padding-block, 0.45em);
      color: var(--mxw-gallery-deck-placard-color, currentcolor);
      background: var(--mxw-gallery-deck-placard-background,
                      color-mix(in oklch, currentcolor 5%, transparent));
      border-end-start-radius: var(--mxw-gallery-deck-placard-radius, 0);
      border-end-end-radius:   var(--mxw-gallery-deck-placard-radius, 0);
    }

    /* The three zones, placed explicitly rather than by source order, so
       an absent counter or an absent pager moves nothing. justify-self is
       logical: the zones mirror in a right-to-left deck by themselves. */
    & .mxw-gallery-deck-count    { grid-area: 2 / 1; justify-self: start;  }
    & .mxw-gallery-deck-pager    { grid-area: 2 / 2; justify-self: center; }
    & .mxw-gallery-deck-controls { grid-area: 2 / 3; justify-self: end;    }

    /* Too narrow a placard for a rail between two zones: the dots take a
       line of their own, still centred across the whole strip, and the
       counter and the controls keep the line below to themselves. Only
       when there IS a rail — with a bare counter the one line always
       fits, and an empty middle row would open a gap for nothing.
       (26em: below it a twelve-dot rail cannot share a line with a
       counter and three controls without wrapping something.) */
    @container (width < 26em) {
      & .mxw-gallery-deck-placard:has(.mxw-gallery-deck-pager) {
        & > .mxw-gallery-deck-pager    { grid-area: 2 / 1 / 3 / -1; }
        & > .mxw-gallery-deck-count    { grid-area: 3 / 1; }
        & > .mxw-gallery-deck-controls { grid-area: 3 / 3; }
      }
    }

    /* justify-content, not a margin: the cluster hangs off the inline
       END of its zone, which is where the placard's edge is. It may wrap
       at an absurd width, and a second line is the right answer where a
       sideways scroll never is. */
    & .mxw-gallery-deck-controls {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      justify-content: end;
      gap: var(--mxw-gallery-deck-control-gap, 0.5em);
    }

    /* Bare marks: no border, no radius, no ground. A photograph's own
       label does not put its arrows in boxes. The 24px pointer target
       (WCAG 2.5.8) is kept by the BOX, still 2.2rem across with a 24px
       floor and padding inside it — the boxes came off, the things you
       hit did not shrink by a pixel. rem and no longer em, because the
       three controls now set their own type size for the glyphs and an
       em box would have made them three different sizes. */
    & :is(.mxw-gallery-deck-prev, .mxw-gallery-deck-next, .mxw-gallery-deck-play) {
      display: grid;
      place-items: center;      /* the glyph ink, not its line box */
      box-sizing: border-box;
      inline-size: max(var(--mxw-gallery-deck-control-size, 2.2rem), 24px);
      block-size:  max(var(--mxw-gallery-deck-control-size, 2.2rem), 24px);
      padding: 0.25em;          /* the target is padding, never a crop */
      /* a <button> does not inherit type: without this the arrowheads
         come out in the UA's serif default, beside a placard set in the
         page's own face (measured: Times New Roman on alux-studio) */
      font-family: inherit;
      font-size: 1.35rem;       /* ‹ and › are hairline glyphs: the ink
                                   has to carry beside the pause bars */
      line-height: 1;
      color: var(--mxw-gallery-deck-control-color, currentcolor);
      cursor: pointer;
      background: var(--mxw-gallery-deck-control-background, none);
      border: 0;
      transition: color 0.15s ease;
    }

    & :is(.mxw-gallery-deck-prev, .mxw-gallery-deck-next, .mxw-gallery-deck-play):hover {
      color: var(--mxw-gallery-accent, var(--mxw-gallery-hover-color, currentcolor));
    }

    /* ‹ and › are typed characters, so in a right-to-left deck they have
       to be turned round: the flex row already reverses the controls'
       PLACE, but the arrowheads would still point the wrong way. */
    &:dir(rtl) :is(.mxw-gallery-deck-prev, .mxw-gallery-deck-next) { scale: -1 1; }

    /* The pause/resume mark is DRAWN, not typed. U+23F8 PAUSE BUTTON and
       U+25B6 have emoji presentation and are absent from a good many
       font stacks (they came out as tofu in the first capture of this
       deck), and a control whose only visible content is a missing glyph
       is a blank button. Two bars out of the inline borders, a triangle
       out of one of them; the accessible name is the aria-label, which
       the script keeps in step. Logical sides, so the triangle points
       the way the deck runs. The em values are proportions of the
       control's own type size (set just above), chosen so the mark
       carries the weight of the arrowheads beside it and no more. */
    & .mxw-gallery-deck-play::before {
      content: "";
      box-sizing: border-box;
      inline-size: 0.46em;
      block-size: 0.6em;
      border: 0 solid currentcolor;
      border-inline-width: 0.15em;
    }

    & .mxw-gallery-deck-play.is-paused::before {
      inline-size: 0;
      block-size: 0;
      border: 0 solid transparent;
      border-block-width: 0.28em;
      border-inline-start: 0.46em solid currentcolor;
    }

    /* Never leave a control without a ring: the buttons are the only way
       through the deck for a keyboard, and site themes do reset outlines. */
    & :is(.mxw-gallery-deck-prev, .mxw-gallery-deck-next,
          .mxw-gallery-deck-play, .mxw-gallery-deck-dot):focus-visible {
      outline: 2px solid var(--mxw-gallery-accent, currentcolor);
      outline-offset: 2px;
    }

    & .mxw-gallery-deck-pager {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      justify-content: center;
      gap: 0;      /* the dots' own padding is the gap: see below */
    }

    /* A dot reads as ~0.6em of ink and is hit as a 24px square — the ink
       is drawn by ::before inside a target that keeps the WCAG 2.5.8
       floor without a rail of fat circles. */
    & .mxw-gallery-deck-dot {
      display: grid;
      place-items: center;
      inline-size: 24px;
      block-size: 24px;
      padding: 0;
      cursor: pointer;
      background: none;
      border: none;
    }

    & .mxw-gallery-deck-dot::before {
      content: "";
      inline-size: var(--mxw-gallery-deck-dot-size, 0.6em);
      block-size:  var(--mxw-gallery-deck-dot-size, 0.6em);
      background: var(--mxw-gallery-deck-dot-color, currentcolor);
      border-radius: 50%;
      opacity: 0.35;
      transition: opacity 0.15s ease, scale 0.15s ease;
    }

    & .mxw-gallery-deck-dot:hover::before { opacity: 0.7; }

    & .mxw-gallery-deck-dot[aria-current]::before {
      background: var(--mxw-gallery-deck-dot-current-color,
                      var(--mxw-gallery-accent, currentcolor));
      opacity: 1;
      scale: 1.25;
    }

    /* tabular figures: "9 / 15" and "10 / 15" must not shuffle the zones */
    & .mxw-gallery-deck-count {
      font-size: var(--mxw-gallery-deck-count-size, 0.85rem);
      font-variant-numeric: tabular-nums;
      line-height: 1;
      white-space: nowrap;
      color: var(--mxw-gallery-deck-count-color, currentcolor);
    }

    /* The rule along the placard's top, which is also the progress bar.
       Full bleed — the negative inline margin cancels the placard's
       padding — so it measures the PICTURE, which is what makes it read
       as a fraction of the hold rather than as a border.

       The fill is driven by the script as a scaleX: one
       compositor-friendly property, no layout per frame, and the same
       Animation object that IS the hold's clock, so pausing the deck
       freezes the rule exactly where it stood. */
    & .mxw-gallery-deck-rule {
      grid-area: 1 / 1 / 2 / -1;
      margin-inline: calc(-1 * var(--mxw-gallery-deck-placard-padding-inline,
                                   0.6em));
      block-size: var(--mxw-gallery-deck-placard-rule-thickness, 2px);
      overflow: hidden;
      background: var(--mxw-gallery-deck-placard-rule-track,
                      color-mix(in oklch, currentcolor 20%, transparent));
    }

    & .mxw-gallery-deck-rule-fill {
      block-size: 100%;
      background: var(--mxw-gallery-deck-placard-rule-color,
                      var(--mxw-gallery-accent, currentcolor));
      transform: scaleX(0);
      transform-origin: left center;
    }

    /* transform-origin has no logical spelling; a right-to-left deck
       fills from its own inline start, which is the right edge. */
    &:dir(rtl) .mxw-gallery-deck-rule-fill { transform-origin: right center; }

    /* Forced colours flatten every background to Canvas, which would
       make the filled part indistinguishable from the track. Two system
       colours instead — the only palette a forced-colours page allows. */
    @media (forced-colors: active) {
      & .mxw-gallery-deck-rule {
        forced-color-adjust: none;
        background: GrayText;
      }

      & .mxw-gallery-deck-rule-fill {
        forced-color-adjust: none;
        background: Highlight;
      }
    }

    /* Position announcements for AT, invisible to everyone else. Written
       only when the reader moved the deck themselves (see mxw-gallery.js). */
    & .mxw-gallery-deck-live {
      position: absolute;
      inline-size: 1px;
      block-size: 1px;
      margin: -1px;
      padding: 0;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
      border: 0;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    /* The rotation is parked by the script, so there is no time left to
       draw: an always-empty fill is worse than none. The RULE stays —
       it is the placard's top edge, not only the countdown — and it is
       exactly what an [autoplay]-less deck shows anyway. It comes back
       with the setting: the media query is live, not sampled. */
    mxw-gallery[layout="deck"] .mxw-gallery-deck-rule-fill { display: none; }

    /* And a hand-driven step lands instantly. The blanket rule at the
       foot of this file already says transition:none for everything
       inside a gallery, but these two selectors are more specific than
       it and would keep their cross-fade; the deck says it again where
       it will be heard. Consistent with mxw-hero, which holds that a
       photograph replacing another IS the motion and the fade is only
       how it is drawn. */
    mxw-gallery[layout="deck"]:defined > mxw-photo,
    mxw-gallery[layout="deck"]:defined > mxw-photo.is-current {
      transition: none;
    }
  }

  /*== Captions ======================================================*/

  /* Below the thumbnail — the DEFAULT caption, written once and shared
     with an explicit .with-caption-below class via this selector list
     (the vanilla stand-in for a Sass @extend / %placeholder): the block
     styles both the bare default AND the class, so "below" is the
     default and is also nameable. NB the class only re-asserts below; to
     force it back where a site defaults to captions="popover", also set
     the gallery's caption="below" (the attribute is the single mode
     source of truth — a class alone won't outrank the popover rules). */
  mxw-photo,
  mxw-gallery.with-caption-below mxw-photo {
    /* the gap sits above the first line */
    & [slot="headline"] {
      display: block;
      margin-block-start: var(--mxw-gallery-caption-gap, 1ex);
      font-weight: bold;
    }

    & [slot="headline"] + [slot="description"] { margin-block-start: 0; }

    & [slot="description"] {
      display: block;
      margin-block-start: var(--mxw-gallery-caption-gap, 1ex);
    }
  }

  mxw-gallery[captions="none"] mxw-photo {
    & [slot="headline"],
    & [slot="description"] { display: none; }
  }

  /* Caption as an anchored tooltip that pops UNDER the picture on
     hover/focus (flipping above if it would clip). The component gives
     it neutral shape and behaviour; a theme paints it. Falls back to
     the native title tooltip where anchor positioning is unavailable
     (the caption stays hidden). */
  mxw-gallery[captions="popover"] mxw-photo {
    /* scope the names per photo: --mxw-gallery-pic (the actual picture),
       --mxw-gallery-frame (its layout box — the grid's fixed square, the
       cell in other layouts) and --mxw-gallery-cap (the headline the
       description hangs under). The caption chooses pic vs frame through
       --mxw-gallery-caption-anchor (default: the picture). */
    anchor-scope: --mxw-gallery-pic, --mxw-gallery-frame, --mxw-gallery-cap;

    /* the frame / layout box — the .mxw-picture wrapper */
    & > .mxw-picture { anchor-name: --mxw-gallery-frame; }

    /* the picture itself — the <img> inside the wrapper */
    & > .mxw-picture > img { anchor-name: --mxw-gallery-pic; }

    & [slot="headline"],
    & [slot="description"] { display: none; }

    /* A shown caption overflows outside the photo box (it sits below the
       picture); lift the hovered/focused photo above the later-DOM
       sibling pictures so the caption paints over them, not behind. */
    &:hover,
    &:has(:focus-visible) { z-index: 2; }

    @supports (anchor-name: --x) {
      & [slot="headline"],
      & [slot="description"] {
        display: grid;         /* so --mxw-gallery-caption-place can align the
                                  text when the box is given a height */

        /* Fixed, not absolute, and the reason is fragmentation. An
           absolutely positioned box whose containing block sits inside a
           multi-column block belongs to that block's fragmentation
           context, so a caption near the foot of a column was cut in two
           by the gutter: a sliver of 8px left behind and the remaining
           53px at the head of the next column. break-inside: avoid does
           not stop it.
           A fixed box is positioned against the viewport, so it is
           outside the fragmentation context and cannot be cut. Anchor
           positioning still places it against its picture and still
           follows it as the page scrolls, measured at 6.9px below the
           picture before and after scrolling; and position-visibility
           takes it away with its anchor rather than leaving it stranded
           over the page. */
        position: fixed;
        position-visibility: anchors-visible;
        /* the caption's own centre sits under the anchor's centre:
           left edge at anchor centre, then shifted back half its width.
           Flips above (staying centred) only if there is no room below.
           --mxw-gallery-caption-width defaults to the text's own width; set it
           to anchor-size(width) to lock the caption to the photo width. */
        left: anchor(center);
        translate: var(--mxw-gallery-caption-translate, -50% 0);
        position-try-fallbacks: flip-block;
        inline-size: var(--mxw-gallery-caption-width, max-content);
        block-size: var(--mxw-gallery-caption-height, auto);
        max-inline-size: 16em;
        padding: 0.4ex 0.8ex;
        place-content: var(--mxw-gallery-caption-place, start);
        color: white;
        background: oklch(0% 0 0 / 0.72);
        border-radius: 0.5ex;
        font-size: 0.8rem;
        line-height: 1.3;
        text-align: center;
        /* display, not visibility. A hidden caption used to stay in the
           layout so it could fade, and an absolutely positioned box
           counts towards the scrollable area whether or not anyone can
           see it: on a strip whose outermost thumbnail sits at the page
           edge, a 197px caption centred on a 133px picture hung 28px
           past it, hovered or not, and the page scrolled sideways.

           Out of the layout there is nothing to hang. The fade survives
           on the transition: display is discrete, so it needs
           allow-discrete to be animatable at all, and @starting-style
           below gives the entry somewhere to come from. */
        display: none;
        opacity: 0;
        scale: 0.96;
        transition: opacity 0.15s ease, scale 0.15s ease,
                    display 0.15s allow-discrete;
        pointer-events: none;
      }

      /* headline: by DEFAULT its top edge sits just below the picture
         (outside). The class with-caption-inside flips these tokens to
         anchor the caption's BOTTOM inside the picture. Physical top/
         bottom, NOT logical inset-block-*: anchor(bottom) is a physical
         side and is dropped from a logical inset property, which would
         leave both auto and place the caption over the picture. */
      & [slot="headline"] {
        position-anchor: var(--mxw-gallery-caption-anchor, --mxw-gallery-pic);
        top:    var(--mxw-gallery-caption-top, anchor(bottom));
        bottom: var(--mxw-gallery-caption-bottom, auto);
        margin: var(--mxw-gallery-caption-margin,
                    var(--mxw-gallery-caption-gap, 1ex) 0 0);
        font-weight: bold;
        anchor-name: --mxw-gallery-cap;      /* the description hangs off it */
      }

      /* description: below the headline, or below the picture if there
         is no headline */
      & [slot="description"] {
        position-anchor: --mxw-gallery-cap;
        top: anchor(bottom);
        margin-block-start: 0.3ex;
      }
      &:not(:has([slot="headline"])) [slot="description"] {
        position-anchor: var(--mxw-gallery-caption-anchor, --mxw-gallery-pic);
        top:    var(--mxw-gallery-caption-top, anchor(bottom));
        bottom: var(--mxw-gallery-caption-bottom, auto);
        margin: var(--mxw-gallery-caption-margin,
                    var(--mxw-gallery-caption-gap, 1ex) 0 0);
      }

      &:hover [slot="headline"],
      &:hover [slot="description"],
      &:has(:focus-visible) [slot="headline"],
      &:has(:focus-visible) [slot="description"] {
        display: grid;
        opacity: 1;
        scale: 1;
        transition-delay: 0s;

        @starting-style { opacity: 0; scale: 0.96; }
      }
    }
  }

  @media (prefers-reduced-motion: reduce) {
    mxw-gallery[captions="popover"] mxw-photo :is([slot="headline"],
                                                  [slot="description"]) {
      transition: none;
      scale: none;
    }
  }

  /* Pick what the popover caption anchors to. The DEFAULT is the picture
     (--mxw-gallery-pic), so the caption hugs the real image bottom in every
     orientation: in a grid a landscape picture is letter-boxed inside its
     square frame, and anchoring to the frame would drop the caption into
     the empty band below the picture (fine for portraits, which fill the
     frame, wrong for landscape). Add .with-caption-on-frame to anchor to
     the fixed frame box instead — a stable baseline at the price of that
     gap under short pictures. .with-caption-on-picture is the explicit
     spelling of the default. (Only the grid layout differs: elsewhere the
     picture box already is the frame box.) */
  mxw-gallery.with-caption-on-picture { --mxw-gallery-caption-anchor: --mxw-gallery-pic;   }
  mxw-gallery.with-caption-on-frame   { --mxw-gallery-caption-anchor: --mxw-gallery-frame; }

  /* Opt-in modifier: drop the popover caption INSIDE the picture as a
     photo-width band anchored to the picture's bottom (default is the
     tooltip hanging below). Apply on the gallery: class="with-inside-
     caption". The band is inset from every picture edge by the caption
     gap, so it clears the frame border. */
  mxw-gallery.with-caption-inside {
    --mxw-gallery-caption-top:    auto;
    --mxw-gallery-caption-bottom: anchor(bottom);
    --mxw-gallery-caption-margin: 0 0 var(--mxw-gallery-caption-gap, 1ex);
    --mxw-gallery-caption-width:
      calc(anchor-size(width) - 2 * var(--mxw-gallery-caption-gap, 1ex));
    --mxw-gallery-caption-place: center;  /* the band spans the picture:
                                             centred text reads as a label,
                                             start-aligned as a leftover */
  }

  /* Opt-in modifier: the caption fills the picture centred both ways,
     text centred in the middle. Inset from every edge by the caption
     gap so it clears the frame border. */
  mxw-gallery.with-caption-centered {
    --mxw-gallery-caption-top:       anchor(center);
    --mxw-gallery-caption-bottom:    auto;
    --mxw-gallery-caption-translate: -50% -50%;
    --mxw-gallery-caption-margin:    0;
    --mxw-gallery-caption-place:     center;
    --mxw-gallery-caption-width:
      calc(anchor-size(width)  - 2 * var(--mxw-gallery-caption-gap, 1ex));
    --mxw-gallery-caption-height:
      calc(anchor-size(height) - 2 * var(--mxw-gallery-caption-gap, 1ex));
  }

  /* ...and ellipsize when the text overflows the filled box: clamp to
     the lines that fit (tune via --mxw-gallery-caption-lines) with a
     trailing ellipsis, the clamped block staying vertically centred.
     Inside @supports since it only matters where the popover renders. */
  @supports (anchor-name: --x) {
    mxw-gallery.with-caption-centered mxw-photo
        :is([slot="headline"], [slot="description"]) {
      display: -webkit-box;
      -webkit-box-orient: vertical;
      -webkit-box-pack: center;      /* keep the clamped lines centred */
      -webkit-line-clamp: var(--mxw-gallery-caption-lines, 5);
      line-clamp: var(--mxw-gallery-caption-lines, 5);
      overflow: hidden;
    }
  }

  /* Opt-in modifier: no caption at all. Unlike the placement classes
     this wins over any caption mode (display:none, last word), so it
     reliably suppresses even a site-wide caption default. */
  mxw-gallery.with-caption-none mxw-photo :is([slot="headline"],
                                            [slot="description"]) {
    display: none;
  }

  /*== License / author overlays =====================================*
   * The component builds a.mxw-gallery-license / a.mxw-gallery-author from the photo
   * attributes when the gallery asks for them; hidden until hover, as
   * in the .waterfall galleries. */
  mxw-photo {
    & a.mxw-gallery-license,
    & a.mxw-gallery-author {
      position: absolute;
      inset-block-end: 4px;
      z-index: 1;
      display: block;
      block-size: 14px;
      opacity: 0;
      transition: opacity 0.3s;

      & img { block-size: 100%; aspect-ratio: auto; }
    }

    & a.mxw-gallery-license { inset-inline-start: 4px; }

    & a.mxw-gallery-author {
      inset-inline-end: 4px;
      /* Double drop-shadow keeps a light logo readable on any picture. */
      filter: drop-shadow(0 0 1px oklch(0% 0 0))
              drop-shadow(4px 4px 8px oklch(0% 0 0 / 0.8));
    }

    &:hover a.mxw-gallery-license,
    &:hover a.mxw-gallery-author,
    &:focus-within a.mxw-gallery-license,
    &:focus-within a.mxw-gallery-author { opacity: 1; }
  }

  /*-- The deck's licence and author marks ---------------------------*
     Everywhere else those two are 14px and appear on hover: that is the
     thumbnail rule, where a picture is one of many and a standing badge
     would be noise. A deck is ONE photograph filling the page, the
     marks are part of how it is presented, and hover does not exist on
     touch — so on a deck they follow the logo's rule instead, a
     proportion of the picture with the same target floor, and they stay
     visible. <mxw-hero> carried the same three marks the same way; the
     deck kept it for the logo only, and the other two were left at the
     size a thumbnail wants. */

  mxw-gallery[layout="deck"] mxw-photo {
    & a.mxw-gallery-license,
    & a.mxw-gallery-author {
      block-size: max(var(--mxw-gallery-deck-mark-size, 7%), 24px);
      inset-block-end: var(--mxw-gallery-deck-mark-inset, 0.5%);
      opacity: 1;
    }

    & a.mxw-gallery-license { inset-inline-start: var(--mxw-gallery-deck-mark-inset, 0.5%); }
    & a.mxw-gallery-author  { inset-inline-end:   var(--mxw-gallery-deck-mark-inset, 0.5%); }
  }

  /*== Selection (cart mode) =========================================*
   * The component adds button.a-select when the gallery is
   * [selectable]; state is reflected as mxw-photo[selected]. */
  mxw-photo button.a-select {
    position: absolute;
    inset-block-end: 0;
    inset-inline-end: 0;
    z-index: 2;
    inline-size: 20px;
    block-size: 20px;
    margin: 3px;
    padding: 0;
    font-weight: bold;
    line-height: 1;
    color: white;
    cursor: pointer;
    background: oklch(0% 0 0 / 0.6);
    border: none;
    border-radius: 3px;
    opacity: 0;
  }

  mxw-photo:hover button.a-select,
  mxw-photo:focus-within button.a-select { opacity: 1; }

  mxw-photo[selected] button.a-select {
    background: oklch(55% 0.15 145);
    opacity: 1;
  }

  /*== Misc ==========================================================*/

  mxw-gallery[limit] mxw-photo.is-over-limit { display: none; }

  @media (prefers-reduced-motion: reduce) {
    mxw-gallery *, mxw-gallery *::after {
      transition: none;
    }
  }
}
