/* ===== RESET ===== */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  scroll-behavior: smooth;
}

body {
  min-height: 100vh;
  font-family: 'Caveat', cursive;
  line-height: 1.6;
}

/* [hidden] is only 0-0-1-0 specificity in the UA stylesheet, so any ID
   selector setting display on the same element (e.g. #lock-screen below)
   silently wins and the element stays visible. Every element toggled via
   the `hidden` attribute in script.js needs this override. */
[hidden] {
  display: none !important;
}

/* ===== LOCK SCREEN =====
   Pre-unlock: a single 100vh block — there's nothing below to scroll
   into yet since #letter stays [hidden] until a correct password, so no
   extra scroll-prevention is needed here. On unlock, the user's own
   scroll drives a parallax zoom across exactly that 100vh (the three
   golden-hour layers scale up at different rates, .lock-scene fully
   opaque) — this part stays manual/scroll-scrubbed. Once progress reaches
   1 (scrolled exactly one viewport), initLockZoom() (script.js) takes
   over: it locks page scroll and runs a fixed-duration opacity transition
   on .lock-scene (see its `transition` below) that dissolves the zoomed
   scene into beat 1, which is already sitting fully in view underneath
   by that point. Only once that transition finishes does scroll unlock
   again — so the reveal itself is a deliberate, non-scrubbed moment
   rather than something the user has to keep manually scrolling through.
   Reduced motion skips all of this — see the submit handler in
   script.js. */
#lock-screen {
  position: relative;
  /* #letter comes after #lock-screen in the DOM with no z-index of its
     own, so without this, beat 1 paints on top of #lock-screen's fixed
     .lock-scene overlay by plain DOM order the moment beat 1 scrolls
     into the same screen space — regardless of .lock-scene's own
     opacity. This z-index forces #lock-screen above #letter so fading
     .lock-scene's opacity actually dissolves it away to reveal beat 1
     underneath, instead of beat 1 just painting over it unconditionally. */
  z-index: 10;
  height: 100vh;
}

.lock-scene {
  /* fixed, not sticky: keeps the scene pinned to the viewport for the
     whole scroll-driven zoom range regardless of scroll position, and
     stays pinned through the auto-reveal transition below (page scroll
     is JS-locked during that window anyway, but fixed positioning means
     this doesn't depend on that). It's hidden via JS (visibility/
     pointer-events) once fully faded so it stops covering/intercepting
     beat 1 underneath. The opacity transition is what actually performs
     the auto-reveal crossfade — initLockZoom() (script.js) sets
     opacity to 0 once the scroll-driven zoom finishes, rather than
     scrubbing opacity by scroll position. */
  position: fixed;
  inset: 0;
  overflow: hidden;
  transition: opacity 900ms ease;
}

.lock-layer {
  position: absolute;
  /* 20% overscan on all sides (like .bg-photo's vertical-only version)
     so the scale-up in initLockZoom() never exposes an edge */
  top: -10%;
  left: -10%;
  width: 120%;
  height: 120%;
  object-fit: cover;
  will-change: transform;
}

/* ===== LOCK-SCREEN LAYER STACK =====
   Stacking contract for this pattern (a future layer should pick a
   z-index outside this range or extend it deliberately):
     z-index 0 — .lock-layer--bg (golden imagery, base layer)
     z-index 1 — .lock-layer--haze (atmospheric haze overlay)
     z-index 2 — .lock-layer--beams (light beams overlay)
     z-index 3 — .lock-form-wrap (password form, topmost) */
.lock-layer--bg { z-index: 0; }

/* haze/beams use 'screen' blend mode rather than sitting fully opaque on
   top of .lock-layer--bg — otherwise, as full-bleed opaque cover images,
   whichever layer is topmost would completely occlude the ones beneath
   it, and scaling three layers you can only ever see one of defeats the
   whole point of layering. 'screen' lightens/combines instead of
   replacing, so all three warm-toned images stay simultaneously visible
   and blended, and their differing scale rates during scroll actually
   produce a visible shift in the combined image rather than nothing. */
.lock-layer--haze {
  z-index: 1;
  mix-blend-mode: screen;
}

.lock-layer--beams {
  z-index: 2;
  mix-blend-mode: screen;
}

.lock-form-wrap {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 1.5rem;
  /* a soft radial scrim behind the form only, not a full-bleed overlay —
     keeps the site's flat/no-boxes aesthetic while protecting text
     contrast against whatever brightness lands in the generated images'
     center. Contrast re-measured (WCAG relative-luminance formula, same
     method as the letter's .scrim below) and these values re-tuned after
     adding `mix-blend-mode: screen` to .lock-layer--haze/--beams (see
     above) — blending all three light layers together made the resting
     background noticeably brighter overall than a single opaque top
     layer was, so the previous `circle 560px` / alpha 0.75 / `transparent
     70%` values (tuned before that change) read as a harsh dark disc
     rather than a soft vignette. A larger radius with a much longer
     falloff (`circle 800px`, `transparent 90%`) spreads the darkening
     over more of the frame so it reads as gradual rather than a hard
     edge, and needed alpha 0.7 (slightly less than before, since the
     brighter base still requires real darkening to clear 4.5:1) to
     compensate. Verified: worst case 4.55:1 at 1280x800, 4.61:1 at
     375x812 (sampled with the h1/input/button text hidden so only
     background pixels are measured). */
  background: radial-gradient(circle 800px, rgba(0, 0, 0, 0.7), transparent 90%);
  transition: opacity 0.4s ease;
}

.lock-form-wrap.hide {
  opacity: 0;
  pointer-events: none;
}

#lock-screen h1 {
  font-size: 2rem;
  font-weight: normal;
  color: var(--ink-dark);
}

#lock-form {
  display: flex;
  gap: 0.75rem;
  align-items: center;
}

/* explicit color: without it, the browser's UA stylesheet forces plain
   black text on form controls (overriding inheritance), which measured
   well under WCAG 4.5:1 against this warm image+scrim (a scrim tuned to
   protect the h1's light var(--ink-dark) text darkens the background,
   which only makes black text worse) — matching h1's off-white keeps
   every lock-form text element legible against the same scrim
   (re-verified after the scrim retune above: worst case 4.55:1 at
   1280x800, 4.61:1 at 375x812) */
#password-input {
  font-family: inherit;
  font-size: 1.25rem;
  padding: 0.5rem 0.75rem;
  border: 1px solid currentColor;
  background: transparent;
  color: var(--ink-dark);
}

#lock-form button {
  font-family: inherit;
  font-size: 1.25rem;
  padding: 0.5rem 1rem;
  border: 1px solid currentColor;
  background: transparent;
  cursor: pointer;
  color: var(--ink-dark);
}

#password-input::placeholder {
  color: var(--ink-dark);
  opacity: 0.7;
}

#lock-error {
  font-size: 1rem;
  opacity: 0.7;
}

@keyframes lock-shake {
  10%, 90% { transform: translateX(-1px); }
  20%, 80% { transform: translateX(2px); }
  30%, 50%, 70% { transform: translateX(-4px); }
  40%, 60% { transform: translateX(4px); }
}

/* triggered by script.js on a wrong password — the lock screen is
   deliberately quiet by design (no envelope/opening animation, see the
   design spec), so this stays a small, single gesture rather than
   anything showy */
#lock-form.shake {
  animation: lock-shake 0.4s ease-in-out;
}

@media (prefers-reduced-motion: reduce) {
  #lock-form.shake {
    animation: none;
  }
}

/* ===== LETTER TYPOGRAPHY (flat — one size, one weight, everywhere) ===== */
.beat-content p,
.beat-text p {
  font-size: 1.5rem;
  font-weight: normal;
  /* narrower than a typical 640px prose measure — Caveat is a wide,
     irregular script, so a shorter line stays easier to track than the
     same character count would in an upright typeface */
  max-width: 580px;
  margin-bottom: 1.5rem;
}

.beat-content p:last-child,
.beat-text p:last-child {
  margin-bottom: 0;
}

.signature {
  font-style: italic;
  /* extra room above (on top of the previous paragraph's own 1.5rem
     margin-bottom) so the sign-off reads as a deliberate closing beat,
     not just another paragraph — spacing only, no size/weight change,
     so this doesn't reintroduce the hierarchy the flat-typography rule
     above explicitly rules out */
  margin-top: 1.5rem;
}

/* ===== BEAT LAYOUT (base — pattern-specific rules land in later tasks) ===== */
.beat {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem;
}

.beat-content {
  position: relative;
}

/* ===== THEME ARC ===== */
:root {
  --bg-light: #f7f3ec;
  --ink-light: #1c1a17;
  --bg-dark: #14120f;
  --ink-dark: #f2ede4;
  /* warm gold, echoing the golden-hour ferry photos — the site's only
     accent color, used sparingly (roster-name hover only) */
  --accent: #e0ab5c;
}

/* separate from the RESET section's body rule above (font-family,
   line-height) — kept split so theme vars/colors stay grouped with the
   rest of this section instead of mixed into an unrelated concern */
body {
  --bg: var(--bg-light);
  --ink: var(--ink-light);
  background: var(--bg);
  color: var(--ink);
  transition: background 0.6s ease, color 0.6s ease;
}

body[data-theme="dark"] {
  --bg: var(--bg-dark);
  --ink: var(--ink-dark);
}

/* ===== PARAGRAPH REVEAL ===== */
.reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.8s cubic-bezier(.22, .61, .36, 1),
              transform 0.8s cubic-bezier(.22, .61, .36, 1);
}

.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  /* collapse the fade/slide and the theme color-flip to effectively
     instant rather than removing them outright — keeps the same code
     path (no separate no-motion branch to maintain) while respecting
     the user's OS-level preference; parallax is fully disabled instead
     in initParallax() (script.js), since it's driven by inline JS
     transforms a stylesheet rule can't reach */
  .reveal,
  body {
    transition-duration: 0.01ms !important;
  }
}

/* ===== BACKGROUND-PHOTO PATTERN (light beats) =====
   Stacking contract for this pattern (a future layer, e.g. a video-beat
   control, should pick a z-index outside this range or extend it deliberately):
     z-index 0 — .bg-photo-wrap (the photo itself)
     z-index 1 — .scrim (dark gradient, for text legibility)
     z-index 2 — .beat-content (the text) */
.beat[data-layout="background"] {
  position: relative;
  overflow: hidden;
}

.bg-photo-wrap {
  position: absolute;
  inset: 0;
  overflow: hidden;
  z-index: 0;
}

.bg-photo {
  position: absolute;
  top: -10%;
  left: 0;
  width: 100%;
  /* 20% vertical overscan (height 120%, offset -10% top) so the parallax
     drift in initParallax() (±30px) never exposes an edge underneath */
  height: 120%;
  object-fit: cover;
  will-change: transform;
}

.beat[data-layout="background"] .scrim {
  position: absolute;
  inset: 0;
  /* strengthened again from 0.6/0.45 after measuring real contrast ratios
     (WCAG relative-luminance formula) against actual screenshots: the
     weak end of that gradient let bright hotspots (glare, direct sun,
     a white notebook page) drop to ~3:1 against the text color, well
     under the 4.5:1 body-text minimum — explains "sometimes hard to
     read" reports. 0.7/0.6 keeps worst-case hotspots above 4.5:1 with
     margin (verified: a near-white glare patch and a golden-hour
     highlight both land at 4.6-5:1+ after this change) while still
     reading as a gradient, not a flat overlay */
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.6));
  z-index: 1;
}

.beat[data-layout="background"] .beat-content {
  position: relative;
  z-index: 2;
  /* var(--ink-dark) rather than a hardcoded #fff — unifies with the
     sticky (dark) beat's text color, which already used --ink-dark via
     inheritance. Both read as "white" against a dark scrim; using the
     shared token means there's one deliberate off-white across the
     whole letter instead of two unrelated whites that happened to
     converge by accident of how the CSS evolved. */
  color: var(--ink-dark);
}

/* Beat 1's picnic photo has its open space (path/trees) in the upper
   portion of the frame, with people's faces/hands lower down; beat 3's
   storefront selfie is the reverse (raised phones/faces cluster near the
   top, more open space near the bottom). Anchoring each beat's text to
   the openest part of its own photo, rather than every background beat
   sharing one centered position regardless of what's in the shot. */
.beat[data-beat="1"] {
  align-items: flex-start;
  padding-top: 4rem;
}

.beat[data-beat="3"] {
  align-items: flex-end;
  padding-bottom: 4rem;
}

/* ===== STICKY-SIDE PATTERN (dark beat) ===== */
.beat[data-layout="sticky"] {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: stretch;
  padding: 0;
  /* Fixed, not left to show through from body's theme-driven --bg: the
     theme flip (initThemeArc, script.js) fires once this beat's midpoint
     crosses screen center, which happens well before the beat has fully
     scrolled past. .beat-text has no image behind it (unlike the
     background-photo beats), so without its own background it would
     briefly flash to the next theme's light color while still on
     screen. This beat is always dark regardless of body's current
     theme, so there's nothing to flash. */
  background: var(--bg-dark);
}

.sticky-photo {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
}

.sticky-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.beat-text {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2rem;
  padding: 4rem 3rem;
}

/* flex gap above already spaces these paragraphs — override the shared
   .beat-text p margin-bottom (LETTER TYPOGRAPHY section) so the two
   spacing mechanisms don't stack into an uneven rhythm */
.beat-text p {
  margin-bottom: 0;
}

@media (max-width: 820px) {
  .beat[data-layout="sticky"] {
    grid-template-columns: 1fr;
  }

  .sticky-photo {
    position: static;
    height: 50vh;
  }

  .beat-text {
    padding: 2rem 1.5rem;
  }
}

/* ===== CLOSING VIDEO ===== */
.farewell-video {
  position: relative;
  padding: 0;
}

.farewell-video__el {
  width: 100%;
  height: 100vh;
  object-fit: cover;
}

/* ===== LIFEGROUP ROSTER (overlay on closing video) ===== */
.roster-overlay {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1.75rem;
  padding: 2.5rem 1.5rem;
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0.35), rgba(0, 0, 0, 0.6));
  /* safety net, not the primary fit strategy — the grid sizing below is
     tuned to fit a typical viewport, but 20 names is a lot of content
     and this guards against an unusually short window without clipping
     anything */
  overflow-y: auto;
}

.roster-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
  gap: 0.6rem 1.25rem;
  /* width: 100% is required, not cosmetic — .roster-overlay uses
     align-items: center, so without an explicit width this flex child
     sizes to its own intrinsic content instead of the container's
     available space, which throws off auto-fit's column math and
     overflows the viewport on narrow screens */
  width: 100%;
  max-width: 680px;
  list-style: none;
  text-align: center;
}

.roster-name {
  font-size: 1.35rem;
  color: var(--ink-dark);
  opacity: 0.7;
  transition: opacity 0.3s cubic-bezier(.22, .61, .36, 1),
              color 0.3s cubic-bezier(.22, .61, .36, 1);
}

.roster-name:hover {
  opacity: 1;
  color: var(--accent);
}

.roster-grid--honorary {
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  max-width: 540px;
}

.roster-name--honorary {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.roster-note {
  font-size: 0.9rem;
  font-style: italic;
  opacity: 0.85;
}
