/* ============================================================================
   THE PHOTOGRAPH WALL — /photos

   Loaded AFTER assets/page.css, which it never edits (that file's own comment
   warns that a plain rule added below its phone media block silently loses,
   and it is shared with four finished pages). Everything here is additive and
   at a specificity that wins.

   HOW THE WALL IS BUILT, because it is not a grid and the difference matters
   -------------------------------------------------------------------------
   The brief was: connect the photographs into one puzzle, and keep every
   ratio exactly as it is so nothing is cut. Those two pull against each other
   — a grid of equal cells can only hold mixed ratios by cropping them, and
   these run 0.37 to 1.94, a 5x spread. So this is not a grid. It is the
   JUSTIFIED ROW layout: every row is exactly as wide as the page, every
   photograph in a row shares one height, and each one's width is that height
   times its own ratio. Rows differ in height, photographs differ in width,
   nothing is ever cut, and the edges all line up. That is the puzzle.

   The whole thing is three declarations of arithmetic, no script:

     flex-basis: --r * --rowh     a photograph's "natural" width at the target
                                  row height, so the browser's own wrapping
                                  decides how many fit on a line
     flex-grow:  --r              the leftover width on a line is handed out in
                                  proportion to ratio, which is the one split
                                  that gives every photograph on that line the
                                  SAME height: w = r*(H+k) => h = w/r = H+k
     padding-top: 100% / --r      the box's height, from its own width, before
                                  a single byte of the image has arrived — so
                                  the wall has its final shape on first paint
                                  and nothing shifts as 172 files land

   --r is written into the markup per photograph by scripts/genphotos.py, out
   of the real pixel dimensions in data/photos.json.

   THREE THINGS THAT LOOK LIKE STYLE AND ARE NOT
   ---------------------------------------------
   1. THE FRAME IS AN OVERLAY (.pc::after), NOT A BORDER AND NOT PADDING.
      Both of those change the box, and `padding-top:100%/--r` resolves
      against the CONTENT width — so a 2px border makes every tile's height
      (w-4)/r + 4, which is a different error for every different r. Measured
      on the real ratios that is up to 6px of mismatch, i.e. rows that no
      longer line up, which is the one thing this layout exists to do. An
      overlay costs nothing and is exact.

   2. THE LAST ROW IS ABSORBED (.wall::after). Without it the final row's two
      or three photographs grow to fill the whole page width and land three
      times the height of everything above them. The absorber is a zero-width
      flex item with an absurd grow factor: it eats the leftover space, so the
      last row stays at exactly --rowh. Every justified gallery does this.

   3. object-fit: cover, AND IT CROPS NOTHING. The box's ratio IS the picture's
      ratio, to four decimal places; cover is here only so that sub-pixel
      rounding shows a hairline of photograph rather than a hairline of ground.
      `contain` would do the opposite. Do not "fix" this to contain.
   ========================================================================== */

/* ---- the scrolling document ----------------------------------------------
   The same three undos as body.shop, for the same reason: page.css sets
   `main{display:flex;align-items:center}` and `.copy{position:fixed}`, which
   are right for a page that is one screen and wrong for a wall that scrolls.
   `body.photos main` is (0,2,1) and so beats both the base rule and the one
   inside page.css's phone block — a media query adds no specificity. */
body.photos{display:block;min-height:100vh;min-height:100dvh;background:var(--ground)}
body.photos main{display:block;flex:none;min-height:0;padding:0}
body.photos .copy{position:static;text-align:center;
  margin:clamp(20px,3.5vw,38px) 0 0;padding:0 20px clamp(18px,3vw,30px)}

/* There is no fixed photograph behind this page and no .bg element in its
   markup. A background image under 172 photographs is noise, and the ground
   colour is what makes the frames read as frames. */

/* ---- the head ------------------------------------------------------------ */

.phead{text-align:center;
  padding:clamp(16px,2.6vw,30px) clamp(16px,4vw,40px) clamp(12px,2vw,22px)}
.phead .lede{margin:max(14px,1.4vw) auto 0;max-width:min(60ch,92vw)}

/* ---- the wall ------------------------------------------------------------ */

.wall{
  /* the target row height, and the only lever on how dense the wall is */
  --rowh:112px;
  /* the seam of ground between two photographs. Small on purpose: the brief
     was "connect them", so the pieces touch and the frame does the dividing. */
  --seam:3px;
  --frame:rgba(243,236,227,.74);

  display:flex;flex-wrap:wrap;gap:var(--seam);
  /* full bleed. A wall that stops short of the edge is a picture of a wall. */
  padding:0;margin:0;list-style:none;
}

/* the last-row absorber; see note 2 in the header */
.wall::after{content:"";flex-grow:999999;flex-basis:0;min-width:0}

.pc{
  position:relative;display:block;overflow:hidden;min-width:0;
  flex:var(--r) 1 calc(var(--r) * var(--rowh));
  /* what is on the screen before the file lands, so the wall is never a field
     of white holes on the way down */
  background:#221d19;
  -webkit-tap-highlight-color:transparent;
}
.pc::before{content:"";display:block;padding-top:calc(100% / var(--r))}
.pc img{position:absolute;top:0;left:0;width:100%;height:100%;
  object-fit:cover;display:block;
  transition:transform .45s cubic-bezier(.2,.7,.3,1)}

/* the frame; see note 1 in the header. Two rings in one shadow: a light 2px
   at the very edge, and a dark hairline just inside it so the frame still
   reads on a photograph that is white where it meets the edge. */
.pc::after{content:"";position:absolute;inset:0;pointer-events:none;
  box-shadow:inset 0 0 0 2px var(--frame), inset 0 0 0 3px rgba(20,16,13,.55);
  transition:box-shadow .18s ease}

@media (hover:hover){
  .pc:hover img{transform:scale(1.045)}
  .pc:hover::after{box-shadow:inset 0 0 0 2px var(--accent),
                              inset 0 0 0 3px rgba(20,16,13,.55)}
}
.pc:focus-visible{outline:3px solid var(--accent);outline-offset:-3px;z-index:2}
.pc:focus-visible::after{box-shadow:inset 0 0 0 2px var(--accent),
                                    inset 0 0 0 3px rgba(20,16,13,.55)}

/* ---- the lightbox --------------------------------------------------------
   A plain element, not <dialog>: this tree's floor is Safari 14 and <dialog>
   arrives in 15.4.

   NO backdrop-filter ANYWHERE INSIDE. The panel fades in, and an element with
   an animated opacity becomes a BACKDROP ROOT — every backdrop-filter under it
   then renders nothing at all while getComputedStyle still cheerfully reports
   it as set. The buttons therefore use --glass-solid, which is a real colour.
   The ground behind them here is near-black and uniform, so a frosted surface
   would have bought nothing anyway. */

.lb{position:fixed;inset:0;z-index:30;display:flex;flex-direction:column;
  align-items:center;justify-content:center;gap:clamp(10px,1.6vw,18px);
  /* .965, not .95. At .95 the wall behind stayed legible enough to compete
     with the photograph in front of it, which is the one thing a lightbox is
     for. It cannot be helped with a blur: the panel animates its opacity, and
     that makes it a backdrop root where no backdrop-filter renders at all. */
  background:rgba(12,10,8,.965);padding:clamp(10px,2vw,26px);
  animation:lbin .18s ease both}
@keyframes lbin{from{opacity:0}to{opacity:1}}

.lbfig{margin:0;display:flex;align-items:center;justify-content:center;
  min-height:0;flex:1 1 auto}
/* The cap is in VIEWPORT units, not `max-height:100%`. A percentage max-height
   on a child of a flex item resolves against a height the flex algorithm has
   not settled yet, and Safari and Chrome disagree about what it means; the
   picture came out full-bleed under the bar in one of them. Viewport units are
   settled before layout starts. The subtrahend is the bar plus the padding. */
.lbfig img{max-width:100%;width:auto;height:auto;display:block;
  max-height:calc(100vh - 150px);max-height:calc(100dvh - 150px);
  object-fit:contain;
  box-shadow:0 0 0 2px var(--frame), 0 26px 70px rgba(0,0,0,.62)}

.lbbar{display:flex;align-items:center;justify-content:center;gap:12px;
  flex:0 0 auto;flex-wrap:wrap;text-align:center}
.lbcap{color:var(--ink);margin:0;max-width:min(60ch,92vw);
  font-size:calc(clamp(13px,.95vw,16px) * var(--a11y-scale));line-height:1.4}
.lbnum{color:var(--dim);margin:0;letter-spacing:.08em;
  font-size:calc(clamp(11px,.75vw,13px) * var(--a11y-scale))}

.lbbtn{appearance:none;border:0;cursor:pointer;
  width:48px;height:48px;border-radius:var(--btn-r);
  display:flex;align-items:center;justify-content:center;
  background:var(--glass-solid);color:var(--ink);
  box-shadow:inset 0 0 0 1px var(--glass-edge);
  -webkit-tap-highlight-color:transparent;
  transition:background .18s ease,box-shadow .18s ease}
.lbbtn:hover{background:var(--glass-on);box-shadow:inset 0 0 0 1px var(--glass-edge-hi)}
.lbbtn svg{width:20px;height:20px;fill:none;stroke:currentColor;
  stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.lbclose{position:absolute;top:clamp(10px,2vw,20px);
  inset-inline-end:clamp(10px,2vw,20px);z-index:2}
/* prev/next sit either side of the picture on a mouse screen and in the bar on
   a phone, where the gesture is a swipe and a 48px target beside a full-bleed
   photograph would be on top of it */
.lbnav{position:absolute;top:50%;transform:translateY(-50%);z-index:2}
.lbprev{inset-inline-start:clamp(8px,1.6vw,18px)}
.lbnext{inset-inline-end:clamp(8px,1.6vw,18px)}
/* the arrows point INWARD-relative, so they must flip with the document */
html[dir="rtl"] .lbnav svg{transform:scaleX(-1)}

body.lbopen{overflow:hidden}

/* ---- high contrast -------------------------------------------------------
   IS 5568 is a legal requirement, so every component here carries its own
   rule, doubled as (0,2,x) — page.css writes its contrast rules that way and a
   single-attribute rule would lose to them at the overlaps. */
html[data-a11y-contrast][data-a11y-contrast] .pc{background:#000}
html[data-a11y-contrast][data-a11y-contrast] .pc::after{
  box-shadow:inset 0 0 0 3px #fff}
html[data-a11y-contrast][data-a11y-contrast] .pc:hover::after,
html[data-a11y-contrast][data-a11y-contrast] .pc:focus-visible::after{
  box-shadow:inset 0 0 0 4px #fff}
html[data-a11y-contrast][data-a11y-contrast] .lb{background:#000}
html[data-a11y-contrast][data-a11y-contrast] .lbfig img{
  box-shadow:0 0 0 3px #fff}
html[data-a11y-contrast][data-a11y-contrast] .lbbtn{
  background:#000;color:#fff;box-shadow:inset 0 0 0 2px #fff}
html[data-a11y-contrast][data-a11y-contrast] .lbcap,
html[data-a11y-contrast][data-a11y-contrast] .lbnum{color:#fff}

/* ---- the phone, and this block stays LAST --------------------------------
   --rowh is the whole responsive story. 112px on a 390px screen puts two
   landscape photographs or four portraits on a line, which is a wall; 260px
   there would put one, which is a column. The steps below are widths, not
   devices — a narrow desktop window gets the narrow wall, correctly. */
@media (min-width:480px){ .wall{--rowh:140px} }
@media (min-width:768px){ .wall{--rowh:172px} }
@media (min-width:1024px){ .wall{--rowh:210px} }
@media (min-width:1440px){ .wall{--rowh:250px} }
@media (min-width:1920px){ .wall{--rowh:290px} }

@media (max-width:900px){
  /* The arrows move out of the picture's way and sit in the bar, where they
     are already the DOM's children -- position:absolute took them out of that
     flow on a wide screen, and dropping to static simply puts them back. The
     gesture here is a swipe; the buttons are for anyone who would rather tap. */
  .lbnav{position:static;transform:none}
  .lbbar{gap:10px}
  .lbfig img{max-height:calc(100vh - 190px);max-height:calc(100dvh - 190px)}
}
