/* ============================================================
   styles.css — shared base + index/home styles
   Loaded by every page. Index also lives here (it has no
   category-specific stylesheet; its rules are scoped to body.home).
   ============================================================ */

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
  --bg: #0e0d0c;
  --bg2: #161412;
  --text: #d4cfc9;
  --muted: #8f8980;
  --accent: #c8a96e;
  --white: #f0ece6;
  --border: rgba(255,255,255,0.07);
  --focus: rgba(240,236,230,0.9);   /* :focus-visible outline — ≈16:1 on --bg */
  --col-gap: 10px;
}
html, body { background: var(--bg); color: var(--text); font-family: 'Geist Mono', monospace; min-height: 100vh; }

/* ---- default page header (gallery + essay pages) ---- */
header {
  /* eyebrow ABOVE a display title, the capsule shape — it used to sit beside
     a 22px title in one baseline row. */
  padding: 72px 40px 30px;
  border-bottom: 1px solid var(--border);
  display: flex; flex-direction: column; align-items: flex-start; gap: 16px;
}
.header-eyebrow { display: flex; align-items: baseline; gap: 20px; }
.header-meta    { display: flex; align-items: flex-start; gap: 14px; margin-left: 0; }
.back {
  font-size: 11px; color: var(--muted); text-decoration: none;
  letter-spacing: 0.12em; text-transform: uppercase;
  transition: color 0.2s; white-space: nowrap;
}
.back:hover { color: var(--white); }
.ep-label { font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--accent); }
.ep-title {
  /* was 22px, right-aligned into a flex row beside the count and the date — the
     weakest type on the site, on the pages a stranger actually lands on. Now it
     owns its line at the same fluid scale the capsules use. */
  /* 🔴 NOT var(--serif) — that token belongs to the trips family and is not
     defined in this stylesheet, so the declaration silently fell back to the
     inherited mono and the title rendered as 72px Geist Mono. */
  font-family: 'Cormorant Garamond', serif;
  font-weight: 400;
  /* Was clamp(34px, 6vw, 72px) — the same overshoot the home wordmark had, and
     on a capsule page it was worse: no "Justin Fu" here, so a 72px title was
     the loudest thing above a set of photographs. Same scale as the wordmark
     now, so the front door and a capsule speak at one volume. */
  font-size: clamp(27px, 4vw, 44px);
  line-height: 1.02;
  letter-spacing: -0.015em;
  color: var(--white);
}
.header-meta { flex-direction: column; align-items: flex-start; gap: 3px; }
.header-meta .ep-count, .header-meta .ep-date { font-size: 11px; }
.ep-count { font-size: 11px; color: var(--muted); letter-spacing: 0.1em; white-space: nowrap; }
.ep-date {
  font-size: 11px; color: var(--muted); letter-spacing: 0.1em; white-space: nowrap;
  /* 🔴 opacity:0.85 blended --muted (5.60:1 on its own) down to 4.33:1 — under
     AA for 11px. The COLOUR was never the problem, which is why grepping for a
     hex found nothing: the failing value only exists after compositing. 0.9
     gives 4.75:1 and is visually indistinguishable here. */
  font-style: italic; opacity: 0.9;
}
/* the "·" prefix the date used to carry was for the one-line header, where it
   separated the date from the count. The meta stacked into a column and the
   bullet dangled at the start of its own line — removed (operator, 2026-07-29). */

/* ---- shared cross-capsule nav (rendered by capsule-nav.js, sits above footer) ---- */
.capsule-nav {
  width: 100%; max-width: 1180px; margin: 0 auto;
  box-sizing: border-box; padding: 72px 40px 26px;
  display: flex; justify-content: space-between; align-items: flex-start; gap: 32px;
}
.capsule-nav.solo { justify-content: center; }
.cnav { text-decoration: none; display: flex; align-items: center; gap: 16px; max-width: 46%; }
.cnav-text { display: flex; flex-direction: column; gap: 7px; }
.cnav-prev { flex-direction: row; }
.cnav-prev .cnav-text { align-items: flex-start; text-align: left; }
.cnav-next { flex-direction: row-reverse; margin-left: auto; }
.cnav-next .cnav-text { align-items: flex-end; text-align: right; }
.cnav picture { display: block; }
.cnav-cover { width: 96px; height: 64px; object-fit: cover; flex-shrink: 0; opacity: 0.7; transition: opacity .3s ease; }
.cnav:hover .cnav-cover { opacity: 1; }
.cnav-dir {
  font-family: 'Geist Mono', monospace; font-size: 10px; letter-spacing: 0.24em;
  text-transform: uppercase; color: var(--muted); transition: color .3s ease;
}
.cnav-title {
  font-family: 'Cormorant Garamond', serif; font-weight: 300; font-size: 27px;
  line-height: 1.05; color: var(--text); transition: color .3s ease;
}
.cnav:hover .cnav-dir   { color: var(--accent); }
.cnav:hover .cnav-title { color: var(--white); }

/* ---- shared footer ---- */
footer {
  border-top: 1px solid var(--border); padding: 22px 40px;
  display: flex; justify-content: space-between; align-items: center;
}
footer span { font-size: 11px; color: var(--muted); letter-spacing: 0.1em; }
/* 🔴 ONE FOOTER LANGUAGE. Measured before: the root pages had an 11px MONO
   wordmark in a 63px bar, /eyes had 15px mono centred in a 166px bar because
   its own footer CSS targets .mark/.sub/.back and its markup is span/span so
   nothing matched, and the trips family had a 22px CORMORANT wordmark. Three
   languages on one site. Now: Cormorant wordmark, mono nav, everywhere. */
footer span:first-child {
  font-family: 'Cormorant Garamond', serif;
  font-size: 15px;
  /* Gold was tried here and declined — the scale and the fade were the fix, the
     colour was not. Recorded because the a11y question it raised has a settled
     answer: --accent #c8a96e is 8.65:1 on --bg, past AA (4.5) and past AAA
     (7.0), so gold was never blocked on contrast and does not need re-testing
     if it comes up again. The gold that DOES fail is --gold-dim #8a6d38 at
     4.00:1 — AA-large only, never a text colour. */
  color: var(--white);
}
footer a { color: inherit; text-decoration: none; border-bottom: 1px solid transparent; transition: color 0.2s, border-color 0.2s; }
footer a:hover { color: var(--white); border-color: var(--muted); }

/* The view list has grown to six links. Beside the wordmark it collides and
   wraps mid-list, which reads as a spill rather than a block -- measured at
   560px, where the links start within 8px of the wordmark. Below 600px, stack
   them and let the list wrap on its own terms. */
@media (max-width: 700px) {
  /* 🔴 ONE ROW, which costs three links. Measured at 390px: 350px available,
     and the wordmark plus all seven links needs 605px. Nothing legible closes
     that — 10px still overflows by 34px, and 10px is the floor this file
     already raised text TO for WCAG and Apple's HIG. So the tail hides instead
     of the type shrinking.
     Kept: trips · eyes · selects · timeline (~336px with the wordmark).
     Hidden below 600px: the roll, latent, colophon.
     The separators are bare TEXT NODES between the anchors, so `display:none`
     on a link would strand its " · ". Collapsing the span to font-size:0 and
     re-emitting the separator via `a + a::before` is what removes them —
     adjacent-sibling matching ignores text nodes, and a hidden link's ::before
     never renders. Do not "simplify" this back to hiding the anchors alone. */
  footer {
    flex-direction: row; align-items: baseline;
    justify-content: space-between; gap: 12px;
  }
  footer span:last-child {
    line-height: 1.6;
    font-size: 0;                 /* collapses the " · " text nodes */
    white-space: nowrap;
  }
  footer span:last-child a { font-size: 11px; }
  /* margins, not spaces: a leading space inside `content` collapses against the
     zero-sized original separator text node and the dot ends up hugging the
     previous word ("trips· eyes"). */
  footer span:last-child a + a::before {
    content: "·"; margin: 0 0.5em; color: var(--muted);
  }
  /* 🔴 POSITIONAL, not by href. Matching a[href="/about"] also hit the EPISODE
     footer, which is a different footer entirely — `justinfu.com` and a single
     `made with ricoh gr iv` link to /about — so the gallery pages lost their
     only nav item. nth-child(n+5) can only fire on the 7-link site nav; a
     one-link footer has no 5th child. `a + a::before` is self-scoping for the
     same reason. */
  footer span:last-child a:nth-child(n+5) { display: none; }
}

/* ---- timeline (/timeline) ---- */
.timeline { max-width: 1180px; margin: 0 auto; padding: 48px 40px 96px; }
.tl-month { margin-bottom: 44px; }
.tl-label { font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--accent); font-weight: 400; margin: 0 0 16px; }
.tl-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 16px; }
.tl-frame { text-decoration: none; display: block; }
.tl-frame img { width: 100%; aspect-ratio: 3/2; object-fit: cover; display: block; background: var(--bg2); }
.tl-cap { display: block; margin-top: 8px; font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); transition: color 0.2s; }
.tl-frame:hover .tl-cap { color: var(--white); }

/* ---- contact sheet (/roll) ---- */
.roll { padding: 40px 32px 96px; }
.roll-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; }
.roll-frame { position: relative; display: block; background: var(--bg2); text-decoration: none; }
.roll-frame img { width: 100%; aspect-ratio: 3/2; object-fit: cover; display: block; filter: contrast(1.04); transition: filter 0.2s; }
.roll-frame::after { content: ""; position: absolute; inset: 0; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.08); pointer-events: none; }
.roll-frame:hover img { filter: contrast(1.04) brightness(1.12); }
.roll-no {
  position: absolute; bottom: 4px; right: 5px;
  font-family: 'Geist Mono', monospace; font-size: 9px; letter-spacing: 0.06em;
  color: #e0a25a; text-shadow: 0 1px 2px rgba(0,0,0,0.85);
}

/* ---- colophon / about page ---- */
.colophon { max-width: 620px; margin: 0 auto; padding: 64px 40px 96px; }
.colophon .lede {
  font-family: 'Cormorant Garamond', serif; font-weight: 300; font-size: 27px;
  line-height: 1.4; color: var(--white); margin-bottom: 40px;
}
.colophon h2 {
  font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--accent); font-weight: 400; margin: 40px 0 12px;
}
.colophon p { font-size: 15px; line-height: 1.75; color: var(--text); margin-bottom: 14px; }
.colophon .registers { list-style: none; margin: 4px 0 0; padding: 0; }
.colophon .registers li { font-size: 15px; line-height: 1.7; color: var(--text); }
.colophon .registers b { color: var(--white); font-weight: 400; }

/* ---- shared mobile (≤540px) ---- */
@media (max-width: 540px) {
  /* two tiers: line 1 = back · capsule label (eyebrow),
     line 2 = title · count · date (meta, together) */
  header { padding: 22px 20px 18px; flex-direction: column; align-items: flex-start; gap: 9px; }
  .header-eyebrow { align-self: stretch; justify-content: space-between; } /* back left, capsule # right */
  .header-eyebrow .back, .header-eyebrow .ep-label { flex-shrink: 0; white-space: nowrap; }
  .header-meta { margin-left: 0; flex-wrap: wrap; row-gap: 4px; }
  header .ep-title { font-size: clamp(24px, 7vw, 32px); }
  footer { padding: 20px; }
  .capsule-nav { padding: 48px 20px 20px; gap: 18px; }
  .cnav-cover { display: none; }              /* two-up prev/next is tight on mobile */
  .cnav-title { font-size: 21px; }
  .colophon { padding: 40px 20px 64px; }
  .colophon .lede { font-size: 22px; }
  .timeline { padding: 40px 20px 64px; }
  .tl-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; }
  .roll { padding: 28px 14px 64px; }
  .roll-grid { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 6px; }
  .cnav { max-width: 48%; }
}

/* ============================================================
   index.html (body.home)
   ============================================================ */

body.home header {
  /* 🔴 The base `header` rule went COLUMN for the episode hero. The HOME
     header must stay a ROW at every width — that one-row treatment was an
     explicit request, and stacking it put "6 capsules" underneath a 72px
     name with line-height 1.0, overlapping it. */
  padding: 48px 48px 36px;
  flex-direction: row;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}
body.home .site-name {
  font-family: 'Cormorant Garamond', serif;
  /* 🔴 Display scale, but the header stays ONE ROW. The design review asked for
     the trips-family hero — a gold eyebrow ABOVE a clamp() display title — and
     that shape is two rows, which is exactly what was removed from this header
     by request. So take the fluid scale and not the stacking: the name grows,
     "6 capsules" stays on its baseline to the right. The front door announced
     itself at 22px while a sub-page said "Eyes" at 104px. */
  /* Was clamp(34px, 6vw, 72px). 72px was set to stop the front door announcing
     itself at 22px while /eyes said "Eyes" at 104px — a real problem, but it
     overshot: the name became the largest thing on a page of photographs. This
     keeps the fluid scale and the one-row shape and takes the ceiling down ~40%,
     which still clears the old 22px by more than double. */
  font-size: clamp(27px, 4vw, 44px);
  line-height: 1.0;
  font-weight: 300;
  color: var(--white);
  letter-spacing: 0.01em;
}
body.home .site-name em { color: var(--accent); font-style: italic; }

/* The seven lens links moved out of the footer and into the header on
   2026-07-29 — the homepage became a full-window hero with a caption band at
   the bottom, and a footer under the band would push the photograph off its
   own page. verify_footers.py knows the homepage's view-nav lives here. */
body.home .lens-nav { display: flex; gap: 26px; }
body.home .lens-nav a {
  font-size: 10px; letter-spacing: 0.24em; text-transform: uppercase;
  color: var(--muted); text-decoration: none; transition: color 0.2s;
  /* 🔴 24px minimum target height (WCAG 2.5.8, gated by verify_budget). The
     old footer nav was exempt as links-in-a-text-block; standalone flex
     anchors are not. Padding, not height: the header row is baseline-aligned
     and the box must grow around the text without moving it. */
  padding: 7px 0;
}
body.home .lens-nav a:hover { color: var(--white); }

body.home {
  display: flex;
  flex-direction: column;
  height: 100vh;
  height: 100dvh;   /* phones: the URL bar must not push the band off-screen */
  overflow: hidden;
}

/* ============================================================
   The hero: one full-window frame per capsule (2026-07-29).
   The identity never sits on the photograph — it sits under it, on the
   ground. The frame fills the stage object-fit: contain, NO filter and NO
   scrim (the old grid showed every cover at brightness(0.75)); every string
   in the caption band sits on solid --bg at its published contrast ratio,
   so a blown-white frame and a near-black frame need no per-frame handling.
   ============================================================ */
body.home main.hero { flex: 1; display: flex; flex-direction: column; min-height: 0; }

.stage { position: relative; flex: 1; min-height: 0; margin: 18px 48px 0; }
.stage-link { position: absolute; inset: 0; display: block; }
.hero-frame {
  position: absolute; inset: 0;
  opacity: 0;
  transition: opacity 420ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.hero-frame img {
  width: 100%; height: 100%;
  object-fit: contain; object-position: center;
  display: block;
}
/* The advance is a crossfade: the incoming frame (.current) fades in ABOVE
   the outgoing (.prev), which fades OUT underneath on the same clock.
   🔴 The prev must FADE, not hold — under `contain` two frames rarely share
   a box (Morning is 1.21:1, the card frames 1.5:1), and a held prev leaves
   the taller frame's uncovered strips at full opacity for the whole 420ms,
   then pops them off: the "ghost" the operator saw on 01→02 (2026-07-29).
   The strong ease-out bezier keeps the mid-fade luminance dip small on
   same-aspect advances. home.js drops .prev once the fade lands. */
.hero-frame.current { opacity: 1; z-index: 2; }
.hero-frame.prev    { opacity: 0; z-index: 1; }

/* ---- the caption band: label + title, day-line, arrows, index ---- */
.band {
  display: flex; align-items: flex-end; gap: 32px;
  /* 26px of ground under the band — at 18px the whole strip read as
     touching the screen edge (operator, 2026-07-29: "bottom feels too low") */
  padding: 18px 48px 26px;
}
.hero-caption { min-width: 0; }
.hero-label {
  font-size: 10px; letter-spacing: 0.24em; text-transform: uppercase;
  color: var(--accent); margin-bottom: 10px;
}
.hero-title {
  font-family: 'Cormorant Garamond', serif;
  font-weight: 300;
  font-size: clamp(27px, 4vw, 44px);   /* the capsule-page scale, on purpose:
                                          it BECOMES that title on entry */
  line-height: 1.1;
  letter-spacing: -0.015em;
  color: var(--white);
  white-space: nowrap;
}

/* ---- the day-line: an interrupted hairline, not dots. Each segment's length
   is proportional to the capsule's frame count (flex weight emitted by
   build_spine), so the uneven set is visible structure: High Sun really is
   the long middle of the day. Hit area is the full 44px column; the visible
   hairline is the ::after. Height + luminance change together — never color
   alone. ---- */
.day-line {
  /* gap was 2px (the grid gutter, quoted) and the line read as ONE unbroken
     hairline — nobody saw six segments in it (operator, 2026-07-29). 6px of
     interruption is what makes it a count. */
  display: flex; gap: 6px;
  flex: 1 1 auto; max-width: 380px;
  align-self: flex-end;
}
/* 🔴 The segment floor was designed at 18px; 24px is WCAG 2.5.8's minimum
   target width and verify_budget gates a11y at 100, so the floor moved up.
   Costs Blue Hour (3 of 36 frames) a little more overstatement, buys it a
   target a thumb can actually hit — which was the floor's purpose. */
.day-line a { position: relative; display: block; height: 44px; min-width: 24px; }
.day-line a::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 8px;
  height: 2px; background: var(--muted);   /* 5.60:1 — clears the 3:1 non-text floor */
  transition: height 260ms cubic-bezier(0.22, 0.61, 0.36, 1),
              background-color 260ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.day-line a:hover::after { background: var(--text); }
.day-line a[aria-current="step"]::after { height: 4px; background: var(--white); }

.hero-controls { display: flex; align-items: center; gap: 4px; margin-left: auto; }
.adv {
  width: 44px; height: 44px; padding: 0;
  background: none; border: 0; cursor: pointer;
  font-family: 'Geist Mono', monospace; font-size: 15px;
  color: var(--text); transition: color 0.2s;
}
.adv:hover { color: var(--white); }
.adv[aria-disabled="true"] { color: rgba(212,207,201,0.35); cursor: default; }
.index-toggle {
  /* a bordered pill, not bare muted text — "index link not noticeable"
     (operator, 2026-07-29). 32px tall clears the 24px target floor. */
  height: 32px; padding: 0 14px; margin-left: 10px;
  background: none; cursor: pointer;
  border: 1px solid var(--border); border-radius: 2px;
  font-family: 'Geist Mono', monospace;
  font-size: 10px; letter-spacing: 0.24em; text-transform: uppercase;
  color: var(--text); transition: color 0.2s, border-color 0.2s;
  align-self: center;
}
.index-toggle:hover { color: var(--white); border-color: var(--muted); }
/* the controls are inert without JS; don't advertise them */
html:not(.js) .hero-controls { display: none; }

body.home :focus-visible { outline: 1px solid var(--focus); outline-offset: 3px; }

/* ---- the index view: one Binet-style row of six small covers that REPLACES
   the stage content (never overlays the photograph — an overlay is a scrim by
   another name). Real <a> markup, in the document even when closed: six
   crawlable capsule links with JS off. Label + title sit BENEATH each cover,
   on the ground — nothing is written on the pictures here either. ---- */
body.home .index-grid {
  position: absolute; inset: 0;
  display: none;
  /* 🔴 COUNT-AWARE, not hardcoded. This was `repeat(6, 1fr)` for the six
     capsules that existed when it was written; the 2026-07-29 recut made it
     eight and the row wrapped 6 + 2, which reads as broken. build_spine emits
     --index-cols (n if n <= 6, else ceil(n/2)) so the row stays balanced at
     any capsule count. The fallback keeps a JS-less/older page sane. */
  grid-template-columns: repeat(var(--index-cols, 4), 1fr);
  gap: 2px 2px;
  align-content: center;
  overflow-y: auto;
}
body.home.index-open .index-grid { display: grid; }
body.home.index-open .stage-link { display: none; }
.episode-card { display: block; text-decoration: none; min-width: 0; }
.episode-card picture { display: block; }
.episode-card img {
  width: 100%; height: auto; aspect-ratio: 3/2;
  object-fit: cover; display: block; background: var(--bg2);
}
.episode-info { padding-top: 14px; }
.episode-info .ep-label {
  font-family: 'Geist Mono', monospace;
  font-size: 10px; letter-spacing: 0.24em; text-transform: uppercase;
  color: var(--accent); margin-bottom: 6px;
}
.episode-info .ep-title {
  font-family: 'Cormorant Garamond', serif;
  font-size: 20px; font-weight: 300; line-height: 1.2;
  color: var(--text); transition: color 0.2s;
}
.episode-card:hover .ep-title { color: var(--white); }

/* ---- phone (≤700px): letterbox, by design — the same print-on-ground
   grammar, with generous dark air. All seven lens links stay visible, wrapped
   to two lines; that is the whole price. ---- */
@media (max-width: 700px) {
  body.home header {
    padding: 24px 20px 0;
    flex-direction: column;
    align-items: flex-start;
    gap: 14px;
  }
  body.home .site-name { font-size: clamp(22px, 6.5vw, 27px); }
  /* 🔴 ONE row of lenses (operator, 2026-07-29 — he cut /latent, then /eyes
     and /about, to buy it). Four links fit at full tracking on any phone
     ≥320px wide; the wrap stays as a last-resort fallback (with 10px
     row-gap: the 24px targets must not overlap). */
  body.home .lens-nav {
    flex-wrap: wrap;
    column-gap: clamp(12px, 2vw, 18px);
    row-gap: 10px;
  }
  .stage { margin: 12px 20px 0; }
  /* 🔴 WHERE THE LETTERBOX AIR GOES. A 3:2 frame at 350px wide is 234px tall
     in a ~464px stage: ~230px of air exists no matter what, and object-position
     only decides which side gets it. 72% (2026-07-29 round 2) put 178px above
     the photo and 64px below — a void under the header, which is what the
     operator saw. 34% inverts it: ~78 above, ~152 below, so the frame sits
     just under the chrome and the air becomes the gap before the caption.
     Measured, not guessed — see curator/verify_home.py, which now asserts the
     top gap stays the smaller of the two. */
  .stage .hero-frame img { object-position: center 34%; }
  /* 34px of ground: on a phone the browser's own bottom chrome overlaps
     anything closer to the edge (operator screenshot, 2026-07-29 — Safari's
     bar sat ON the day-line at 14px) */
  .band { flex-wrap: wrap; gap: 0 12px; padding: 12px 20px 34px; }
  .hero-caption { flex: 1 0 100%; margin-bottom: 6px; }
  .day-line { max-width: none; }
  body.home.index-open .index-grid { grid-template-columns: repeat(2, 1fr); gap: 20px 2px; }
}

/* ============================================================
   Entry: cross-document view transitions — CSS only, progressive
   enhancement. The hero frame IS the capsule's first gallery frame: it
   glides from the stage into its masonry cell while the root crossfades,
   and the homepage title glides into the capsule H1 (same type scale, same
   reason). Browsers without cross-document VT get a normal navigation into
   the existing LQIP blur-up. Scoped with :has(#gallery) so /timeline and
   /roll headings never pair with the hero title; every VT-capable browser
   has :has.
   ============================================================ */
@view-transition { navigation: auto; }
body.home .hero-frame.current img { view-transition-name: capsule-cover; }
body.home .hero-title             { view-transition-name: capsule-title; }
body:has(#gallery) .masonry .photo-item:first-child img { view-transition-name: capsule-cover; }
body:has(#gallery) h1.ep-title    { view-transition-name: capsule-title; }
::view-transition-group(capsule-cover),
::view-transition-group(capsule-title) {
  animation-duration: 480ms;
  animation-timing-function: cubic-bezier(0.22, 0.61, 0.36, 1);
}
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
  .hero-frame, .day-line a::after { transition: none; }
}

/* ============================================================
   The chrome arrives after the photographs.
   The wordmark and the nav fade up once the grid has painted, so the first
   thing the page resolves into is the pictures and not the name on them.
   Same `rise` idiom /eyes already uses for its hero, restated here because
   /eyes does not load this stylesheet.
   🔴 Opacity ONLY — no transform. A translateY on the footer would move an
   element inside a bar the page has already laid out, and on the home page the
   header is a baseline-aligned flex row, so shifting one child breaks the
   baseline the count sits on. The whole point is that nothing jumps.
   ============================================================ */
@keyframes wordmark-in { to { opacity: 1; } }

body.home .site-name,
body.home .lens-nav,
body.home .hero-caption,
body.home .day-line,
body.home .hero-controls,
.header-eyebrow,
.header-meta,
footer span:first-child,
footer span:last-child {
  opacity: 0;
  animation: wordmark-in 1.1s cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
}
/* the photograph never animates on load; the chrome arrives around it,
   wordmark -> lenses -> caption -> day-line -> arrows, ~120ms apart */
body.home .site-name     { animation-delay: 0.45s; }
body.home .lens-nav      { animation-delay: 0.57s; }
body.home .hero-caption  { animation-delay: 0.69s; }
body.home .day-line      { animation-delay: 0.81s; }
body.home .hero-controls { animation-delay: 0.93s; }
/* Capsule and view pages: the two header groups fade as BLOCKS, not per element
   — the title, the count and the date belong to one another and staggering them
   reads as three things loading rather than one header arriving.
   🔴 Fading the container composites with .ep-date's own opacity:0.9, so the
   date lands at 0.9 exactly as before. Do not also put .ep-date in this list;
   0.9 x 0.9 is 4.33:1 on --muted, which is the AA failure the comment on that
   rule was written about. */
.header-eyebrow         { animation-delay: 0.45s; }
.header-meta            { animation-delay: 0.58s; }
footer span:first-child { animation-delay: 0.60s; }
footer span:last-child  { animation-delay: 0.72s; }

/* 🔴 A fade whose start state is opacity:0 is a page that never renders its own
   name if the animation does not run. Reduced-motion users get the end state
   flat, not a permanently invisible wordmark. */
@media (prefers-reduced-motion: reduce) {
  body.home .site-name,
  body.home .lens-nav,
  body.home .hero-caption,
  body.home .day-line,
  body.home .hero-controls,
  .header-eyebrow,
  .header-meta,
  footer span:first-child,
  footer span:last-child {
    opacity: 1;
    animation: none;
  }
}
