/* Album browser — the church's Flickr, shelved by year.
   Visual direction: a dated shelf of Friday nights. The boldness is spent in
   exactly one place, the year rail; album cards themselves stay quiet so a
   phone screen full of them reads as a list, not a carnival.
   Tokens only — every colour, radius and easing comes from styles.css. */

.albums-page main { padding-bottom: 0; }

.album-search-wrap { padding-top: 16px; padding-bottom: 12px; }
.album-search-label {
  display: block; margin: 0 0 8px;
  color: var(--steel); font: 500 11px/1.3 var(--font-mono);
  letter-spacing: 0.16em; text-transform: uppercase;
}
.album-search {
  width: 100%; height: 48px; padding: 0 16px;
  border: 1px solid var(--control-line); border-radius: var(--radius-input);
  background: var(--control-bg); color: var(--ink); font: 16px/1.2 var(--font-body);
  box-shadow: var(--shadow-control); touch-action: manipulation;
}
.album-search:focus-visible { outline: 2px solid var(--gold-ring); outline-offset: 2px; }

/* ---------- shelf ---------- */

.year-rail {
  grid-column: 1 / -1;
  margin: 26px 0 2px;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 12px;
  letter-spacing: 0.16em;
  color: var(--gold-text);
  display: flex;
  align-items: center;
  gap: 12px;
}
.year-rail::after {
  content: "";
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, rgba(201, 154, 46, 0.55), rgba(201, 154, 46, 0));
}
.year-rail:first-child { margin-top: 0; }

.album-shelf {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
}
/* `display: grid` above outranks the browser's own [hidden]{display:none},
   so albums.js setting shelf.hidden = true did NOTHING: opening an album left
   all 57 cards — 10,433px of them — stacked above the photos you asked for,
   and every album opened onto the shelf you had just left. Found 2026-07-29
   by measuring the page after a click rather than trusting the hidden flag.
   Same trap already guarded on .shot-sentinel and the shotbox parts below;
   the shelf itself was missed. */
.album-shelf[hidden] { display: none; }

.album-card {
  display: block;
  text-decoration: none;
  color: inherit;
  border-radius: var(--radius-card);
  overflow: hidden;
  background: var(--oak-100);
  border: 1px solid var(--line);
  transition: transform var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
  /* 263 ungoverned cards made every un-hide of the shelf lay out and paint a
     32,590px box before one frame could show — ~1s frozen on a phone
     (measured 2026-09-05). `auto` keeps each card's real rendered size once
     seen; 223px is the measured card height until then. */
  content-visibility: auto;
  contain-intrinsic-size: auto 223px;
}
.album-card:active { transform: scale(0.985); }
@media (hover: hover) {
  .album-card:hover { border-color: rgba(201, 154, 46, 0.45); }
  .album-card:hover .album-shot img { transform: scale(1.04); }
}
.album-card:focus-visible {
  outline: 2px solid var(--gold-ring);
  outline-offset: 3px;
}

/* NOTHING WAS OCCUPYING THE RESERVED BOX (2026-09-05). The box is reserved
   correctly — every cover carries width=4 height=3 and an aspect-ratio, so a
   late image shifts nothing — but until it decodes the card held a flat slab of
   cream with no more information in it than a hole would have. On a phone that
   is seconds of it: 19 of 275 covers had decoded 2.5s after networkidle, and
   three seconds after a jump to 6,000px six of the ten covers on screen were
   still empty (measured 2026-09-05).

   CSS ONLY, AND NOT ONE NEW REQUEST. No blurhash, no dominant-colour byte from
   the API, no second image: two background layers on the box that is already
   there. The under-layer is the oak gradient the wall is made of, so the tile
   reads as a piece of the site's own material rather than as a gap in it; the
   over-layer is one soft highlight that sweeps across it, which is what says
   "coming" rather than "empty". --oak-100 to --oak-300 is one step either side
   of the --oak-200 this box used to be a flat sheet of, so the WALL's overall
   tone is unchanged at any distance.

   IT ENDS BY ITSELF, TWICE OVER. The <img> is width/height 100% with
   object-fit: cover, so the moment it decodes it paints over both layers and
   the box is the photograph — nothing has to be told that it arrived, which is
   the whole reason this can be CSS with no load hook in albums.js. And the
   sweep is a COUNTED animation, not an infinite one: a cover that has not
   arrived in ~8s is not arriving in the next frame either, and a wall of tiles
   shimmering forever behind photographs that already landed is main-thread
   paint nobody asked for. After the count it rests on the static gradient.

   background-color, not the `background` shorthand: the shorthand resets
   background-image, and the two layers are the point. */
.album-shot {
  aspect-ratio: 4 / 3;
  /* lighter than the page, so a cover that is still loading reads as a
     placeholder slab instead of a black hole in the card */
  background-color: var(--oak-200);
  background-image:
    linear-gradient(100deg,
      rgba(255, 255, 255, 0) 42%,
      rgba(255, 255, 255, 0.42) 50%,
      rgba(255, 255, 255, 0) 58%),
    linear-gradient(160deg, var(--oak-100), var(--oak-300));
  background-repeat: no-repeat;
  background-size: 240% 100%, 100% 100%;
  background-position: -70% 0, 0 0;
  animation: album-shot-wait 1400ms linear 6;
  overflow: hidden;
}
@keyframes album-shot-wait {
  from { background-position: -70% 0, 0 0; }
  to   { background-position: 170% 0, 0 0; }
}
.album-shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform var(--t-med) var(--ease-glide);
}

.album-body { padding: 10px 12px 13px; }
.album-body h3 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 15px;
  line-height: 1.25;
  letter-spacing: -0.01em;
}
/* NO GLYPH MAY OPEN A LINE (audit 2026-08-12, wave C). This was one text node
   joined with " · ", so it broke wherever it ran out of room: at 320 it wrapped
   as "April 24, 2026" / "· 60 photos" — a separator opening the second line —
   and at 390 and 768 "photos" was orphaned onto a line of its own. It is now a
   wrapping row of nowrap atoms, one <span> per bit, written by setMeta() in
   albums.js (card + album head), church-photos.js (card + album head) and
   rewind.js. The GAP is the separator now: it reads as one when the atoms sit
   on one line and simply disappears when they stack, which is the whole point
   — a glyph cannot do that. Any writer left setting textContent would land a
   bare text node here as one anonymous flex item and wrap the old way.
   16px, not the 9px this was first drawn at: a character of this mono at 11px
   with 0.04em of tracking is ~7px wide, so a 9px gap was two pixels more than
   a word space — "August 7, 2026 21 photos" read as one run at 1280. 16px is
   a hair over two characters, which separates the atoms as plainly as the
   middot did. Measured on shot albums-cards-1280.png, 2026-08-12. */
.album-meta {
  margin: 5px 0 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 16px;
  row-gap: 0;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--steel);
}
.album-meta > span { white-space: nowrap; }

/* ---------- one album ---------- */

#album-view:focus { outline: none; }

.album-head { margin-bottom: 18px; }
.album-back {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.08em;
  color: var(--gold-text);
  text-decoration: none;
}
.album-back:focus-visible { outline: 2px solid var(--gold-ring); outline-offset: 3px; }
.album-head h2 {
  margin: 4px 0 6px;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(24px, 1.5rem - 24px + 6vi, 38px);
  line-height: 1.08;
  letter-spacing: -0.02em;
}
.album-head .album-meta { font-size: 12px; }
.album-partial {
  margin: 10px 0 0;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--steel);
}

/* One quiet line of context under the title — see renderRecap in albums.js
   for what is allowed in it. Deliberately smaller and dimmer than the meta
   line above: it is the footnote, not the identity of the album. */
.album-recap {
  margin: 6px 0 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--steel);
}

/* Slideshow and Share, side by side. They wrap onto a second line on a
   narrow phone rather than shrinking below the 44px target. */
.album-acts { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.album-share .arr { font-size: 12px; }
/* The download fallback and the one-line status the share path can leave
   behind. Both are display-setting rules, so both carry the [hidden] guard
   the rest of this file learned the hard way. */
.album-share-dl {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin-top: 8px;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.06em;
  color: var(--gold-text);
  text-decoration: underline;
  text-underline-offset: 4px;
  overflow-wrap: anywhere;
}
.album-share-dl[hidden] { display: none; }
.album-share-note {
  margin: 8px 0 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--steel);
}
.album-share-note[hidden] { display: none; }

/* One bounded share confirmation. It stays below the photo viewer's z-layer,
   clears the mobile dock/safe area and never accepts pointer input. */
.album-toast {
  position: fixed;
  left: 50%;
  /* 88px + the safe area was a clearance for a bottom dock, and this site has
     no dock on /albums/ (2026-08-23): the pill landed a third of the way up the
     photo grid, dead centre over two photographs, with nothing under it to
     clear. A confirmation belongs at the bottom edge, out of the picture. */
  bottom: max(18px, env(safe-area-inset-bottom, 0px));
  z-index: 55;
  max-width: calc(100% - 32px);
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 10px 18px;
  border: 1px solid rgba(236, 195, 92, 0.48);
  border-radius: 999px;
  background: var(--ink);
  box-shadow: var(--shadow-control-lift);
  color: var(--paper);
  font-size: 14px;
  font-weight: 600;
  line-height: 1.35;
  pointer-events: none;
  transform: translate3d(-50%, 0, 0);
}
.album-toast[hidden] { display: none; }
.album-toast.is-showing { animation: album-toast-in 200ms var(--ease-glide) both; }
.album-toast.is-leaving { opacity: 0; transform: translate3d(-50%, 8px, 0); transition: opacity 160ms var(--ease-glide), transform 160ms var(--ease-glide); }
@keyframes album-toast-in {
  from { opacity: 0; transform: translate3d(-50%, 8px, 0) scale(0.98); }
}

/* Three columns on a 390px phone gave 111px tiles — a wall of chips you
   cannot recognise anyone in, which is the whole reason to open an album
   (the owner, 2026-07-29). Two columns is ~172px and reads. Desktop counts are
   deliberately unchanged: the album thumb is Flickr's `c` size (800 long →
   533 on the short edge of a 3:2 frame), and widening the desktop tiles too
   would push them past that budget. Phone spec: 2-col at 390 / 4 from 620 /
   5 from 980 — ADAPTED. */
.shot-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}
/* zero-height tripwire that pulls the next block of tiles in (albums.js).
   overflow is the belt for the /people/ fault of 2026-08-01: a block of sixty
   tiles appended INTO this 1px box painted straight over the report form under
   it. The cause is fixed in people.js; this makes the failure invisible rather
   than catastrophic if it ever comes back. */
.shot-sentinel { height: 1px; overflow: hidden; }
.shot-sentinel[hidden] { display: none; }
.shot-tile {
  display: block;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border-radius: 8px;
  background: var(--oak-200);  /* loading reads as loading, not a black square */
}
/* The photo tiles are the shelf's and /people/'s densest tap target and had
   focus-visible but no press state — the photograph dips inside its own
   rounded box, so the grid rhythm never moves. (2026-08-13) */
.shot-tile img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform var(--press-up) var(--ease-press);
}
/* True shape (Daniel 2026-09-20: "they're presented in a square ... the images
   are either portrait or landscape. They're never square"). The album grid lays
   its tiles in justified rows: each tile keeps its photograph's own ratio
   (--r = width / height, set in albums.js), every tile in a row shares one
   height, and nothing is cropped. The ::after soaks up a short last row so it
   is not stretched; the width cap does the same for a lone tile before a day
   divider. /people/ and the church grids keep the square .shot-grid. */
.shot-grid.is-true-shape { display: flex; flex-wrap: wrap; --row-h: 150px; }
.shot-grid.is-true-shape::after { content: ''; flex: 1000000 1 0; }
.shot-grid.is-true-shape .shot-tile {
  --r: 1.5;
  flex: var(--r) 1 calc(var(--r) * var(--row-h));
  max-width: calc(var(--r) * var(--row-h) * 2.2);
  aspect-ratio: var(--r);
}
.shot-grid.is-true-shape .day-rail { flex: 0 0 100%; }
@media (min-width: 620px) { .shot-grid.is-true-shape { --row-h: 170px; } }
@media (min-width: 980px) { .shot-grid.is-true-shape { --row-h: 210px; } }
.shot-tile:active img { transition-duration: var(--press-down); transform: scale(1.04); }
.shot-tile:focus-visible { outline: 2px solid var(--gold-ring); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .shot-tile img { transition: none; }
  .shot-tile:active img { transform: none; }
}
/* A video in the set shows as its poster still with a play badge; the tap
   follows the tile's real Flickr link, where the video actually plays.
   Same badge at both viewports — 44px is already a full tap target. */
.shot-video { position: relative; }
.shot-video::before {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: rgba(11, 11, 12, 0.62);
  border: 1px solid rgba(255, 255, 255, 0.35);
}
.shot-video::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 0;
  height: 0;
  border-left: 14px solid #fff;
  border-top: 9px solid transparent;
  border-bottom: 9px solid transparent;
  transform: translateX(3px);
}

/* ---------- lightbox ---------- */

.shotbox {
  position: fixed;
  inset: 0;
  z-index: 60;
  /* OPAQUE, not 94% (2026-08-09 audit). Six percent of the wall behind it was
     coming through, so a photograph on this screen sat on a faint ghost of the
     grid it came out of — album titles and tile edges readable through the
     black. A viewer is the one screen on the site that is only the picture. */
  background: #060608;
  display: flex;
  flex-direction: column;
  padding: max(12px, env(safe-area-inset-top))
           max(12px, env(safe-area-inset-right))
           max(12px, env(safe-area-inset-bottom))
           max(12px, env(safe-area-inset-left));
}
.shotbox[hidden] { display: none; }
/* THE CHROME BAND (2026-08-23). One row, above the stage, holding the ✕ and
   nothing else — so the picture below it is chrome-free on a phone. It is a
   flow row rather than fixed/absolute chrome on purpose: fixed chrome is what
   ends up over the photograph, which is the thing Daniel reported. */
.shotbox-top {
  flex: 0 0 auto;
  display: flex;
  justify-content: flex-end;
  align-items: center;
  min-height: 44px;
  margin-bottom: 6px;
}
/* The stage is now only the picture. The arrows still live inside it because
   at desktop widths the contain-fit leaves a dark band at each edge for them
   to stand in; on a phone they are gone entirely (see .shotbox-edge). */
.shotbox-stage {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* The rail pans by a whole stage width either way, and the neighbouring
   photographs must not show beside the open one — so the album viewer's stage
   clips. Scoped by .has-rail because the people viewers share this file and
   have no rail to clip. */
.shotbox-stage.has-rail { overflow: hidden; }
/* The swipe-down-to-close flies the photograph out through the BOTTOM of the
   stage, and that gesture is untouched (it was already good). So the clip is
   lifted for exactly as long as that drop runs. */
.shotbox-stage.has-rail.is-dropping { overflow: visible; }
.shotbox-stage:focus { outline: none; }
/* THE RAIL (2026-08-23). Three panes wide, one stage width apart: the previous
   photograph sits at -100%, the open one in the middle, the next at +100%.
   A horizontal drag moves THIS element 1:1 with the finger and the completing
   slide is a transform on it, so the picture that arrives was already painted
   — nothing swaps a src while anybody is looking. The vertical swipe-down
   still transforms .shotbox-track inside it, untouched. */
.shotbox-rail {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  will-change: transform;
  transform: translate3d(0, 0, 0);
}
.shotbox-pane {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  /* Hidden until a sideways drag is actually running. The stage clip is lifted
     during the downward close, and a pane sitting exactly one stage width away
     would otherwise show a sliver of the next photograph in the viewer's own
     12px gutter. visibility, not display: the browser still downloads and
     decodes the neighbours, which is the whole reason they exist. */
  visibility: hidden;
}
.shotbox-rail.is-panning .shotbox-pane { visibility: visible; }
.shotbox-pane.prev { left: -100%; }
.shotbox-pane.next { left: 100%; }
.shotbox-pane img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: var(--radius);
}
/* nothing to show either end of the album — the rail rubber-bands instead */
.shotbox-pane.is-empty { visibility: hidden; }
.shotbox-track {
  max-width: 100%;
  max-height: 100%;
  display: flex;
  /* align-items stays at its default `stretch` ON PURPOSE, and this is not
     obvious. Stretch is what gives the <img> a height-bounded box for
     max-height:100% to resolve against; centring it instead lets a portrait
     photo size to 1406x2109 and overflow the stage (tried 2026-07-29).
     The cost is that the img BOX (1406x752) is much wider than the photo
     painted inside it (501) — so anything asking "how big is this photo on
     screen" must compute the contain-fit, not read the box. albums.js
     upgradeIfShort() and photo-size-check.js both do. */
  will-change: transform;
}
.shotbox-stage img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: var(--radius);
}
/* the [hidden] trap again: styles.css's `img, video { display: block }` beats
   the browser's own [hidden] { display: none }, so albums.js hiding the photo
   for a video frame did NOTHING — the player was pushed off the bottom of the
   stage and the photo you had just left stayed on screen (audit 2026-08-01).
   Four siblings in this file already carry this guard; the picture did not. */
.shotbox-stage img[hidden] { display: none; }
/* the full-size file is still arriving — an honest hairline, not a fake blur
   over a thumbnail that is already nearly native resolution on a phone */
.shotbox-stage::after {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  height: 2px;
  width: 100%;
  transform: scaleX(0);
  transform-origin: 0 50%;
  background: var(--accent-glow);
  opacity: 0;
  pointer-events: none;
}
.shotbox-stage.is-loading::after {
  opacity: 0.85;
  animation: shot-wait 1.1s ease-in-out infinite;
}
@keyframes shot-wait {
  0%   { transform: scaleX(0); transform-origin: 0 50%; }
  50%  { transform: scaleX(1); transform-origin: 0 50%; }
  100% { transform: scaleX(0); transform-origin: 100% 50%; }
}

/* Whole side of the screen, not a pill to aim at. Kept off the middle third
   so a tap meant for the photo is never stolen.
   /people/ and /church-photos/people/ still use these as they are — their
   viewers are built by people.js and are not this file's to change. The
   albums viewer takes them off the phone: see the .shotbox-rail-scoped rule
   below. */
.shotbox-edge {
  position: absolute;
  top: 50%;
  bottom: auto;
  width: 56px;
  height: 56px;
  transform: translateY(-50%);
  min-width: 56px;
  border: 0;
  background: transparent;
  color: var(--paper);
  font-size: 22px;
  cursor: pointer;
  display: flex;
  align-items: center;
  opacity: 0.55;
  -webkit-tap-highlight-color: transparent;
}
.shotbox-edge.prev { left: 0; justify-content: flex-start; padding-left: 6px; }
.shotbox-edge.next { right: 0; justify-content: flex-end; padding-right: 6px; }
.shotbox-edge span {
  width: 44px;
  height: 44px;
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: rgba(10, 10, 12, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.16);
}
.shotbox-edge:focus-visible { outline: 2px solid var(--accent-glow); outline-offset: -4px; opacity: 1; }
/* GONE ON PHONES in the album viewer (2026-08-23, Daniel: "those side to side
   buttons aren't needed"). Two 56px circles stood at mid-height over the
   picture, which is both an obstruction and a target nobody aims for once the
   swipe works. They survive only where there is a pointer to hover and room to
   stand outside the photograph. The keyboard arrows are untouched either way:
   they are on document keydown, never on these buttons. Scoped by the rail so
   the people viewers, which have no rail, keep the arrows they had. */
.shotbox-rail ~ .shotbox-edge { display: none; }
@media (hover: hover) and (min-width: 720px) {
  .shotbox-rail ~ .shotbox-edge { display: flex; }
}
/* The way out that needs no gesture and no keyboard. 46px so the target is
   comfortably over the 44px floor on a phone, and dark enough over a bright
   photo to be found without hunting for it — this is the control someone
   reaches for when the swipe isn't obvious to them. The people viewers still
   place it on the photograph's corner; in the album viewer it has moved into
   the chrome band (below), which is why the absolute placement stays here
   rather than moving wholesale. */
.shotbox-x {
  position: absolute;
  top: 0;
  right: 0;
  width: 46px;
  height: 46px;
  padding: 0;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.28);
  background: rgba(10, 10, 12, 0.72);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  color: var(--paper);
  font-size: 24px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 2;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: background .15s cubic-bezier(.4, 0, .2, 1),
              border-color .15s cubic-bezier(.4, 0, .2, 1),
              transform .15s cubic-bezier(.4, 0, .2, 1);
}
.shotbox-x:active { transform: scale(.94); }
.shotbox-x:focus-visible { outline: 2px solid var(--accent-glow); outline-offset: 2px; }
/* In the album viewer the ✕ is a flow item in the band above the picture, so
   nothing of ours is over the photograph at all (2026-08-23). */
.shotbox-top .shotbox-x {
  position: relative;
  top: auto;
  right: auto;
  flex: 0 0 auto;
}

/* The slideshow is GONE (2026-08-23, Daniel: "Remove the play button
   slideshow"). What stood here was .shotbox-play — the disc in the stage's
   top-left corner, mirroring the ✕ across the top of the photograph — and
   .shotbox-tick, its countdown hairline. Both are removed along with the code
   path they reached in albums.js. .album-show below survives because the
   Share button on the album header wears the same pill.  */

/* The way IN, on the album's own header. A quiet brass ghost pill rather than
   a filled button: the album's photographs are the thing on this page, and an
   invitation to watch them should not shout over them. */
.album-show {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  margin-top: 12px;
  padding: 0 18px;
  border-radius: 999px;
  /* brass is the right voice for "play the album" — but 42% of the gold on an
     oak band measures under the non-text floor, so the ring is the solid
     light-canvas gold and the fill is a real tint, not a suggestion */
  border: 1px solid var(--gold-ring);
  background: rgba(201, 154, 46, 0.14);
  box-shadow: var(--shadow-control);
  color: var(--gold-text);
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: background .18s cubic-bezier(.4, 0, .2, 1),
              border-color .18s cubic-bezier(.4, 0, .2, 1),
              transform .18s cubic-bezier(.4, 0, .2, 1);
}
.album-show[hidden] { display: none; }
.album-show .arr { font-size: 10px; line-height: 1; }
.album-show:active { transition-duration: var(--press-down); transform: scale(0.97); }
.album-show:focus-visible { outline: 2px solid var(--gold-ring); outline-offset: 3px; }
@media (hover: hover) {
  .album-show:hover { background: rgba(201, 154, 46, 0.24); border-color: var(--accent-deep); box-shadow: var(--shadow-control-lift); }
}
/* Asked for less movement: nothing on this pill sweeps, slides or pulses. */
@media (prefers-reduced-motion: reduce) {
  .album-show:active { transform: none; }
}

.shotbox-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding-top: 10px;
  flex-wrap: wrap;
}
/* On a phone "September 19, 2025 · 7:42 PM" plus a counter plus a link do not
   fit on one 390px line, and the date is the part that was missing — so it
   takes its own row rather than being cut to "September 19, 20…". */
@media (max-width: 519px) {
  .shotbox-when { flex: 1 0 100%; order: -1; }
  /* THE OWNER'S PILL SITS IN THE ACTION ROW (owner's order, 2026-08-23:
     "make it smaller and inline with Share and Download"). It used to take a
     row of its own — `flex: 1 0 100%` — which made a 366px slab under the two
     real pills and read as the biggest thing in the bar. It is now a pill of
     its own width like its two siblings; what it costs is the thing the old
     rule was buying, that "Make cover" and "Cover ✓" are different widths, so
     the bar may re-flow by one pill-width as he swipes past the photograph
     that is already the cover. He chose the trade.
     The three pills only fit one 366px line together with less air inside
     them, so the whole action row goes compact — all three, never one, or the
     owner's tool would read as the odd shape it is not. 44px of height is
     untouched: this narrows the pills, it does not shrink the target.
     SCOPED WITH :has() TO THE BAR THAT ACTUALLY HAS THE PILL. albums.js
     appends it for the owner and nobody else, so every other reader — and the
     church-photos viewer, which shares this bar and never mounts one — keeps
     the bar it has today, byte for byte. Where :has() is missing the pills
     stay wide and the cover simply wraps at its own width, which is still not
     the full-width slab this replaced. */
  .shotbox-bar:has(.shotbox-cover) { gap: 8px; }
  .shotbox-bar:has(.shotbox-cover) .shotbox-full { padding: 0 13px; }
}
/* the date this photo was taken — the album header can only name one day, and
   a camp runs across three */
.shotbox-when {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--paper);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.shotbox-when[hidden] { display: none; }
.shotbox-count {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.06em;
  color: rgba(255, 255, 255, 0.72);
  flex: none;
}
/* .shotbox-link is GONE (2026-08-01). The per-photo "View on Flickr" link was
   removed from the viewer at the owner's ask on 2026-07-30; the element left the
   markup then, and this rule plus albums.js's lookup for it were the plumbing
   that stayed behind pointing at nothing. */

/* The full-size link. Since 2026-08-01 it opens the church's OWN copy off the
   mirror (the 2048px one, ~1 MB), not Flickr's original — but it is still the
   heavy option next to the picture already on screen, so it stays quieter than
   the rest of the bar: available rather than inviting, with the real pixel
   size always in its label. */
/* ── the Download control ─────────────────────────────────────────────────
   The owner, real iPhone, 2026-08-09: "nearly invisible". It was 12px mono at 62%
   white on a near-black bar, underlined only on hover — which on a phone means
   never. The one thing in this viewer that saves a photograph to somebody's
   camera roll looked like a caption.
   It is a button now: the house pill, a real border, accent-tinted so it reads
   as the action in the bar without shouting over the photograph, and 44px of
   target. Same treatment on the Whole Church viewer's "Open full size", which
   is the same control wearing a different verb. */
.shotbox-full,
.church-lightbox-bar a {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--accent-glow);
  text-decoration: none;
  min-height: 44px;
  padding: 0 18px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  flex: none;
  border: 1px solid rgba(236, 195, 92, 0.45);
  border-radius: 999px;
  background: rgba(236, 195, 92, 0.1);
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease),
              transform var(--press-up) var(--ease-press);
}
/* Two changes on the press, never one, and no hover-only affordance. */
.shotbox-full:active,
.church-lightbox-bar a:active {
  transition-duration: var(--press-down);
  transform: scale(0.96);
  background: rgba(236, 195, 92, 0.18);
}
@media (hover: hover) {
  .shotbox-full:hover,
  .church-lightbox-bar a:hover {
    background: rgba(236, 195, 92, 0.18);
    border-color: rgba(236, 195, 92, 0.75);
  }
}
.shotbox-full:focus-visible,
.church-lightbox-bar a:focus-visible {
  outline: 2px solid var(--accent-glow);
  outline-offset: 3px;
}
/* Reserved, not collapsed. Stepping from a photo onto a video hides this
   link, the bar lost a row and the picture above it grew by 77px mid-swipe
   (audit 2026-08-01). visibility keeps the space AND keeps the link out of the
   accessibility tree, which display:none was the only thing doing before. */
.shotbox-full[hidden] { visibility: hidden; }

/* THE OWNER'S COVER PILL (2026-08-23). Built by albums.js only after the server
   has said this is his account, so it is absent — not hidden — for everybody
   else and reserves nothing. It borrows .shotbox-full's whole geometry (44px,
   the pill radius, the two-change press) and drops only the amber: Download is
   what the church came for, this is a tool that happens to be in the same bar,
   and a third gold shape would make the bar read as three equal offers. Once
   it IS the cover the pill stops being a button and becomes a statement — same
   place, no press, no target. */
.shotbox-cover {
  color: rgba(255, 255, 255, 0.78);
  border-color: rgba(255, 255, 255, 0.22);
  background: rgba(255, 255, 255, 0.06);
}
.shotbox-cover:active {
  background: rgba(255, 255, 255, 0.12);
}
@media (hover: hover) {
  .shotbox-cover:hover {
    background: rgba(255, 255, 255, 0.12);
    border-color: rgba(255, 255, 255, 0.42);
  }
}
.shotbox-cover:focus-visible { outline-color: rgba(255, 255, 255, 0.9); }
.shotbox-cover[disabled] {
  color: var(--accent-glow);
  border-color: rgba(236, 195, 92, 0.35);
  background: rgba(236, 195, 92, 0.08);
  transform: none;
}
@media (hover: hover) {
  .shotbox-cover[disabled]:hover {
    background: rgba(236, 195, 92, 0.08);
    border-color: rgba(236, 195, 92, 0.35);
  }
}

/* Filmstrip: where you are in the album, and a way to cross it. Windowed in
   albums.js — a 573-photo camp never builds 573 cells. */
.shotbox-strip {
  position: relative;
  display: flex;
  gap: 6px;
  overflow-x: auto;
  overflow-y: hidden;
  padding: 8px 0 2px;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  scroll-snap-type: inline mandatory;
  scroll-padding-inline: 8px;
}
.shotbox-strip::-webkit-scrollbar { display: none; }
.shotbox-strip[hidden] { display: none; }
.strip-cell {
  position: relative;
  z-index: 1;
  flex: 0 0 auto;
  width: 52px;
  height: 52px;
  padding: 0;
  border: 0;
  border-radius: 8px;
  overflow: hidden;
  background: var(--charcoal);
  opacity: 0.5;
  cursor: pointer;
  scroll-snap-align: start;
  /* two changes on the active cell, not one: it brightens AND gains a ring */
  box-shadow: inset 0 0 0 0 var(--accent-glow);
  transition: opacity .18s ease, box-shadow .18s ease;
}
.strip-cell img { width: 100%; height: 100%; object-fit: cover; display: block; }
.strip-cell[aria-selected="true"] {
  opacity: 1;
  box-shadow: inset 0 0 0 1px rgba(236, 195, 92, 0.45);
}
.strip-cell:focus-visible { outline: 2px solid var(--accent-glow); outline-offset: 2px; }
.strip-indicator {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 2;
  width: var(--strip-indicator-w, 52px);
  height: var(--strip-indicator-h, 52px);
  border: 2px solid var(--accent-glow);
  border-radius: 8px;
  pointer-events: none;
  transform: translate3d(var(--strip-indicator-x, 0), var(--strip-indicator-y, 0), 0);
  transition: transform 180ms var(--ease-glide), width 180ms var(--ease-glide);
}

/* ---------- who is in this photograph ----------
   A short row of face crops under the frame, each one a link into /people/.
   Built by albums.js on every paint(); it sits INSIDE #shotbox so nav-swap's
   closeOverlays (which already hides #shotbox) needed no change.
   Deliberately quieter and smaller than the filmstrip above it: the strip is
   how you cross the album, this is an aside about the photograph you are on. */
/* TWO ELEMENTS, NOT ONE, AND THAT IS THE WHOLE POINT (fixed 2026-08-09).
   .shotbox-faces is the block; .shotbox-faces-row inside it is the sideways
   scroller that holds the crops. They were the same element until this
   morning, and when the hint line was added to the strip on 2026-08-09
   (95e3ce002) it went in as a flex item of the scroller at `flex: 1 0 100%` —
   full width, no shrink, in a nowrap row. So the hint filled the visible box
   and pushed EVERY face circle past its right edge: measured on an iPhone at
   390px, container 366px wide, first crop at x=386, scrollWidth 522. The
   circles were all still there, all still links, all still off-screen, which
   is why the DOM checks that shipped it passed — and why the owner opened a
   church photograph and reported no faces at all.
   Anything laid out sideways in this strip goes in the ROW. */
.shotbox-faces { display: block; }
.shotbox-faces-row {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}
.shotbox-faces-row::-webkit-scrollbar { display: none; }
/* Reserved, not collapsed — the same rule as .shotbox-full above. The faces
   are a network round trip behind the photograph, and the stage is `flex: 1`,
   so the picture visibly jumped 26px the moment the row landed under it
   (audit 2026-08-01). The height is held from the start; only what is IN the
   row changes. visibility also keeps an empty row out of the accessibility
   tree, which is what the old display:none was for.
   52 became 74 when the hint line moved above the crops instead of through
   them: 8px of padding, the hint's own line, and the 44px crop under it. */
/* The strip is the last thing before the bottom of the screen, and on an
   iPhone the bottom ~30px belong to Safari — a tap there summons the toolbar
   instead of opening the face (the owner, 2026-08-09: "Not clickable faces").
   The bottom padding lifts every crop clear of that contested band, plus the
   home-indicator inset where there is one (viewport-fit=cover is already on
   every page). The min-heights move in lockstep so the reserved empty strip
   and the filled strip stay the same height — the no-jump rule above. */
.shotbox-faces { padding: 6px 0 calc(14px + env(safe-area-inset-bottom, 0px)); min-height: calc(86px + env(safe-area-inset-bottom, 0px)); }
.shotbox-faces[hidden] { visibility: hidden; }
/* The person page's own viewer carries no hint line — the page you are
   standing on has already said what a face circle does — so it reserves the
   crops and nothing more. */
.people-page .shotbox-faces { min-height: calc(64px + env(safe-area-inset-bottom, 0px)); }
/* A FACE YOU CAN OPEN LOOKS LIKE ONE (design review 2026-08-08). Every crop in
   this row used to sit at 0.72 with a hairline ring and the plain ones at 0.42
   with a fainter hairline — a difference you had to go looking for, on the row
   whose whole job is to say which of these faces is a door. The link state is
   now the loud one: full opacity and a real ring. */
.shotbox-face {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;          /* a full tap target at the smallest size it can be */
  border-radius: 50%;
  overflow: hidden;
  background: var(--charcoal);
  opacity: 1;
  box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.62);
  transition: transform .18s ease, box-shadow .18s ease;
}
.shotbox-face img { width: 100%; height: 100%; object-fit: cover; display: block; }
.shotbox-face.has-name {
  width: auto;
  max-width: 150px;
  padding-right: 10px;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  border-radius: 999px;
}
.shotbox-face.has-name img { flex: 0 0 44px; width: 44px; height: 44px; border-radius: 50%; }
.shotbox-face-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--font-body);
  font-size: 11px; font-weight: 600;
  color: rgba(255, 255, 255, 0.88);
}
/* two changes on the active state, never one — the ring turns brass AND the
   crop lifts, because full opacity is now the resting state and can no longer
   be the thing that changes. The pointer half is hover-gated so a phone never
   keeps a lit ring on the last face it was tapped through. */
.shotbox-face:focus-visible {
  box-shadow: inset 0 0 0 2px var(--accent-glow);
  transform: scale(1.06);
}
@media (hover: hover) {
  .shotbox-face:hover {
    box-shadow: inset 0 0 0 2px var(--accent-glow);
    transform: scale(1.06);
  }
}
.shotbox-face:focus-visible { outline: 2px solid var(--accent-glow); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .shotbox-face { transition: box-shadow .18s ease; }
  .shotbox-face:hover, .shotbox-face:focus-visible { transform: none; }
}
/* The line that says what the circles are for (2026-08-09). A block, above the
   scroller and OUTSIDE it — see the note on .shotbox-faces-row: as a flex item
   of the row this same line hid every crop on the site. */
.shotbox-faces-hint {
  margin: 0 0 6px;
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.4;
  letter-spacing: 0.06em;
  color: rgba(255, 255, 255, 0.55);
}
/* The signed-out answer in the strip's place: the hint's quiet mono voice,
   but a real link — underlined so it reads as one, padded so the tap target
   clears 44px even though the line itself stays small. */
.shotbox-faces-signin {
  display: inline-block;
  padding: 15px 0;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.4;
  letter-spacing: 0.06em;
  color: rgba(255, 255, 255, 0.7);
  text-decoration: underline;
  text-underline-offset: 3px;
}
/* A person the archive has barely met. Since 2026-08-09 every circle is a
   link — the thin person's page exists and says there is not much yet — so
   this only sets how the circle READS: quiet, not dead. 0.42 read as
   "broken/blurred" on a phone (the owner, 2026-08-09), hence 0.6, and the circle
   carries a title/aria-label saying why it is dimmer. */
.shotbox-face.is-plain {
  opacity: 0.6;
  box-shadow: none;
}
/* The remainder at the end of the row, and now a BUTTON (the owner, 2026-08-10:
   he tapped it and nothing happened). Tapping it swaps the pill for the rest
   of the crops in place; the row already scrolls sideways, so thirty faces
   just make a longer row. */
.shotbox-face-more {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 44px; height: 44px; padding: 0 12px;
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.05);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14);
  color: rgba(255, 255, 255, 0.66);
  font-family: var(--font-mono);
  font-size: 12px; font-weight: 700; letter-spacing: 0.04em;
  cursor: pointer;
}
.shotbox-face-more:active { transform: scale(0.95); }
@media (min-width: 620px) {
  .shotbox-face { width: 48px; height: 48px; }
  .shotbox-face.has-name { width: auto; }
  .shotbox-face.has-name img { flex-basis: 48px; width: 48px; height: 48px; }
  .shotbox-face-more { height: 48px; }
  .shotbox-faces { min-height: calc(90px + env(safe-area-inset-bottom, 0px)); }
}

/* ---------- who is in this ALBUM ----------
   The same circle as the viewer strip above (.shotbox-face, same markup, same
   builder), one level up: under the album header, before the grid, so the
   circles exist for somebody who never opens a photograph — which is how the
   owner reads this page (2026-08-22, his sixth report of "no faces").
   Everything below is only what changes when that row leaves the dark viewer
   and lands on the cream page. The strip dresses itself in white — a 62%-white
   inset ring and a --charcoal backing — which on #f6f0e3 is a circle with no
   edge at all. The house control edge replaces it, exactly as the "Find
   yourself" row on the shelf wears it. Sizes are NOT changed: 44px here and
   48px past 620px is the strip's own scale, and the two rows are one design. */
.album-people {
  /* the block sits inside .container, so the gutter is already paid; only the
     gap to the grid under it belongs here */
  margin: 2px 0 20px;
}
.album-people[hidden] { display: none; }
/* The house mono label, same rank and same ink as .album-find-head on the
   shelf — this is the same kind of quiet heading over the same kind of row. */
.album-people-head {
  margin: 0 0 9px;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--steel);
}
/* No fixed height and nothing reserved: the row is REMOVED when there is
   nobody in it or nobody signed in, so there is no jump to protect against —
   and reserving space on a cream page for a row that may never come is the
   105px of empty cream the shelf's own note warns about. */
.album-people .shotbox-faces-row {
  align-items: center;
  padding-bottom: 2px;
}
.album-people .shotbox-face {
  background: var(--oak-100);
  /* --control-line composites to rgb(119,130,146) on this band: 3.4:1, past
     the WCAG 1.4.11 non-text floor, and the same navy every other tappable
     thing on a light band wears. 2px because a 1px ring disappears against the
     busiest third of a face crop. */
  border: 2px solid var(--control-line);
  box-shadow: var(--shadow-control);
  background-origin: border-box;   /* iOS hairline inside a 999px radius */
}
/* A person the archive has barely met — quiet, never dead: every circle is
   still a link (see faceLink), and it still says why on its own title. The RING
   is deliberately NOT weakened to carry that: it is the tap boundary of a
   44px control, so it has to hold the 3:1 non-text floor whatever the person's
   count is (--line is #e2d8c6 on this band — 1.24:1, a shape rather than an
   edge). The lift goes instead, and the crop dims a little: 0.82 rather than
   the viewer's 0.6, which read as "broken picture" on a phone the last time it
   was tried on a light band. */
.album-people .shotbox-face.is-plain {
  opacity: 0.82;
  box-shadow: none;
}
.album-people .shotbox-face.has-name { padding-right: 10px; }
.album-people .shotbox-face-name { color: var(--ink); }
.album-people .shotbox-face:focus-visible {
  outline: 2px solid var(--gold-ring);
  outline-offset: 3px;
  box-shadow: var(--shadow-control);
  transform: none;
}
@media (hover: hover) {
  .album-people .shotbox-face:hover {
    border-color: var(--control-line-strong);
    box-shadow: var(--shadow-control-lift);
  }
}
@media (prefers-reduced-motion: reduce) {
  .album-people .shotbox-face:hover { transform: none; }
}

/* Day markers inside an album that spans more than one day. */
.day-rail {
  grid-column: 1 / -1;
  margin: 20px 0 2px;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--gold-text);
}

/* With a mouse there is no swipe, so the arrows are the whole control — make
   them answer the pointer. Gated on hover so a phone never inherits a state
   it cannot reach. */
@media (hover: hover) {
  .shotbox-edge { opacity: 0.35; transition: opacity .18s ease; }
  .shotbox-edge:hover { opacity: 1; }
  .shotbox-x:hover { background: rgba(10, 10, 12, 0.9); border-color: rgba(255, 255, 255, 0.55); }
  .strip-cell:hover { opacity: 0.85; }
  .strip-cell { width: 60px; height: 60px; }
}

@media (prefers-reduced-motion: reduce) {
  .shotbox-track { transition: none !important; }
  .shotbox-stage.is-loading::after { animation: none; transform: scaleX(1); opacity: 0.5; }
  .strip-cell { transition: none; }
  /* the drag still follows the finger — that is direct manipulation, not
     animation — but nothing springs back and nothing flies away: albums.js
     closes on the spot instead */
  .shotbox-x { transition: none; }
  .shotbox-x:active { transform: none; }
}
/* ROUND 13: #page-scroll owns vertical scrolling, so freezing that container
   preserves its scrollTop without the old fixed-body/top-offset workaround. */
.page-scroll.noscroll { overflow: hidden; }

.album-status {
  margin: 24px 0;
  text-align: center;
  color: var(--steel);
}

@media (min-width: 620px) {
  .album-shelf { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; }
  .shot-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
  .album-body h3 { font-size: 16.5px; }
}
@media (min-width: 980px) {
  .album-shelf { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .shot-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}

/* Reduced motion: the shelf is a list of links, not a show. */
@media (prefers-reduced-motion: reduce) {
  .album-card, .album-shot img { transition: none; }
  .album-card:active { transform: none; }
  /* the gradient stays — that is the tone of the box, not motion. Only the
     sweep across it is motion, and this is a full stop for motion. */
  .album-shot { animation: none; }
}

/* ── filter bar ──────────────────────────────────────────────────────────
   253 albums in one column is a scroll, not a browse (the owner, 2026-07-28:
   "right now it's just a massive scrolling thing… figure out a better
   sorting way"). ONE pill row cuts it down in one tap: which event, then
   which year, and since 2026-08-02 the two COMPOSE. Counts are on the chips
   because "Summer camp 7" tells you whether it's worth the tap and "Summer
   camp" doesn't.
   There used to be a second row above this one — "kind of event", five
   pills — and it was the SAME cut of the archive at a coarser zoom, holding
   the sticky slot while the finer row scrolled away. It is gone; the row that
   filters is the row that sticks.
   The bar sits OUTSIDE .container on purpose — sticky needs the full width,
   and on a phone the rows scroll edge to edge instead of wrapping to four
   stacked lines. */
.album-filters {
  position: sticky;
  /* parks under the sticky site .navbar; the offset is a CSS constant
     defined in styles.css, not a measured value */
  top: var(--youth-bar-h, 0px);
  z-index: 20;
  /* ONE 44px row plus air. It carried two, and with the site bar above it that
     was 182px of a 844px phone screen held permanently on a page whose first
     album was below the fold. Padding is trimmed to the minimum that still
     reads as a bar. */
  padding: 5px 0;
  /* Opaque, because the shelf scrolls under this bar and a translucent surface
     would let album titles read through it — but a tone DOWN from the page
     since 2026-08-10, not the page's own colour. It was --cream, which is
     exactly what the pills sitting on it are made of, so the bar had no edges
     and the pills had no ground: the whole strip read as loose words. One step
     of oak under them is what makes the row a shelf and the pills objects on
     it (the owner's direction: the buttons "coincide with the background"). Since 2026-08-10
     the year row below carries the SAME oak, so the two rows are one band and
     one section — see .album-years for why the ground is oak and not the page,
     and why the section's closing hairline moved down there. */
  background: var(--oak-100);
  border-bottom: 1px solid var(--line);
  box-shadow: 0 1px 0 rgba(var(--ink-rgb), 0.04);
}
.album-filters[hidden] { display: none; }

/* ── THE PINNED ROW LEAVES WITH THE HEADER ───────────────────────────────
   (the owner, iPhone, 2026-08-14 11:21 PM: the pill bar "stays fixed in place,
   floating over the album cards… it must scroll away with the content.")

   The site bar hides on scroll down since 0a7b3a31d (2026-08-12), and BOTH of
   this page's sticky rows park at its height. With the bar gone they kept the
   64px offset, so each hung a bar's height down the screen with album cards
   scrolling through the gap above it — a light strip floating over the
   photographs, which is what a sticky row parked under a header that is no
   longer there looks like. Measured at 390/430/1280: filter row and album
   topbar both left at top:64 with the navbar at top:-64.

   styles.css makes the same correction for the front page's progress hairline
   (`html.nav-hidden .progress { top: 0 }`); these two rows are the rest of the
   chrome parked at that offset, and they go the other way — OFF the top rather
   than up to it, because a filter bar alone at the top of a photo wall is still
   a bar the visitor did not ask to keep. --youth-bar-h (64px) clears both boxes
   (63px filter row, 57px topbar at every width), so the row leaves with the
   header and comes back with it on any scroll up. `top` only bites while a
   sticky box is actually stuck, so the stretch where the row is still in the
   flow is untouched — nothing jumps. */
.album-filters, .album-topbar { transition: top 200ms var(--ease-glide); }
html.nav-hidden .album-filters,
html.nav-hidden .album-topbar { top: calc(-1 * var(--youth-bar-h, 64px)); }
@media (prefers-reduced-motion: reduce) {
  .album-filters, .album-topbar { transition: none; }
}

/* ── AN OPEN ALBUM IS NOT A LOADING WALL ─────────────────────────────────
   (the owner, iPhone, 2026-08-14: "you see how it's dragged down. The screen is
   empty. It's a whole mess.")

   styles.css reserves the height of the three rows above the shelf — the
   sticky filter bar, the year pills and the Find-yourself faces — while the
   albums API is still in flight, so the wall lands IN the space instead of
   making it. It does that by out-specifying the [hidden] guards above with
   `html.js :is(#album-filters, #album-years, #album-find)[hidden]
   { display: block; visibility: hidden }`.

   But those three ids are hidden for a SECOND reason: albums.js's
   hideShelfChrome puts the whole wall away when an album takes the screen.
   The reservation could not tell the two apart, so opening an album left
   63 + 109 + 105 = 277px of invisible boxes above it — measured 294px of
   empty cream between the site header and the "All albums" bar at 390px,
   with the album title, Slideshow and the photographs all a third of a
   screen down. (WebKit hid it from the harnesses because showAlbum's
   `albumView.focus()` scrolls the ghost band out of view; iOS Safari does
   not scroll a focus() on a tabindex=-1 div, so the phone shows it.)

   Reserve for content that is COMING, never for content that has been put
   away. Same correction the `#album-shelf[hidden]:empty` line in styles.css
   makes for the wall itself — `:empty` cannot serve here because two of the
   three rows carry markup children and one is filled with pills. */
.media-wall-band:has(#album-view:not([hidden]))
  :is(#album-filters, #album-years, #album-find)[hidden] {
  display: none;
}

.pill-row {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  /* The topic row is 1,947px wide inside a 375px phone — the most-flicked strip
     on the site. Without this, a flick that runs out of pills hands the rest of
     the gesture to the PAGE: iOS rubber-bands the whole document sideways and,
     started near the left edge, arms the back-swipe. Every other rail here
     (.shotbox-strip, .album-find-rail, .album-hot-rail, .pp-with) already
     contains its overscroll; the pill rows were the omission (audit 2026-08-06,
     from a visitor's "maneuvering" report). */
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  /* 4px of block padding is the room the pill's own shadow and its focus ring
     need — overflow-x:auto clips the cross axis as well. BOTH filter rows now
     carry the same number: they were 3px and 9px, which is why the second row
     sat lower in its own band than the first one did (2026-08-10). */
  padding: 4px max(20px, env(safe-area-inset-right)) 4px max(20px, env(safe-area-inset-left));
  /* snap-align:start on the pills would otherwise park the first pill flush
     against the scrollport edge, clipping its left cap past the padding */
  scroll-padding-inline: max(20px, env(safe-area-inset-left));
}
/* flex outranks the browser's own [hidden] { display: none } — the year row
   ships hidden and stayed on screen as an empty 44px band without this */
.pill-row[hidden] { display: none; }
.pill-row::-webkit-scrollbar { display: none; }

/* T32: the selected semantics stay on the buttons while one measured ring
   travels between them. It overlays only the edge, so focus remains visible. */
.albums-page .pill-row { position: relative; }
.albums-page .pill-row .a-pill { position: relative; z-index: 1; }
.album-filter-indicator {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 2;
  width: var(--filter-indicator-w, 0);
  height: var(--filter-indicator-h, 44px);
  border: 1px solid var(--accent-deep);
  border-radius: 999px;
  box-shadow: inset 0 0 0 1px rgba(201, 154, 46, 0.24);
  pointer-events: none;
  transform: translate3d(var(--filter-indicator-x, 0), var(--filter-indicator-y, 0), 0);
  transition: transform 220ms var(--ease-glide), width 220ms var(--ease-glide), height 220ms var(--ease-glide);
}

/* ── WHICH WAY A RAIL STILL GOES (audit 2026-08-12, wave C) ──────────────
   Every sideways row on this page hides its scrollbar, so a pill or a face
   sliced by the screen edge was the only cue that the row moved — and half a
   word reads as a rendering fault, not as an invitation: the filter rail was
   cut at "B" (320), "BAPTISM 11" (390), "ICE S" (768) and "YOUTH CONFE"
   (1280) with nothing to say it scrolled. The row now dissolves at whichever
   end still has content behind it, and at NEITHER end while everything
   already fits — a fade over a complete row would be a lie about there being
   more. THE PATTERN IS .pp-with[data-edge] IN people.css; that is the
   original and the place to fix the idea. Its own attribute, so the two can
   never collide. albums.js and church-photos.js (railEdges) write
   data-rail-edge on scroll, on resize and on every refill.
   Kept off .album-kin-rail on purpose: that row lives inside an open album,
   which this audit did not cover. */
.pill-row[data-rail-edge],
.album-find-rail[data-rail-edge],
.album-hot-rail[data-rail-edge] {
  -webkit-mask-image: var(--rail-fade);
  mask-image: var(--rail-fade);
}
/* The three gradients, named once. Setting a custom property on the bare
   attribute paints nothing by itself — only the rails above opt in, and
   church-photos.css opts its year rail in the same way. */
[data-rail-edge="end"]   { --rail-fade: linear-gradient(to right, #000 calc(100% - 40px), transparent); }
[data-rail-edge="start"] { --rail-fade: linear-gradient(to right, transparent, #000 40px); }
[data-rail-edge="both"]  { --rail-fade: linear-gradient(to right, transparent, #000 40px, #000 calc(100% - 40px), transparent); }

/* THE ROWS START ON THE PAGE'S OWN LEFT EDGE (2026-08-10). They are full-bleed
   so a phone flick runs the pills off both screen edges, but the gutter they
   pay was a flat 20px at every width while .container steps 20 → 32 → 48. On a
   1280px screen the first pill therefore began 28px LEFT of the heading, the
   "Find yourself" label, the face rail and the first album — the whole section
   above and below it — which is the "not centered the same way" the owner saw.
   Same three steps as .album-find-head / .album-find-rail below, which is the
   pattern this section already used everywhere except here. */
@media (min-width: 640px) {
  .album-topics, .album-years {
    padding-inline: max(32px, env(safe-area-inset-left)) max(32px, env(safe-area-inset-right));
    scroll-padding-inline: max(32px, env(safe-area-inset-left));
  }
}
@media (min-width: 1024px) {
  .album-topics, .album-years {
    padding-inline: max(48px, env(safe-area-inset-left)) max(48px, env(safe-area-inset-right));
    scroll-padding-inline: max(48px, env(safe-area-inset-left));
  }
}

/* The year row: NOT pinned (2026-08-02), and it sits directly under the bar so
   arriving on the page shows both. Two pinned rows plus the site bar was 182px
   of a 844px phone held permanently; the year is the filter people change
   least, and apply() scrolls this row flush to the bar on every pick, so it is
   on screen exactly when it is in use.
   ── ONE BAND, NOT TWO (2026-08-10) ──────────────────────────────────────
   The two rows are one control group and they now sit on one ground. This row
   used to sit on the page's own --cream while the pinned row above it sat on
   --oak-100: the same white pill read as two different objects, because the
   ground under one of them was a step lighter, and the section had no bottom
   edge at all — it just stopped. The band is the OAK, not the page, for the
   reason the bar is oak: a control has to sit on something darker than itself
   or it is a tint of the page rather than a raised object. So the oak runs
   under both rows and the hairline that closes the section moved down here;
   the bar keeps its own hairline because when the page scrolls, that row
   detaches and floats over album covers, where it still needs an edge. */
.album-years {
  background: var(--oak-100);
  border-bottom: 1px solid var(--line);
  padding-block: 8px 12px;
  margin-bottom: 16px;
}

/* THE ROWS HOLD THEIR OWN HEIGHT, FROM THE FIRST PAINT (2026-09-05).

   The two ids are already reserved — `html.js #album-filters { min-height:
   63px }` and `#album-years { min-height: 65px }` ship in the inline critical
   block, which is the right place for a region that is EMPTY while its answer
   is in flight. What was missing is that the height those numbers describe is
   built out of `.a-pill { min-height: 44px }`, and .a-pill lives in styles.css,
   which is deferred behind media="print" until boot.js flips it — 3,130ms out
   on Fast 3G. So between the pills landing and that sheet arriving, a filled
   row was only as tall as unstyled buttons made it, and then grew to its real
   size underneath the headline (measured 2026-09-05: the section container
   stepping 0 -> 52 -> 64/65px, CLS 0.0330 at 258ms broadband / 2,973ms phone).

   THIS FILE IS RENDER-BLOCKING (albums/index.html loads it as a plain
   stylesheet, not behind media="print"), so a floor written here is in force at
   the first paint and cannot be undone by how late styles.css is. The rows can
   then only ever be filled or reserved, never briefly short.

   THE NUMBERS ARE THE ROWS' OWN GEOMETRY, not a guess: 44px of pill plus
   .pill-row's 4px+4px is 52; plus .album-years' 8px+12px in place of that
   padding is 64, carried at 65 so this floor and the inline reservation for the
   same row cannot disagree by a pixel. .album-filters needs none of its own:
   5px + 52 + 5 + its 1px hairline is the 63 already reserved for it.

   A ROW THAT IS GENUINELY ABSENT STILL COSTS NOTHING. Both ship `hidden` and
   stay hidden until they have something in them, and `hidden` here resolves
   either to `display: none` (the .pill-row rule above) or to the inline
   block's reserved `display: block; visibility: hidden` — and a min-height on
   the row cannot turn either of those into empty space. */
.album-topics { min-height: 52px; }
.album-years  { min-height: 65px; }

/* The pill itself lives in styles.css now (2026-08-10) — one component for
   every filter row on the site, shared with /media/ and /rewind/. This file
   keeps only what is this page's own: where the rows sit, what band they are
   on, and how they bleed. */

/* ── the topic row ───────────────────────────────────────────────────────
   WHICH thing: Summer camp, Caroling, the New Year's party. Since 2026-08-02
   this is THE event filter and it is INSIDE the sticky bar — it used to sit
   above it and scroll away while a coarser row it contradicted stayed pinned.
   ONE PILL FOR THE WHOLE SECTION (2026-08-10). This row used to draw its own
   pill — 11px type against the year row's 12px, 0.1em tracking against 0.08,
   3px of row padding against 9 — as a way of ranking itself below the years.
   Two rows of the same control at two sizes is not a hierarchy, it is a
   mismatch (the owner's direction: "the bottom pills look a bit different"), and it was the
   only place on this page where the same object was drawn twice. The ranking
   that was actually wanted is carried by POSITION and GROUND instead: the
   event row is the one pinned to the top of the screen, the year row scrolls
   under it. Every control in this section is the shared .a-pill (styles.css),
   and this row has no rule of its own left beyond the gutter steps up in the
   .pill-row block — its old copies of the pressed and hover states went with
   the second pill. */

/* ── find yourself ───────────────────────────────────────────────────────
   The door to the face archive, on the page where somebody is already looking
   through photographs of themselves (the owner, 2026-08-02 — /people/ was "kind
   of a hidden feature"). It sits between the filters and the first album, and
   the whole thing is one row about 100px tall: a door has to be seen without
   costing the shelf its place on the phone screen. Any taller and it becomes a
   second gallery in front of the gallery, which is the mistake the front page
   spent 2026-08-02 undoing.
   Full-bleed like the filter rows and the most-opened rail below, so the row
   scrolls edge to edge on a phone and the last circle running off the screen
   is what says "there are more". */
.album-find {
  padding: 4px 0 14px;
}
.album-find[hidden] { display: none; }
/* The church wall puts this row INSIDE its .container (church-photos.js hides
   that whole view when an album opens, and the row has to go with it), so the
   container already pays the gutter and the row must not pay it twice. The
   rail still scrolls — ten 64px circles plus the pill overflow a phone by a
   wide margin — so the "there is more of this" cue survives the change; what
   is lost is only the edge-to-edge bleed, which a container cannot give. */
.album-find--inline .album-find-head,
.album-find--inline .album-find-rail { padding-inline: 0; }
/* Two classes deep for the same reason .album-hot-head is: this is an <h2>
   inside a .section, and `.section h2` display type outranks a single class. */
.album-find .album-find-head {
  margin: 0 0 9px;
  padding-inline: max(20px, env(safe-area-inset-left)) max(20px, env(safe-area-inset-right));
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--steel);
}
/* The count, quieter than the label — but only as quiet as 11px text is
   allowed to be on this ink. 0.52 white over --ink is 5.8:1; the 0.42 it was
   first drawn at measures 4.0 and fails AA on a size this small. */
.album-find .album-find-head .n {
  color: var(--fg-quiet);
  letter-spacing: 0.08em;
}
.album-find-rail {
  display: flex;
  align-items: center;
  gap: 10px;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  padding-inline: max(20px, env(safe-area-inset-left)) max(20px, env(safe-area-inset-right));
  padding-bottom: 2px;
}
.album-find-rail::-webkit-scrollbar { display: none; }
@media (min-width: 640px) {
  .album-find .album-find-head, .album-find-rail {
    padding-inline: max(32px, env(safe-area-inset-left)) max(32px, env(safe-area-inset-right));
  }
}
@media (min-width: 1024px) {
  .album-find .album-find-head, .album-find-rail {
    padding-inline: max(48px, env(safe-area-inset-left)) max(48px, env(safe-area-inset-right));
  }
}
/* 64px of face: the crop the scan already cut is 112px, so this is a
   downscale on every device and never a blur. The circle IS the tap target —
   64px clears the 44px floor on its own, so there is no padding to add and no
   label under it to grow the row. */
.album-find-face {
  flex: 0 0 auto;
  display: block;
  width: 64px;
  height: 64px;
  border-radius: 999px;
  overflow: hidden;
  background: var(--oak-100);
  /* THE HOUSE CONTROL EDGE, not a structure hairline (the owner, 2026-08-10:
     these should "carry a border… so they read as raised tappable objects").
     It wore --line until today — #e2d8c6 on the --oak-100 band measures 1.24:1,
     which is a shape drawn round a photograph rather than an edge you can see.
     --control-line composites to rgb(119,130,146) here: 3.4:1, past the WCAG
     1.4.11 non-text floor, and the same navy every other tappable thing on a
     light band wears. 2px because a 1px ring disappears against the busiest
     third of a face crop; --shadow-control is the same lift the pill at the end
     of this row already has, so the circles and the pill read as one rank of
     objects instead of photographs beside a control.
     Brass is deliberately NOT used here: gold is this site's SELECTED state,
     and ten faces are not ten selections. It stays on hover and focus. */
  border: 2px solid var(--control-line);
  box-shadow: var(--shadow-control);
  /* iOS leaves a hairline of backdrop inside a 999px radius otherwise — the
     gold-pill fix, 2026-07-26 */
  background-origin: border-box;
  transition: transform var(--press-up) var(--ease-press),
              border-color var(--t-fast) var(--ease),
              box-shadow var(--t-med) var(--ease);
}
.album-find-face img { display: block; width: 100%; height: 100%; object-fit: cover; }
.album-find-face:active {
  transition-duration: var(--press-down);
  transform: scale(0.94);
  box-shadow: none;                    /* pressed in, so the lift goes away */
}
.album-find-face:focus-visible { outline: 2px solid var(--gold-ring); outline-offset: 3px; }
@media (hover: hover) {
  .album-find-face:hover {
    border-color: var(--control-line-strong);
    box-shadow: var(--shadow-control-lift);
  }
}
/* "All people →" — the tail of the row, and the only words in it. Same pill
   the filter rows use, at their height, so the row reads as one object. */
.album-find-all {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 16px;
  border: 1px solid var(--control-line);
  border-radius: 999px;
  background: var(--control-bg);
  box-shadow: var(--shadow-control);
  background-origin: border-box;
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  text-decoration: none;
  /* the graceful press, which this one control never had: every other pill in
     these rows pushes in and springs back, and the tail of the row did not */
  transition: border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease),
              background var(--t-fast) var(--ease), box-shadow var(--t-med) var(--ease),
              transform var(--press-up) var(--ease-press);
}
.album-find-all:active { transition-duration: var(--press-down); transform: scale(0.96); box-shadow: none; }
.album-find-all:focus-visible { outline: 2px solid var(--gold-ring); outline-offset: 3px; }
@media (hover: hover) {
  .album-find-all:hover { border-color: var(--control-line-strong); background: #fff; box-shadow: var(--shadow-control-lift); color: var(--ink); }
}
@media (prefers-reduced-motion: reduce) {
  .album-find-face, .album-find-face:active { transition: none; transform: none; }
}

/* ── what people actually open ───────────────────────────────────────────
   A rail of the photographs opened most since the counter started. It sits
   BELOW the shelf since 2026-08-02 and it no longer prints a count on each
   tile: the order is the ranking, and site analytics set in display type told
   a visitor nothing while making the front of the archive read like a scored
   table. Never a ranking of people either: the archive ranks photographs and
   nothing else.
   Full-bleed like the filter bar, so the last tile runs off the edge and the
   rail reads as scrollable without a scrollbar to say so. A hairline above it
   because it is now a footnote under the albums rather than a lead-in. */
.album-hot {
  padding: 26px 0 2px;
  margin-top: 12px;
  border-top: 1px solid var(--line);
}
.album-hot[hidden] { display: none; }
/* Two classes deep on purpose: this is an <h2> inside a .section, and
   `.section h2` (37–58px display type) outranks a single class — the label
   rendered as a headline until it was raised above it (2026-08-02). */
.album-hot .album-hot-head {
  margin: 0 0 12px;
  padding-inline: max(20px, env(safe-area-inset-left)) max(20px, env(safe-area-inset-right));
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--steel);
}
/* No scroll-snap here, deliberately. A snap container aligns its first snap
   target with the scrollport edge, which swallows the rail's own left inset —
   the tiles would start hard against the screen edge while the heading above
   them sat at the container margin, and the two would read as misaligned.
   A rail of photographs has nothing to snap to anyway. */
.album-hot-rail {
  display: flex;
  gap: 12px;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  scroll-snap-type: inline mandatory;
  padding-inline: max(20px, env(safe-area-inset-left)) max(20px, env(safe-area-inset-right));
  padding-bottom: 4px;
}
.album-hot-rail::-webkit-scrollbar { display: none; }
@media (min-width: 640px) {
  .album-hot .album-hot-head, .album-hot-rail {
    padding-inline: max(32px, env(safe-area-inset-left)) max(32px, env(safe-area-inset-right));
  }
}
@media (min-width: 1024px) {
  .album-hot .album-hot-head, .album-hot-rail {
    padding-inline: max(48px, env(safe-area-inset-left)) max(48px, env(safe-area-inset-right));
  }
}
.hot-tile {
  flex: 0 0 auto;
  width: 136px;
  color: inherit;
  text-decoration: none;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  scroll-snap-align: center;
  opacity: 0.72;
  transform: scale(0.96);
  transition: opacity 220ms var(--ease-glide), transform 220ms var(--ease-glide);
}
.hot-tile.is-current { opacity: 1; transform: none; }
.hot-shot {
  display: block;
  aspect-ratio: 1 / 1;
  border-radius: 12px;
  overflow: hidden;
  background: var(--oak-100);
  border: 1px solid var(--line);
  transition: transform var(--press-up) var(--ease-press),
              border-color var(--t-fast) var(--ease);
}
.hot-shot img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* the album it came from, at a whisper — the only words the tile carries now */
.hot-alb {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin-top: 8px;
  font-size: 12px;
  line-height: 1.35;
  color: var(--steel);
}
.hot-tile:active .hot-shot { transition-duration: var(--press-down); transform: scale(0.97); }
.hot-tile:focus-visible { outline: 2px solid var(--gold-ring); outline-offset: 3px; border-radius: 12px; }
@media (hover: hover) {
  .hot-tile:hover .hot-shot { border-color: rgba(201, 154, 46, 0.5); }
  .hot-tile:hover .hot-alb { color: var(--ink); }
}
@media (min-width: 640px) {
  .hot-tile { width: 168px; }
}
@media (prefers-reduced-motion: reduce) {
  .hot-shot, .hot-tile { transition: none; }
  .hot-tile { transform: none; }
  .hot-tile:active .hot-shot { transform: none; }
}

/* T01 (the thumbnail→viewer morph) was removed on 2026-08-23 together with
   openBoxFromTile in albums.js — the reasoning and the frame evidence are in
   that comment. Nothing draws .album-photo-transition or names 'album-photo'
   any more, so its rules came off with it. */

@media (prefers-reduced-motion: reduce) {
  .album-filter-indicator, .strip-indicator { transition: none; }
  .album-toast.is-showing, .album-toast.is-leaving { animation: none; transition: none; }
}

.album-none {
  grid-column: 1 / -1;
  margin: 8px 0 0;
  font-size: 14px;
  color: var(--steel);
}

/* ── album ↔ album stepping ──────────────────────────────────────────────
   "easy to open and to go back and forth on for mobile" — walking to the
   next camp shouldn't mean Back, find your place, scroll, tap. The pair
   repeats under the photos because that's where you are when you finish
   one. */
/* STICKY since 2026-07-29 — this is the fix for "albums have no easy way back
   to choose another one". The filter bar is deliberately hidden while an album
   is open, so this bar takes its place at the same offset: exactly one sticky
   row on screen at any time, never two. "All albums" and Newer/Older are
   therefore reachable from photo 400 without scrolling to the top.
   Full-bleed background: it lives inside .container, so it cancels the
   container padding and re-adds it, otherwise photos show through the gutters
   as they scroll under the bar. */
.album-topbar {
  position: sticky;
  top: var(--youth-bar-h, 0px);
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  background: var(--cream);
  border-bottom: 1px solid var(--line);
  margin-inline: calc(-1 * max(20px, env(safe-area-inset-left)))
                 calc(-1 * max(20px, env(safe-area-inset-right)));
  padding: 6px max(20px, env(safe-area-inset-right)) 6px max(20px, env(safe-area-inset-left));
}
@media (min-width: 640px) {
  .album-topbar {
    margin-inline: calc(-1 * max(32px, env(safe-area-inset-left)))
                   calc(-1 * max(32px, env(safe-area-inset-right)));
    padding-inline: max(32px, env(safe-area-inset-left)) max(32px, env(safe-area-inset-right));
  }
}
@media (min-width: 1024px) {
  .album-topbar {
    margin-inline: calc(-1 * max(48px, env(safe-area-inset-left)))
                   calc(-1 * max(48px, env(safe-area-inset-right)));
    padding-inline: max(48px, env(safe-area-inset-left)) max(48px, env(safe-area-inset-right));
  }
}
.album-step { display: flex; gap: 8px; }
.album-step-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  padding: 0 14px;
  /* the control hairline + a raised fill, same as every other pill on this
     page since 2026-08-10; the Newer/Older pair sits inside a sticky bar over
     a photograph shelf and had the least edge of anything on the page */
  border: 1px solid var(--control-line);
  border-radius: 999px;
  background: var(--control-bg);
  box-shadow: var(--shadow-control);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 11.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  transition: border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease),
              background var(--t-fast) var(--ease), box-shadow var(--t-med) var(--ease),
              transform var(--press-up) var(--ease-press);
}
.album-step-btn:active { transition-duration: var(--press-down); transform: scale(0.96); box-shadow: none; }
.album-step-btn:focus-visible { outline: 2px solid var(--gold-ring); outline-offset: 3px; }
.album-step-btn[disabled] { opacity: 0.32; cursor: default; }
.album-step-lbl { white-space: nowrap; }
/* The neighbour's year, shown only when the step crosses one ("Older · 2022").
   The span is in the markup either way and reserves its width either way —
   7ch is nbsp + "· " + four digits, and in the mono face every glyph is one
   ch — so the pair is the same size on both sides of a boundary. .album-topbar
   is a sticky flex row with wrap on: a button that grows mid-tap can push its
   twin onto a second line and change the bar's height under the finger, which
   is a worse fault than a button carrying a little empty space. */
.album-step-yr { display: inline-block; min-width: calc(7ch + 0.62em); }
@media (hover: hover) {
  .album-step-btn:not([disabled]):hover { border-color: var(--control-line-strong); background: #fff; box-shadow: var(--shadow-control-lift); color: var(--ink); }
}
.album-step-foot {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  margin: 22px 0 0;
}
/* the phone gets the words; a 44px arrow-only button is a guessing game */
@media (max-width: 419px) {
  .album-step-btn { padding: 0 12px; font-size: 11px; }
}
/* THE BAR IS ONE ROW ON A PHONE (2026-08-01). The year reserve above costs
   ~55px per button, which pushed the Newer/Older pair onto a second line at
   390px and doubled this sticky bar to 113px — a quarter of the screen held
   permanently, over the photos it sits on. What the reserve was protecting is
   the bar's HEIGHT, so hold that directly: no wrap, and the year hint costs
   nothing. The back link is the flexible item and gives up the width. */
@media (max-width: 479px) {
  .album-topbar { flex-wrap: nowrap; }
  .album-back { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .album-step { flex: none; }
  .album-step-yr { min-width: 0; }
}
/* The bottom pair says "Newer album" because down there you are far from
   anything that says what the arrows step through. Once the year rides along
   too ("Older album · 2022") the two no longer fit side by side on a phone.
   The qualifier is what goes: "Older · 2022" still says it, in the same words the
   pair at the top of the album uses. The full phrase stays in data-label, so
   the button's spoken name never loses it. 479px covers every phone. */
@media (max-width: 479px) {
  .album-step-more { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  /* .a-pill's own reduced-motion rule ships with the component in styles.css */
  .album-step-btn { transition: none; }
}

/* ── the same event, in other years ───────────────────────────────────────
   A rail of years under the album header. It is a NAVIGATION strip, not a
   feature band: quiet mono chips on ink, no card, no picture, so it reads as
   an index line over the photographs rather than competing with them. The one
   filled chip is where you are standing.
   Horizontally scrollable on purpose — thirteen Easters do not fit a 390px
   phone, and wrapping them into three rows would push the album's first
   photograph below the fold. */
.album-kin {
  margin: 0 0 20px;
  padding-top: 2px;
}
/* display:block on a flex child outranks the browser's own [hidden] rule —
   the same trap .shotbox-full and the pill rows carry a guard
   for. Without it an album with no siblings shows an empty heading. */
.album-kin[hidden] { display: none; }
.album-kin-head {
  margin: 0 0 8px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  /* 0.62, not 0.5 (2026-08-01): 11px uppercase mono is small text, so it is
     held to the 4.5:1 line, and 0.5 white on --ink sat at 5.3:1 — clearing it
     by less than a step of letter-spacing. This is the same value every other
     quiet label in this file uses. */
  color: var(--steel);
  font-weight: 500;
}
.album-kin-rail {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  overflow-y: hidden;
  padding-bottom: 4px;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}
.album-kin-rail::-webkit-scrollbar { display: none; }
.kin-chip {
  flex: none;
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  min-height: 44px;
  padding: 0 16px;
  border-radius: 999px;
  border: 1px solid var(--control-line);
  background: var(--control-bg);
  box-shadow: var(--shadow-control);
  color: var(--ink);
  text-decoration: none;
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: 0.04em;
  align-content: center;
  flex-wrap: wrap;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: background .18s cubic-bezier(.4, 0, .2, 1),
              border-color .18s cubic-bezier(.4, 0, .2, 1),
              color .18s cubic-bezier(.4, 0, .2, 1),
              transform .18s cubic-bezier(.4, 0, .2, 1);
}
.kin-chip b { font-weight: 700; }
/* the month, only where a year holds two of the same event — see renderKin */
.kin-mon { font-size: 11px; opacity: 0.6; letter-spacing: 0.06em; }
.kin-chip:active { transition-duration: var(--press-down); transform: scale(0.96); }
.kin-chip:focus-visible { outline: 2px solid var(--gold-ring); outline-offset: 3px; }
@media (hover: hover) {
  .kin-chip:hover {
    border-color: rgba(201, 154, 46, 0.5);
    background: rgba(201, 154, 46, 0.1);
    color: var(--ink);
  }
}
/* Where you are standing: colour AND weight AND fill, so it reads at a glance
   on a phone rather than as a slightly different grey. */
.kin-chip.is-here {
  border-color: var(--accent);
  background: rgba(201, 154, 46, 0.18);
  color: var(--gold-text);
  font-weight: 700;
  cursor: default;
}
@media (prefers-reduced-motion: reduce) {
  .kin-chip:active { transform: none; }
}

/* ---------- the other ways to look, at the FOOT of the shelf ----------
   The Rewind (a year in one scroll) and the People pages (the same shelf
   sorted by who is in it). Both are ways of re-cutting the albums this page
   lists — which is exactly why they spent 2026-08-01 as two brass pills ABOVE
   the first album, links to other pages standing in front of this page's own
   content. Since 2026-08-02 they are two quiet text links at the end, under a
   small heading: still findable, no longer competing with the archive.
   Stacked, not a row: a full-width target is easier to hit than a pill, and
   nothing here shrinks below 44px. */
.shelf-doors-wrap {
  margin-top: 34px;
  padding-top: 22px;
  border-top: 1px solid var(--line);
}
.shelf-doors-wrap[hidden] { display: none; }
.shelf-doors-wrap .shelf-doors-head {
  margin: 0 0 6px;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fg-quiet);
}
.shelf-doors { display: flex; flex-direction: column; }
.shelf-door {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  color: var(--ink);
  text-decoration: none;
  font-size: 14.5px;
  line-height: 1.4;
  border-bottom: 1px solid var(--line);
  transition: color var(--t-fast) var(--ease);
}
.shelf-door:last-child { border-bottom: 0; }
/* the arrow is the row's one moving part — same nudge a.week-row and .ex-card
   already use, so a door reads as a door on a phone too (2026-08-13: the row
   had a hover-only colour change and nothing at all for a finger) */
.shelf-door .arr { color: var(--gold-text); transition: transform var(--press-up) var(--ease-press); }
.shelf-door:active { transition-duration: var(--press-down); color: var(--accent-deep); }
.shelf-door:active .arr { transition-duration: var(--press-down); transform: translateX(3px); }
@media (hover: hover) {
  .shelf-door:hover { color: var(--ink); }
  .shelf-door:hover .arr { transform: translateX(3px); }
}
@media (prefers-reduced-motion: reduce) {
  .shelf-door .arr { transition: none; }
  .shelf-door:hover .arr, .shelf-door:active .arr { transform: none; }
}

/* ---------- the line that says what is filtered ----------
   An event and a year now stack, so the wall has to state the pair it is
   showing and offer the way out of it — a pressed chip somewhere in a
   scrolled row is not an answer. Centred with the status line it shares an
   element with. */
.album-clear {
  display: inline-flex;
  align-items: center;
  /* the house floor for anything a thumb has to hit */
  min-height: 44px;
  margin-left: 8px;
  padding: 0 2px;
  border: 0;
  background: none;
  color: var(--gold-text);
  font: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.album-clear:focus-visible { outline: 2px solid var(--gold-ring); outline-offset: 2px; }

/* With a year in hand, the same year as one scroll — this is how the Rewind
   is reached now that it is not in the menu. Quiet on purpose: it is an offer,
   not the point of the page. */
.album-yr-rewind { margin: 0 0 14px; text-align: center; }
.album-yr-rewind[hidden] { display: none; }
.album-yr-rewind a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--gold-text);
  text-decoration: none;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.06em;
}
@media (hover: hover) {
  .album-yr-rewind a:hover { text-decoration: underline; text-underline-offset: 4px; }
}

/* A video in the album plays on the stage itself (Flickr's own player in an
   iframe — see showVideo in albums.js). It sizes to the video's shape, not
   the image rules above, and stays inside the stage on a phone in landscape
   where 16:9 is taller than the screen is. */
.shotbox-video {
  width: 100%;
  max-width: 100%;
  max-height: 100%;
  aspect-ratio: 16 / 9;
  border: 0;
  border-radius: var(--radius);
  background: #000;
}
.shotbox-video[hidden] { display: none; }
.shotbox-stage.is-video .shotbox-track { width: 100%; }

/* The stepper is waiting on the next album's photos. Not a spinner — the
   arrows just read as busy, so a slow tap doesn't look like a dead button. */
.album-step-btn.is-busy { opacity: 0.45; }

/* ── the gold long-press wash (the owner, real iPhone, 2026-08-09) ────────────
   Pressing and holding a photo tile for a couple of seconds turned the WHOLE
   picture strong yellow-orange.

   It is not a press state and not the tap highlight — every tile already sets
   `-webkit-tap-highlight-color: transparent`. It is `::selection` in
   styles.css, which paints the brand gold (--accent, #c99a2e) and is exactly
   right for text. On iOS a long press on an image inside a link begins a
   CONTENT selection, and Safari then fills the selected node with that colour:
   one tile, entirely gold.

   The cure is to say these are controls, not prose. A photo tile, an album
   card, a filmstrip cell and a face circle hold nothing anybody would ever want
   to select, so selection simply does not start on them and there is nothing
   for ::selection to paint. Everything else on the site stays selectable, and
   the gold selection over real text is untouched.

   NOT -webkit-touch-callout: that would also remove iOS's own long-press menu,
   and "Save Image" on a photograph of your own church is a feature here, not an
   accident. The press states these tiles already have (a slight scale) are what
   gives the touch its feedback. */
.album-card,
.shot-tile,
.strip-cell,
.hot-tile,
.album-find-face {
  -webkit-user-select: none;
  user-select: none;
}

/* Skeleton tiles drawn in the tap itself while an uncached album fetches
   (albums.js albumShell, 2026-09-05) — same grid cell as a real tile. */
.shot-tile.shot-skel {
  background: var(--oak-200);
  border-radius: var(--radius-tile, 10px);
  aspect-ratio: 1 / 1;
}
