/* ============================================================
   Photography Portfolio — Kees Leemeijer
   Munich '72 spirited inheritance · alectear-feel craft
   ============================================================ */

:root {
  --field:         #F2EEE5;
  --blue:          #1635EE;
  --charcoal:      #1A1A1A;
  --blue-soft:     rgba(22, 53, 238, 0.6);

  /* Spatial — 12x8 cells of an 88px module with a 24px gutter. Must stay in
     sync with TILE_W / TILE_H in app.js; tools/check-arrangements.js asserts it. */
  --tile-width:   1320px;
  --tile-height:   872px;
  --inset-corner:   14px;  /* species plate offset from viewport bottom-left */
  --radius-card:     8px;  /* bloomed species plate corner radius */

  /* Type sizes */
  --wm-size-d:    0.95rem;
  --wm-size-m:    0.86rem;
  --label-size-m: 0.78rem;

  /* Motion */
  --focus-fade:      200ms;
  --focus-easing:    cubic-bezier(0.22, 0.61, 0.36, 1);
  --label-in:        200ms;
  --label-out:       150ms;
  --photo-decode:    200ms;

  /* Substitute for ABC Diatype */
  --font-stack: 'ABC Diatype', system-ui, -apple-system, 'Segoe UI', sans-serif;

  /* Ambient treatment for out-of-focus photographs. app.js used to mirror these
     into custom properties at boot; the two copies had drifted, so the bridge
     was removed on 2026-07-29 and these are now the single source of truth.
     --veil-ambient stands in for the old brightness(0.81): compositing black at
     (1 - 0.81) dims each channel the same way, without a filter on every tile. */
  --ambient-opacity: 0.9;
  --ambient-saturate: 0;
  --veil-ambient: 0.19;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: var(--field);
  color: var(--charcoal);
  font-family: var(--font-stack);
  font-feature-settings: "kern" 1, "liga" 1;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow: hidden;
  letter-spacing: -0.005em;
}

/* ============================================================
   Stage — the 2D pannable plane
   ============================================================ */
.stage {
  position: fixed;
  inset: 0;
  overflow: hidden;
  /* Static grab cursor. Swapping to `grabbing` during drags changed an
     inherited property on the ancestor of ~1700 nodes, forcing a whole-
     subtree style recalc (745ms+ long task at 6x CPU) at every gesture
     start and end. The cursor staying `grab` while dragging is the
     accepted trade. */
  cursor: grab;
  background: var(--field);
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
}

.plane {
  position: absolute;
  top: 0;
  left: 0;
  will-change: transform;
}

/* ============================================================
   Tile — one arrangement
   ============================================================ */
.tile {
  position: absolute;
  width: var(--tile-width);
  height: var(--tile-height);
}

.photo {
  position: absolute;
  /* overflow:hidden removed so the focus shadow (::before) can render outside
     the box. The img (object-fit:cover, inset:0) and placeholder are exactly
     the slot size, so nothing else overflows. */
  background: var(--field);
  /* opacity (cream wash) and transform are the only transitioned properties on
     focus — both compositor-friendly. Brightness and shadow are carried by the
     opacity-only pseudo overlays below; saturation is a STATIC filter that
     swaps instantly (animating filter on ~15 focused tile-copies per focus
     change was a top long-task source). */
  opacity: var(--ambient-opacity);
  filter: saturate(var(--ambient-saturate));
  transition:
    opacity var(--focus-fade) var(--focus-easing),
    transform var(--focus-fade) var(--focus-easing);
}
/* Brightness veil — see --veil-ambient. Reproduces the old brightness() dim
   exactly; only its opacity transitions (composited). */
.photo::after {
  content: '';
  position: absolute;
  inset: 0;
  background: #000;
  opacity: var(--veil-ambient);
  transition: opacity var(--focus-fade) var(--focus-easing);
  pointer-events: none;
  z-index: 3;
}
/* Pre-rendered focus shadow — static geometry, opacity-faded (composited). */
.photo::before {
  content: '';
  position: absolute;
  inset: 0;
  box-shadow:
    0 1px 2px rgba(0, 0, 0, 0.04),
    0 18px 48px -12px rgba(0, 0, 0, 0.18);
  opacity: 0;
  transition: opacity var(--focus-fade) var(--focus-easing);
  pointer-events: none;
  z-index: -1;
}
.photo.is-focused {
  opacity: 1;
  filter: none;                /* instant saturation reveal (untransitioned) */
  z-index: 2;
}
.photo.is-focused::after  { opacity: 0; }   /* brightness up */
.photo.is-focused::before { opacity: 1; }   /* shadow fades in */

/* Decode fade-in */
.photo.is-entering {
  opacity: 0;
  transform: translateY(4px);
}
.photo.is-entered {
  transition:
    opacity var(--photo-decode) ease-out,
    transform var(--photo-decode) ease-out,
    filter var(--focus-fade) var(--focus-easing);
}

/* The placeholder bar — subtly striped SVG-style fill with
   a centered monospace identifier. Stand-in for real photo. */
.photo .placeholder {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background:
    repeating-linear-gradient(
      0deg,
      var(--ph-band-a, #d9d3c5) 0 14px,
      var(--ph-band-b, #cfc7b6) 14px 28px
    );
}
.photo .placeholder::after {
  content: attr(data-label);
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 11px;
  letter-spacing: 0.06em;
  color: rgba(26, 26, 26, 0.5);
  text-transform: uppercase;
  padding: 4px 8px;
  background: rgba(242, 238, 229, 0.85);
  border-radius: 1px;
  white-space: nowrap;
}

/* Real photograph layer — covers the placeholder once decoded.
   object-fit: cover crops to slot rectangle (e.g. landscape → wide W slots). */
.photo .photo-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  opacity: 0;
  transition: opacity 360ms ease-out;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}
.photo.has-image .photo-img { opacity: 1; }
.photo.has-image .placeholder { opacity: 0; }

/* The plane once carried a brand card in one slot per tile (.photo.is-brand).
   Removed 2026-07-28 so every slot is a bird; identity is the .identity corner
   element below. */

/* ============================================================
   Identity — wordmark + pictogram (top-left)
   ============================================================ */
.identity {
  position: fixed;
  top: 18px;
  left: 18px;
  z-index: 10;
  display: inline-flex;
  align-items: center;
  gap: 14px;
  text-decoration: none;
  color: var(--charcoal);
  font-size: var(--wm-size-d);
  font-weight: 500;
  letter-spacing: -0.01em;
  opacity: 0;
  animation: fadeIn 200ms ease-out 80ms forwards;
  cursor: pointer;
}
.identity .pictogram {
  width: 22px;
  height: 22px;
  display: block;
  border-radius: 1px;
  flex-shrink: 0;
}
.identity .name { color: var(--charcoal); }
.identity .dot  { color: var(--blue); }
.identity .name,
.identity .dot {
  opacity: 0;
  transition: opacity var(--label-out) ease-in;
}
.identity:hover .name,
.identity:hover .dot,
.identity:focus-visible .name,
.identity:focus-visible .dot {
  opacity: 1;
  transition: opacity var(--label-in) ease-out;
}

/* ============================================================
   Species label — pinned bottom-left of viewport
   Foreground / background pattern:
     Resting   — vernacular only, small-caps tracked, on photo
     Dwell cue — 24px hairline fades in beneath after 800ms idle
     Bloomed   — cream plate scales open from corner, full trio
   Container is pointer-events:none so the viewport remains pannable
   everywhere except the visible text itself.
   ============================================================ */
.species {
  position: fixed;
  bottom: var(--inset-corner);
  left: var(--inset-corner);
  /* Above .fullscreen (15): the opener and its plate stay readable as an
     overlay on top of the full-screen image. Only .fs-toggle (40) is higher. */
  z-index: 30;
  pointer-events: none;
  /* No resting padding: the resting label is absolutely pinned to the corner,
     so padding only frames the bloom plate (set on .is-blooming). Padding is no
     longer transitioned — that morph was part of the bloom's CLS + long tasks. */
  padding: 0;
  max-width: 40vw;
  opacity: 0;
  transition: opacity var(--label-out) ease-in;
  isolation: isolate;
}
.species.is-visible {
  opacity: 1;
  transition: opacity var(--label-in) ease-out;
}

/* Bloom plate — pseudo-element scales out from bottom-left corner.
   The plate is now a lifted floating card (--inset-corner from edges),
   so it carries a full hairline + a soft shadow rather than the old
   half-frame that relied on the viewport edges. */
.species::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--field);
  border: 1px solid rgba(26, 26, 26, 0.22);
  border-radius: var(--radius-card);
  box-shadow: 0 6px 24px -8px rgba(0, 0, 0, 0.22);
  transform-origin: bottom left;
  transform: scale(0);
  opacity: 0;
  transition:
    transform 280ms cubic-bezier(0.2, 0.8, 0.2, 1),
    opacity 200ms ease;
  z-index: -1;
  pointer-events: none;
}
.species.is-blooming {
  pointer-events: auto;
  padding: 14px 20px;
  width: 40vw;
}
.species.is-blooming::before {
  transform: scale(1);
  opacity: 1;
}

/* On narrower viewports (phones, tablets, small laptops) the 40vw bloom
   plate feels cramped — let it span the full width of the viewport, minus
   the inset-corner gutters. Stays anchored to the bottom-left corner; only
   the width changes. The resting (non-bloomed) label is untouched. */
@media (max-width: 960px) {
  .species.is-blooming {
    width: calc(100vw - 2 * var(--inset-corner));
    max-width: none;
  }
}

/* Resting label — vernacular only, small-caps tracked, white with deep shadow.
   Absolutely pinned to the plate's bottom-left corner (~22px from left, ~18px
   from bottom) so the bloomed title can take normal flow. Bloom is a pure
   opacity crossfade between this and .line-name--bloom — no font-size /
   letter-spacing / color / text-shadow / padding morph, which was the page's
   only CLS and a stack of nine non-composited transitions per open/close. */
.species .line-name--rest {
  position: absolute;
  left: 8px;
  bottom: 4px;
  pointer-events: auto;
  cursor: pointer;
  display: inline-block;
  white-space: nowrap;
  font-weight: 500;
  font-size: 11px;
  color: #ffffff;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  line-height: 1.3;
  text-shadow:
    0 1px 4px rgba(0, 0, 0, 0.55),
    0 0 1px rgba(0, 0, 0, 0.85);
  opacity: 1;
  transition:
    opacity var(--label-in) ease,
    padding-left 520ms cubic-bezier(0.16, 1, 0.3, 1);
}
.species.is-blooming .line-name--rest {
  opacity: 0;
  pointer-events: none;
}

/* Bloomed title — vernacular display over italic binomial. In normal flow so
   the plate ::before sizes to it; hidden (opacity 0) until bloom, then fades in
   as the resting label fades out and the plate scales open. */
.species .line-name--bloom {
  display: block;
  pointer-events: auto;
  cursor: pointer;
  font-size: 26px;
  font-weight: 600;
  color: var(--charcoal);
  letter-spacing: -0.018em;
  line-height: 1.0;
  opacity: 0;
  transition: opacity var(--label-in) ease;
}
.species.is-blooming .line-name--bloom { opacity: 1; }
.species:not(.is-blooming) .line-name--bloom { pointer-events: none; }
.species .line-name--bloom .latin {
  display: block;
  font-style: italic;
  color: var(--blue);
  font-weight: 400;
  font-size: 17px;
  line-height: 1.0;
  letter-spacing: 0.002em;
  margin-top: 6px;
}

.species .latin { display: none; }
.species .line-lede,
.species .line-vitals,
.species .line-fact { display: none; }

/* Dwell cue — the logo glyph fades in beside the resting label after idle and
   on hover; the label shifts right to reveal it. Lives only on the resting
   label (the bloomed title leads cleanly without it). */
.species .line-name--rest::before {
  content: '';
  position: absolute;
  left: 0;
  top: 50%;
  width: 18px;
  height: 18px;
  background: url('files/logo.svg') no-repeat center / contain;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
  opacity: 0;
  transform: translate(-4px, -50%) scale(0.88);
  transform-origin: left center;
  transition:
    opacity 520ms cubic-bezier(0.16, 1, 0.3, 1),
    transform 520ms cubic-bezier(0.16, 1, 0.3, 1);
  pointer-events: none;
  will-change: opacity, transform;
}
.species:not(.is-blooming) .line-name--rest:hover {
  padding-left: 26px;
}
.species:not(.is-blooming) .line-name--rest:hover::before {
  opacity: 0.65;
  transform: translate(0, -50%) scale(1);
}

/* Idle invitation: the resting label stays solid for most of each
   cycle and softens briefly to draw the eye, then returns. The long
   solid hold (0%–60%) plus a 1800ms idle threshold means quick
   browsing never sees the cue — it only appears once you've truly
   lingered on a photo. Suppressed on hover and during bloom. */
.species.is-dwell:not(.is-blooming) .line-name--rest:not(:hover) {
  animation: speciesInvite 4000ms ease-in-out infinite;
}
@keyframes speciesInvite {
  0%, 60%, 100% { opacity: 1;   }
  80%           { opacity: 0.6; }
}

/* Lede — full width inside the plate, no max-width. Hairline above. */
.species.is-blooming .line-lede {
  display: block;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid rgba(26, 26, 26, 0.18);
  font-size: 12px;
  line-height: 1.5;
  color: #2a2a2a;
}

/* Vitals strip — flex row of stat columns. */
.species.is-blooming .line-vitals {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 22px;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid rgba(26, 26, 26, 0.18);
}
.species.is-blooming .line-vitals .stat {
  display: flex;
  flex-direction: column;
}
.species.is-blooming .line-vitals .k {
  font-size: 7.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(26, 26, 26, 0.55);
  font-weight: 500;
  line-height: 1.0;
}
.species.is-blooming .line-vitals .v {
  font-size: 11.5px;
  line-height: 1.1;
  margin-top: 2px;
  font-weight: 500;
  color: var(--charcoal);
}

/* Fun-fact pull — italic body with 2px blue rule on the left. */
.species.is-blooming .line-fact {
  display: block;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid rgba(26, 26, 26, 0.18);
}
.species.is-blooming .line-fact .fact-body {
  display: block;
  padding-left: 12px;
  border-left: 2px solid var(--blue);
  font-style: italic;
  font-size: 12px;
  line-height: 1.5;
  color: #2a2a2a;
}

/* ============================================================
   Compass — quiet pan affordance bottom-right
   ============================================================ */
.compass {
  position: fixed;
  bottom: 22px;
  right: 22px;
  z-index: 10;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--blue-soft);
  text-align: right;
  line-height: 1.5;
  opacity: 0;
  animation: fadeIn 200ms ease-out 280ms forwards;
}
.compass .arr {
  color: var(--blue);
  font-weight: 500;
  letter-spacing: 0.12em;
}

@keyframes fadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* ============================================================
   Full-screen viewer — top-right toggle + the plate it opens
   The toggle borrows the bloom plate's surface (cream field,
   hairline, --radius-card, soft lift) so the two corners read as
   one family. Open or closed it holds the same spot; only the
   glyph crossfades, so the control the visitor pressed is the
   control that closes.
   ============================================================ */
.fs-toggle {
  position: fixed;
  top: var(--inset-corner);
  right: var(--inset-corner);
  z-index: 40;
  width: 38px;
  height: 38px;
  display: grid;
  place-items: center;
  padding: 0;
  background: var(--field);
  border: 1px solid rgba(26, 26, 26, 0.22);
  border-radius: var(--radius-card);
  box-shadow: 0 6px 24px -8px rgba(0, 0, 0, 0.22);
  color: var(--charcoal);
  cursor: pointer;
  opacity: 0;
  animation: fadeIn 200ms ease-out 280ms forwards;
  transition: background var(--label-in) ease;
}
.fs-toggle:hover { background: #fff; }
.fs-toggle:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
}
/* Both glyphs share one grid cell so the swap costs no layout. */
.fs-toggle .fs-ico {
  grid-area: 1 / 1;
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-linecap: square;
  transition: opacity var(--label-in) ease;
}
.fs-toggle .fs-ico--close          { opacity: 0; }
.fs-toggle.is-open .fs-ico--expand { opacity: 0; }
.fs-toggle.is-open .fs-ico--close  { opacity: 1; }

/* ============================================================
   Collection switcher — birds / events / products, right edge
   A vertical column of *individual* plates hanging down the
   right-hand side, under the viewer toggle. Each button carries
   its own cream field, hairline, radius and lift — the same
   surface family as the toggle and the bloom plate — rather than
   sharing one strip, so the three read as three separate marks
   against the photo plane. They hang from the right (align-items:
   flex-end), so each one is only as wide as its own label and the
   ragged edge falls inward, away from the viewport edge.
   The active button inverts to the brand blue.
   ============================================================ */
.collections {
  position: fixed;
  /* Clear the 38px toggle plus a gutter, so the two never overlap. */
  top: calc(var(--inset-corner) + 38px + 8px);
  right: var(--inset-corner);
  z-index: 40;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 6px;
  opacity: 0;
  animation: fadeIn 200ms ease-out 280ms forwards;
}
.collections .coll-btn {
  appearance: none;
  background: var(--field);
  border: 1px solid rgba(26, 26, 26, 0.22);
  border-radius: var(--radius-card);
  box-shadow: 0 6px 24px -8px rgba(0, 0, 0, 0.22);
  padding: 0 12px;
  height: 32px;
  font: inherit;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(26, 26, 26, 0.55);
  cursor: pointer;
  white-space: nowrap;
  transition:
    background var(--label-in) ease,
    border-color var(--label-in) ease,
    color var(--label-in) ease;
}
.collections .coll-btn:hover { color: var(--charcoal); background: #fff; }
.collections .coll-btn.is-active {
  background: var(--blue);
  border-color: var(--blue);
  color: #fff;
}
.collections .coll-btn:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
}

.fullscreen {
  position: fixed;
  inset: 0;
  z-index: 15;
  background: rgba(12, 12, 12, 0.94);
  opacity: 0;
  transition: opacity var(--label-out) ease-in;
  /* Every touch gesture over the viewer is ours (app.js reads swipes as steps
     between photographs), so hand none of them to the browser — no scroll, no
     pull-to-refresh, no double-tap zoom stealing the swipe. Same reason .stage
     sets it. */
  touch-action: none;
}
/* [hidden] is a UA rule; the class selector above outranks it, so restate it. */
.fullscreen[hidden] { display: none; }
.fullscreen.is-open {
  opacity: 1;
  transition: opacity var(--label-in) ease-out;
}
/* Fit to screen — the whole frame is always visible, capped by whichever axis
   runs out first: a wide photograph meets the left and right edges, an upright
   one meets the top and bottom. `contain` letterboxes the slack axis against the
   backdrop; no inset and no radius, so the photograph reaches the edges it can.

   The box is pinned to .fullscreen's inset rather than centred in a grid track.
   A grid row here is auto-sized *from the image*, which makes `height: 100%` a
   cyclic percentage — it resolved to the natural height, so anything taller than
   the viewport overflowed and got cropped instead of being capped by it.
   Absolute inset gives both percentages a definite box: the viewport. */
.fullscreen .fs-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;       /* object-position defaults to 50% 50% — centred */
  user-select: none;
  -webkit-user-drag: none;
  /* A held finger on an image raises iOS's save/copy callout, which eats the
     start of a swipe. The plane's photographs are backgrounds and never had the
     problem; this one is a real <img>. */
  -webkit-touch-callout: none;
  /* Only the snap-back rides this. While the finger is down the offset must
     track it frame for frame, so .is-swiping switches the transition off. */
  transition: transform var(--label-out) ease-out;
}
.fullscreen.is-swiping .fs-img { transition: none; }

/* ============================================================
   Mobile placeholder
   ============================================================ */
.mobile-edition {
  display: none;
  position: fixed;
  inset: 0;
  background: var(--field);
  padding: 18px;
  overflow-y: auto;
  z-index: 20;
}
/* Wordmark left, current-set count right. Full-width flex rather than the old
   inline-flex so the count can sit at the far end and so the gradient masks the
   whole strip the photos scroll under. The right padding is the corner control's
   footprint (--inset-corner + 44px button, less the edition's own 18px padding)
   plus breathing room, so the count can never slide under the button. */
.mobile-edition .mobile-identity {
  position: sticky;
  top: 0;
  padding: 10px 50px 18px 0;
  background: linear-gradient(to bottom, var(--field) 60%, transparent);
  font-size: var(--wm-size-m);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  color: var(--charcoal);
  font-weight: 500;
  z-index: 2;
}
.mobile-edition .mobile-mark {
  display: inline-flex;
  align-items: center;
  gap: 9px;
}
.mobile-edition .mobile-identity .pictogram { width: 14px; height: 14px; }
/* "Where am I" — written by JS from the same data-set the CSS filters on, so
   the header and the menu cannot drift apart. */
.mobile-edition .mobile-count {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(26, 26, 26, 0.5);
  white-space: nowrap;
}
.mobile-edition .mobile-note {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 11px;
  color: rgba(26, 26, 26, 0.5);
  letter-spacing: 0.04em;
  margin: 8px 0 24px;
  line-height: 1.5;
}
.mobile-edition .mcell { margin-bottom: 32px; }
.mobile-edition .mcell .mphoto {
  position: relative;
  background:
    repeating-linear-gradient(0deg,
      var(--ph-band-a, #d9d3c5) 0 14px,
      var(--ph-band-b, #cfc7b6) 14px 28px);
  display: grid;
  place-items: center;
  overflow: hidden;
}
.mobile-edition .mcell .mphoto img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  user-select: none;
  -webkit-user-drag: none;
}
.mobile-edition .mcell .mphoto:has(img) span { display: none; }
.mobile-edition .mcell .mphoto span {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 10px;
  letter-spacing: 0.06em;
  color: rgba(26, 26, 26, 0.5);
  text-transform: uppercase;
  padding: 4px 8px;
  background: rgba(242, 238, 229, 0.85);
}
.mobile-edition .mcell .mlabel {
  margin-top: 10px;
  font-size: var(--label-size-m);
  font-weight: 500;
  color: var(--charcoal);
}
.mobile-edition .mcell .mlabel .latin { font-style: italic; color: var(--blue); margin-left: 0.4em; font-weight: 400; }

/* Mobile bloom — per-cell <details> with the same lede / vitals / fun-fact
   content as the desktop bloom plate, anchored inline rather than floating.
   The species label is moved into the summary (see populateMobileBloom), so
   the label row itself is the toggle: label left, "Read more" right, one
   shared underline. Open state animates height via interpolate-size where
   supported. */
.mobile-edition .mcell .mbloom summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
  margin-top: 10px;
  padding-bottom: 8px;
  border-bottom: 1px solid rgba(26, 26, 26, 0.18);
  user-select: none;
  -webkit-user-select: none;
}
.mobile-edition .mcell .mbloom summary::-webkit-details-marker { display: none; }
.mobile-edition .mcell .mbloom summary .mlabel { margin-top: 0; }
.mobile-edition .mcell .mbloom .mbloom-cta {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--blue);
  font-weight: 500;
  white-space: nowrap;
}
.mobile-edition .mcell .mbloom .mbloom-cta::before {
  content: '+';
  display: inline-block;
  width: 0.7em;
  margin-right: 5px;
  font-size: 14px;
  line-height: 1;
  text-align: center;
}
.mobile-edition .mcell .mbloom .mbloom-cta::after { content: 'Read more'; }
.mobile-edition .mcell .mbloom[open] .mbloom-cta::before { content: '−'; }
.mobile-edition .mcell .mbloom[open] .mbloom-cta::after { content: 'Close'; }

.mobile-edition .mcell .mbloom-body {
  padding-top: 12px;
}
.mobile-edition .mcell .mbloom .mlede {
  font-size: 13px;
  line-height: 1.55;
  color: #2a2a2a;
}
.mobile-edition .mcell .mbloom .mvitals {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 22px;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid rgba(26, 26, 26, 0.18);
}
.mobile-edition .mcell .mbloom .mvitals .stat {
  display: flex;
  flex-direction: column;
}
.mobile-edition .mcell .mbloom .mvitals .k {
  font-size: 8px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(26, 26, 26, 0.55);
  font-weight: 500;
  line-height: 1.0;
}
.mobile-edition .mcell .mbloom .mvitals .v {
  font-size: 12px;
  line-height: 1.1;
  margin-top: 3px;
  font-weight: 500;
  color: var(--charcoal);
}
.mobile-edition .mcell .mbloom .mfact {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid rgba(26, 26, 26, 0.18);
}
.mobile-edition .mcell .mbloom .mfact .fact-body {
  display: block;
  padding-left: 12px;
  border-left: 2px solid var(--blue);
  font-style: italic;
  font-size: 13px;
  line-height: 1.5;
  color: #2a2a2a;
}

/* Smooth open/close where the engine supports animating to/from `auto`.
   Chromium 131+, Firefox 137+, Safari 18.2+. Older engines: instant toggle. */
@supports (interpolate-size: allow-keywords) {
  .mobile-edition .mcell .mbloom::details-content {
    block-size: 0;
    overflow: clip;
    opacity: 0;
    transition:
      block-size 280ms cubic-bezier(0.2, 0.8, 0.2, 1),
      opacity 200ms ease,
      content-visibility 280ms allow-discrete;
    interpolate-size: allow-keywords;
  }
  .mobile-edition .mcell .mbloom[open]::details-content {
    block-size: auto;
    opacity: 1;
  }
}

/* ============================================================
   Mobile set switcher
   One attribute drives everything: .mobile-edition[data-set] vs
   [data-cat] on each cell. Switching sets is an attribute write —
   no re-render, no rebuild, no reflow of anything but visibility.

   Because the inactive cells are display:none they generate no box,
   never intersect the viewport, and so their loading="lazy" images
   are never fetched. An unvisited set costs nothing.
   ============================================================ */
.mobile-edition[data-set="birds"]    [data-cat]:not([data-cat="birds"]),
.mobile-edition[data-set="products"] [data-cat]:not([data-cat="products"]),
.mobile-edition[data-set="events"]   [data-cat]:not([data-cat="events"]),
.mobile-edition[data-set="portraits"] [data-cat]:not([data-cat="portraits"]),
.mobile-edition[data-set="lifestyle"] [data-cat]:not([data-cat="lifestyle"]),
.mobile-edition[data-set="architecture"] [data-cat]:not([data-cat="architecture"]) {
  display: none;
}

/* The corner control. Same family as .fs-toggle — same corner, same plate,
   same "the button you pressed is the button that closes" glyph swap — but
   44px, the touch minimum, because this one is only ever tapped.
   Hidden by default; the mobile breakpoint below turns it on. */
.set-toggle {
  display: none;
  position: fixed;
  top: var(--inset-corner);
  right: var(--inset-corner);
  z-index: 41;
  width: 44px;
  height: 44px;
  place-items: center;
  padding: 0;
  background: var(--field);
  border: 1px solid rgba(26, 26, 26, 0.22);
  border-radius: var(--radius-card);
  box-shadow: 0 6px 24px -8px rgba(0, 0, 0, 0.22);
  color: var(--charcoal);
  cursor: pointer;
  transition: background var(--label-in) ease;
}
.set-toggle:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
}
/* Both glyphs share one grid cell so the swap costs no layout. */
.set-toggle .set-ico {
  grid-area: 1 / 1;
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-linecap: square;
  transition: opacity var(--label-in) ease;
}
.set-toggle .set-ico--close        { opacity: 0; }
.set-toggle.is-open .set-ico--open  { opacity: 0; }
.set-toggle.is-open .set-ico--close { opacity: 1; }

/* Catches the tap that dismisses. Barely tinted — the photographs stay
   readable behind it; it is there to be pressed, not to be looked at. */
.set-backdrop {
  position: fixed;
  inset: 0;
  z-index: 38;
  background: rgba(26, 26, 26, 0.12);
  opacity: 0;
  transition: opacity var(--label-out) ease-in;
}
.set-backdrop[hidden] { display: none; }
.set-backdrop.is-open {
  opacity: 1;
  transition: opacity var(--label-in) ease-out;
}

/* Directly beneath the button, hung from the same right edge, so it reads as
   having come out of the control that opened it. */
.set-menu {
  position: fixed;
  top: calc(var(--inset-corner) + 44px + 8px);
  right: var(--inset-corner);
  z-index: 40;
  min-width: 176px;
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  background: var(--field);
  border: 1px solid rgba(26, 26, 26, 0.22);
  border-radius: var(--radius-card);
  box-shadow: 0 12px 32px -10px rgba(0, 0, 0, 0.3);
  opacity: 0;
  transform: translateY(-6px) scale(0.97);
  transform-origin: top right;
  transition: opacity var(--label-out) ease-in, transform var(--label-out) ease-in;
}
.set-menu[hidden] { display: none; }
.set-menu.is-open {
  opacity: 1;
  transform: none;
  transition: opacity var(--label-in) ease-out, transform var(--label-in) var(--focus-easing);
}

/* Stacked, left-aligned, the count trailing quietly on the right. */
.set-opt {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
  width: 100%;
  min-height: 40px;
  padding: 9px 11px;
  border: 0;
  border-radius: 5px;
  background: none;
  font-family: inherit;
  font-size: var(--label-size-m);
  font-weight: 500;
  text-align: left;
  color: var(--charcoal);
  cursor: pointer;
  transition: background var(--label-out) ease;
}
.set-opt .set-n {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.06em;
  color: rgba(26, 26, 26, 0.45);
}
.set-opt:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: -2px;
}
/* The one you are looking at is filled. */
.set-opt[aria-current="true"] {
  background: var(--blue);
  color: var(--field);
}
.set-opt[aria-current="true"] .set-n { color: rgba(242, 238, 229, 0.7); }

/* Below the mobile breakpoint: hide the spatial plane and show the
   editorial scroll instead. Mobile visitors get the curated sequence;
   AT users get a properly-structured list with real headings + alts. */
@media (max-width: 720px) {
  .stage, .identity, .species, .compass,
  .fs-toggle, .fullscreen, .collections { display: none; }
  .mobile-edition { display: block; }
  .set-toggle { display: grid; }
}

/* The switcher is a mobile-only control: above the breakpoint the plane is the
   navigation, so the panel must never appear even if a stale open state or a
   resize mid-gesture left the class on. */
@media (min-width: 721px) {
  .set-toggle, .set-backdrop, .set-menu { display: none !important; }
}

/* ============================================================
   Semantic markup neutralizers
   <h1> wordmark + mobile-name should look identical to the
   former <span><span></span></span> structure. Zero default
   heading margins and inherit type from the parent.
   ============================================================ */
.identity .wordmark {
  font: inherit;
  margin: 0;
  display: inline;
}
.mobile-name {
  font: inherit;
  margin: 0;
  display: inline;
}
.mobile-name .name { color: var(--charcoal); }
.mobile-name .dot  { color: var(--blue); }

/* ============================================================
   Image failure fallback — only ever visible if a .webp 404s
   ============================================================ */
.photo.img-failed::after {
  content: attr(data-species);
  position: absolute;
  inset: auto 8px 8px 8px;   /* override the veil's inset:0 → bottom strip */
  background: none;          /* override the veil fill */
  opacity: 1;                /* override the veil's dim opacity so the label reads */
  z-index: 3;
  color: rgba(255, 255, 255, 0.7);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  pointer-events: none;
}

/* ============================================================
   404 page — minimal layout for /404.html only
   ============================================================ */
.notfound {
  display: grid;
  place-content: center;
  min-height: 100vh;
  gap: 1rem;
  padding: 2rem;
  text-align: center;
}
.notfound a { color: var(--blue); }
.notfound p { font-size: 14px; }

/* ============================================================
   Print / reduced motion
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  .photo,
  .photo::before,
  .photo::after,
  .species,
  .species::before,
  .species .line-name,
  .fs-toggle,
  .fs-toggle .fs-ico,
  .collections .coll-btn,
  .fullscreen,
  .fullscreen .fs-img,
  .set-toggle,
  .set-toggle .set-ico,
  .set-backdrop,
  .set-menu,
  .set-opt,
  .mobile-edition .mcell .mbloom::details-content { transition-duration: 0ms; animation-duration: 0ms; }
}
