:root {
  --banner-w: clamp(141px, 23vw, 264px); /* ~88% of the original clamp(160px, 26vw, 300px) */
  --banner-ratio: 0.5946; /* 1100 / 1850 */
  --ink: #1c1a17;
  --muted: #746f66;
  --paper: #f7f3eb;
  --accent: #b5502e;
  --text-size: 0.95rem;
  --text-size-sm: 0.8rem;
  --font-body: "Caudex", "Helvetica Neue", Arial, serif;
  --font-display: "Cinzel Decorative", "Caudex", serif; /* menu, date, 2027 headline only */
  color-scheme: light;
}

* { box-sizing: border-box; }

/* ---------- character reveal ---------- */
/* Backs js/text-reveal.js's per-character "blur into focus" effect. Every
   character gets this one class at split time; revealing a whole group is
   just adding `.text-reveal-root.is-revealed` on one shared ancestor —
   every `.char-reveal` descendant picks up the visible state via this
   selector, so a paragraph of hundreds of characters costs a handful of
   style writes at reveal time instead of one write per character. The
   per-character stagger comes from `--i` (set once per span, at split time)
   combined with `--reveal-delay`/`--reveal-stagger` (set on the shared
   ancestor when reveal() runs). */

.char-reveal {
  display: inline-block;
  opacity: 0;
  filter: blur(var(--char-reveal-blur, 6px));
  transition:
    filter var(--char-reveal-duration, 450ms) var(--char-reveal-ease, ease)
      calc(var(--reveal-delay, 0ms) + var(--i, 0) * var(--reveal-stagger, 0ms)),
    opacity var(--char-reveal-duration, 450ms) var(--char-reveal-ease, ease)
      calc(var(--reveal-delay, 0ms) + var(--i, 0) * var(--reveal-stagger, 0ms));
}

.text-reveal-root.is-revealed .char-reveal {
  opacity: 1;
  filter: blur(0px);
}

@media (prefers-reduced-motion: reduce) {
  .char-reveal {
    opacity: 1;
    filter: none;
    transition: none;
  }
}

/* ---------- page loader ---------- */
/* Covers the whole viewport (same color as the page itself, so there's
   nothing to visually "pop" when it's removed) while js/preloader.js loads
   the gallery's images/font ahead of the reveal sequence. See
   js/page-transitions.js, which waits on window.__pagePreload before
   starting the normal main-fade/reveal chain. */

.page-loader {
  position: fixed;
  inset: 0;
  z-index: 5000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--paper);
  transition: opacity 0.5s ease;
}

.page-loader.is-hidden {
  opacity: 0;
  pointer-events: none;
}

.page-loader-percent {
  font-size: 1.1rem;
  letter-spacing: 0.08em;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  min-width: 3.5ch;
  text-align: center;
  opacity: 0;
  transform: translateY(6px);
  animation: page-loader-percent-in 0.6s ease 0.15s forwards;
}

@keyframes page-loader-percent-in {
  to { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .page-loader { transition: none; }
  .page-loader-percent { animation: none; opacity: 1; transform: none; }
}

/* ---------- cross-page transitions ---------- */
/* Manual, plain CSS+JS page transition (see js/page-transitions.js) instead
   of the native Cross-Document View Transitions API — that API isn't
   supported in Firefox at all, and couldn't be confirmed working even in a
   supporting Chromium build during testing, so it can't be relied on. This
   targets <main> specifically (both pages have exactly one), rather than
   <body>, so the header/nav and footer stay put and only the page's actual
   content transitions. */
/* main starts hidden with no transition at all — this is the very first
   painted state, before page-transitions.js runs. The transition itself is
   only added along with .is-page-ready, so that first "hidden -> visible"
   style change is the one that actually gets animated (adding a class that
   changes a property's value *and* introduces its transition in the same
   style recalc still transitions correctly). Keeping opacity:1 permanently
   on `main` itself — as an earlier version did — meant toggling a temporary
   "entering" class on top of an already-transitioning property raced with
   its own reversal a few ms later and never produced a visible fade. */
main {
  opacity: 0;
  transform: scale(1.02) translateY(-10px);
}

main.is-page-ready {
  opacity: 1;
  transform: scale(1) translateY(0);
  transition: opacity 0.7s cubic-bezier(0.4, 0, 0.2, 1),
              transform 0.7s cubic-bezier(0.4, 0, 0.2, 1);
}

main.is-page-ready.is-page-leaving {
  opacity: 0;
  transform: scale(0.97) translateY(14px);
}

@media (prefers-reduced-motion: reduce) {
  main {
    opacity: 1;
    transform: none;
  }
  main.is-page-ready {
    transition: none;
  }
}

html, body {
  height: 100%;
  margin: 0;
  overflow: hidden; /* the whole page never scrolls; only the gallery does */
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
}

body {
  display: flex;
  flex-direction: column;
  position: relative; /* containing block for .site-footer's absolute positioning */
}

/* content pages (About, etc.) aren't the single-viewport gallery app — let
   them scroll normally if their content runs taller than the viewport */
body.page-about {
  overflow-y: auto;
  overflow-x: hidden;
}

/* ---------- header ---------- */

.site-header {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.7rem clamp(1.25rem, 4vw, 3rem) 0;
}

.logo-link { display: block; line-height: 0; }

.logo-img {
  height: clamp(28px, 4vw, 40px);
  width: auto;
  display: block;
}

.site-nav {
  display: flex;
  gap: clamp(1.25rem, 3vw, 2.5rem);
}

.site-nav a {
  font-family: var(--font-display);
  color: var(--ink);
  text-decoration: none;
  font-size: var(--text-size);
  font-weight: 400;
  letter-spacing: 0.04em;
  transition: color 0.2s ease;
}

.site-nav a:hover { color: var(--ink); }

/* phones only (the logo does this job on desktop) */
.site-nav .nav-home { display: none; }

.nav-icons {
  display: flex;
  align-items: center;
  gap: 20px;
}

.nav-icon-link {
  display: flex;
  align-items: center;
  color: var(--ink);
  transition: color 0.2s ease;
}

.nav-icon-link:hover { color: var(--ink); }

/* mobile-only nav toggle/close — hidden on desktop, see the mobile media
   query below for their actual (small-screen) appearance */
.nav-toggle,
.nav-close {
  display: none;
  appearance: none;
  border: none;
  background: none;
  color: var(--ink);
  cursor: pointer;
  padding: 0;
}

.nav-toggle svg,
.nav-close svg {
  width: 1.6rem;
  height: 1.6rem;
  display: block;
}

.nav-icon {
  font-size: 1.05em;
}

/* ---------- main / gallery viewport ---------- */

.gallery-viewport {
  flex: 1 1 auto;
  min-height: 0;
  position: relative;
  overflow: hidden;
}

/* scoped (not the bare .gallery-stage rule below, which banner-lab.html's
   #labStage also uses inside its own, differently-laid-out wrapper): fills
   the viewport independently of .caption-block's own position, so the
   gallery's size and vertical centering never shift no matter where the
   caption is nudged. The top offset reserves the same footprint
   .caption-block occupied before it became independently positioned,
   so the gallery doesn't grow into that space now that the caption no
   longer pushes it down in normal flow. */
.gallery-viewport .gallery-stage {
  position: absolute;
  top: 51px;
  left: 0;
  right: 0;
  bottom: 0;
  width: 100%;
}

.scroll-hint {
  position: absolute;
  bottom: clamp(1.5rem, 5.5vh, 2.75rem);
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4em;
  margin: 0;
  font-size: var(--text-size-sm);
  letter-spacing: 0.06em;
  color: var(--muted);
  transition: opacity 0.5s ease;
  pointer-events: none;
  z-index: 5;
}

.scroll-hint.is-hidden { opacity: 0; }

.hint-icon {
  width: 1em;
  height: 1em;
  flex: 0 0 auto;
}

.hint-desktop {
  display: flex;
  align-items: center;
  gap: 0.4em;
}

.hint-mobile { display: none; }

/* ---------- product detail view ---------- */

.detail-close,
.detail-back {
  appearance: none;
  position: absolute;
  top: clamp(0.5rem, 2vh, 1.5rem);
  width: 2rem;
  height: 2rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: none;
  color: var(--ink);
  cursor: pointer;
  padding: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.4s ease;
  z-index: 2001;
}

.detail-close { right: clamp(1.25rem, 4vw, 3rem); }
.detail-back { left: clamp(1.25rem, 4vw, 3rem); }

.detail-close svg,
.detail-back svg {
  width: 1.4rem;
  height: 1.4rem;
}

.detail-close.is-active,
.detail-back.is-active {
  opacity: 1;
  pointer-events: auto;
}

.product-detail {
  position: absolute;
  top: 0;
  left: 0;
  width: 33.333%;
  height: 100%;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 1.2em;
  padding: 2rem clamp(1.5rem, 4vw, 3rem) 2rem clamp(1.5rem, 4vw, 3rem);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.5s ease;
  z-index: 2000;
}

.product-detail.is-active {
  opacity: 1;
  pointer-events: auto;
}

.product-detail-text {
  color: var(--ink);
}

.product-detail-kicker {
  margin: 0 0 0.5em;
  font-size: 0.875rem; /* 14px */
  font-weight: normal;
  line-height: normal;
  letter-spacing: 4px;
  text-transform: uppercase;
}

.product-detail-quote {
  margin: 0 0 0.15em;
  font-size: 2rem; /* 32px */
  font-weight: 700;
  line-height: 1.15;
}

/* only the very long quotes (see LONG_QUOTE_CHARS in main.js) shrink, so
   they still fit the left column; every other quote/title stays at 32px */
/* the 2027 headline (not the per-year quotes, which share this element) */
.product-detail-quote.is-headline {
  font-family: var(--font-display);
}

.product-detail-quote.is-long {
  font-size: 1.5rem;
  line-height: 1.2;
}

.product-detail-source {
  margin: 0;
  font-size: var(--text-size-sm);
  font-style: italic;
  color: var(--muted);
  letter-spacing: 0.02em;
}

.product-detail-description {
  margin: 0 0 1.1em;
  font-size: 1.25rem; /* 20px */
  line-height: normal;
}

.product-detail-bullets {
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 1.125rem; /* 18px */
  line-height: normal;
}

.product-detail-bullets li {
  /* hanging indent via flex, not text-indent: a negative text-indent here
     collapses the nested inline-block character spans to zero width in
     some engines. The "•" glyph is real text (see index.html) so it's
     included in the character reveal, unlike a native ::marker, which
     would render instantly regardless of the text's own hidden/revealed
     state. */
  display: flex;
  gap: 0.6em;
  margin-bottom: 8px;
}

.product-detail-bullets li:last-child {
  margin-bottom: 0;
}

.product-detail-bullets .bullet-mark {
  flex: 0 0 auto;
  font-size: 0.85em;
}

.product-detail-bullets .bullet-text {
  flex: 1 1 auto;
}

.gallery-stage {
  position: relative;
  width: 100%;
  flex: 1 1 auto;
  min-height: 0;
  display: block;
  outline: none;
  touch-action: pan-y;
  cursor: pointer;
  perspective: 1400px;
  perspective-origin: 50% 45%;
}

.gallery-stage.is-dragging { cursor: grabbing; }

/* each banner: an outer (rigid, coverflow-positioned) wrapper */
.banner {
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--banner-w);
  aspect-ratio: var(--banner-ratio);
  will-change: transform;
  /* deliberately NOT preserve-3d: this element's own translateZ/rotateY
     (the coverflow depth effect) still renders correctly regardless, since
     that's governed by .gallery-stage's perspective, not this property.
     What this property change actually does is stop its two children
     (.banner-rod and .banner-tilt) from being depth-sorted against each
     other in true 3D — they sat at nearly identical depth, and browsers
     resolve that kind of tie inconsistently (worked in Chromium, silently
     let the poster paint over the rod in Firefox, and even then not always
     the same way twice, since the sway animation shifts the tie by a
     fraction of a pixel every frame). Flattening them onto this element's
     own plane makes their stacking a plain, deterministic z-index compare
     instead of an ambiguous 3D one. */
}

/* wraps .banner-rod + .banner-tilt for the one-time page-load reveal
   (fade + slide-up) — CSS-transition-driven so it keeps animating via the
   browser's own style/compositor engine even if banner-engine.js's per-frame
   render loop is briefly delayed by other page-load work, instead of
   depending on that loop running on schedule every frame. See
   triggerReveal() in js/banner-engine.js. Matches .banner's own box exactly
   (inset:0 = same size/position), so it doesn't shift where .banner-rod's
   percentage-based positioning resolves. */
.banner-reveal {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.banner-rod {
  position: absolute;
  top: 0;
  left: 50%;
  width: calc(100% + 12px); /* 6px past the poster on each side */
  /* fixed at the source image's own ratio (1400/25) so layout is correct
     before JS ever measures/draws it — see drawRod() in banner-engine.js,
     which pre-renders the wood texture onto this canvas at its exact
     display size instead of letting the browser rescale an <img> at
     runtime inside a rotated 3D (coverflow) context */
  aspect-ratio: 56;
  transform: translate(-50%, -55%);
  pointer-events: none;
  user-select: none;
  display: block;
  z-index: 2;
}

.banner-tilt {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  aspect-ratio: var(--banner-ratio);
  transform-style: preserve-3d;
  transform-origin: 50% 0%;
  will-change: transform, filter;
}

.banner-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  user-select: none;
}

.banner-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: fill;
  display: block;
  -webkit-user-drag: none;
  -webkit-user-select: none;
  user-select: none;
}

/* ---------- caption ---------- */

.caption-block {
  position: relative; /* re-establishes z-index above .gallery-stage, now that it's not a fellow flex item */
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
  padding: clamp(0.3rem, 0.8vh, 0.5rem) 0 clamp(0.2rem, 0.6vh, 0.35rem);
  margin-top: clamp(1.75rem, 4vh, 2.75rem); /* nudges the date/availability text down toward the gallery, independent of the gallery's own vertical centering */
  z-index: 5;
}

.caption-year {
  font-family: var(--font-display);
  display: flex;
  gap: 0.04em;
  font-size: 1.2rem;
  font-weight: 600;
  color: var(--ink);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

.odometer-digit {
  position: relative;
  height: 1em;
  width: 0.7em; /* fits the widest Cinzel Decorative digit ("0", 0.69em) */
  overflow: hidden;
}

.odometer-strip {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  backface-visibility: hidden;
  transition: transform 1s cubic-bezier(0.65, 0, 0.35, 1);
}

.odometer-strip span {
  height: 1em;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-variant-numeric: tabular-nums;
}

.caption-availability {
  position: relative;
  height: 1.6em;
  font-size: var(--text-size-sm);
  display: flex;
  align-items: center;
  justify-content: center;
}

.avail-state {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity 0.35s ease;
  pointer-events: none;
}

.avail-state.is-active {
  opacity: 1;
  pointer-events: auto;
}

.buy-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  appearance: none;
  border: 1px solid var(--ink);
  background: var(--ink);
  color: var(--paper);
  /* uneven on purpose: Caudex caps sit low in their line box, so this
     puts equal space above and below the letters themselves */
  padding: 0.92em 1.2em 0.88em;
  font-size: var(--text-size);
  line-height: 1;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  border-radius: 4px;
  cursor: pointer;
  white-space: nowrap;
  align-self: flex-start;
  transition: transform 0.15s ease, filter 0.2s ease;
}

.buy-btn:hover { filter: brightness(1.25); }
.buy-btn:active { transform: scale(0.96); }
.buy-btn[hidden] { display: none; }

.sold-out,
.buy-hint {
  font-style: italic;
  color: var(--muted);
  font-size: var(--text-size-sm);
  letter-spacing: 0.04em;
  white-space: nowrap;
}

/* ---------- about page ---------- */

.about-main {
  flex: 1 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: clamp(2rem, 5vw, 4rem);
  padding: clamp(2rem, 6vh, 4rem) clamp(1.25rem, 4vw, 3rem);
}

.about-image-placeholder {
  flex: 0 0 auto;
  width: min(42vw, 480px);
  aspect-ratio: 1278 / 960; /* matches assets/images/about-thats-us.jpg's native ratio */
  overflow: hidden;
  border-radius: 20px;
}

.about-photo {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.about-text {
  flex: 1 1 320px;
  max-width: 420px;
  display: flex;
  flex-direction: column;
  gap: 1em;
}

.about-text h1 {
  margin: 0;
  font-size: 2rem;
  font-weight: 600;
  color: var(--ink);
}

.about-text p {
  margin: 0;
  font-size: 1.05rem;
  line-height: 1.6;
  color: var(--ink);
}

/* ---------- footer ---------- */

.site-footer {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  text-align: left;
  padding: 0 clamp(1.25rem, 4vw, 3rem) 0.5rem;
  z-index: 1500; /* above every banner (max z-index 1000, see banner-engine.js) but below .product-detail (2000) */
}

.site-footer p {
  margin: 0;
  font-size: var(--text-size-sm);
  color: var(--muted);
  letter-spacing: 0.04em;
}

/* ---------- responsive ---------- */

@media (max-width: 640px) {
  :root {
    --banner-w: clamp(132px, 40vw, 194px); /* ~88% of the original clamp(150px, 46vw, 220px) */
  }
  /* header at 1.2x on phones: logo, menu icons and padding */
  .site-header { padding: 0.66rem 1.2rem 0; }
  .logo-img { height: 33.6px; }
  .nav-toggle svg,
  .nav-close svg {
    width: 1.92rem;
    height: 1.92rem;
  }

  .hint-desktop { display: none; }
  .hint-mobile { display: block; }

  /* swipes in any direction move the gallery (see pagedSwipe in
     banner-engine.js), so the browser shouldn't claim vertical ones */
  .gallery-stage { touch-action: none; }

  /* the hamburger replaces the inline nav row on mobile */
  .nav-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
  }

  /* .site-nav becomes a full-screen drawer, opened by .nav-toggle and
     hidden until then — same links, same hrefs, same icons as desktop,
     just presented as a typical mobile menu instead of an inline row */
  .site-nav {
    position: fixed;
    inset: 0;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2rem;
    background: var(--paper);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.3s ease;
    z-index: 3000;
  }

  .site-nav.is-open {
    opacity: 1;
    pointer-events: auto;
  }

  .site-nav a {
    font-size: 1.3rem;
  }

  .site-nav .nav-home { display: block; }

  /* landing view: text stacked above the enlarged poster, which
     stackedHomeGeometry() in main.js places right below this block — so
     the vertical padding is the spacing: 48px header → text, 48px text →
     poster. The side padding matches the poster's (SIDE in main.js). */
  .gallery-viewport.is-home .product-detail {
    width: 100%;
    height: auto;
    justify-content: flex-start;
    gap: 0.9em;
    padding: 48px 28px;
  }

  .gallery-viewport.is-home .product-detail-quote {
    font-size: 1.6rem;
  }

  /* About: photo on top filling the free height (cropped to fit, never
     shorter than its own proportions), then the text — 28px side margins
     like the landing view, 48px from the header and between the two, and
     room at the bottom for the copyright line */
  .about-main {
    flex-direction: column;
    flex-wrap: nowrap;
    align-items: stretch;
    justify-content: flex-start;
    gap: 48px;
    padding: 48px 28px calc(48px + 2rem);
  }

  .about-image-placeholder {
    width: 100%;
    flex: 1 1 auto;
    aspect-ratio: auto;
    min-height: calc((100vw - 56px) * 960 / 1278);
  }

  .about-text {
    flex: 0 0 auto;
    max-width: none;
  }

  .nav-close {
    display: flex;
    align-items: center;
    justify-content: center;
    position: absolute;
    top: clamp(0.85rem, 3vh, 1.5rem);
    right: 1rem;
  }
}

/* ---------- reduced motion ---------- */

.gallery-stage.reduced-motion .banner-tilt,
.gallery-stage.reduced-motion .banner {
  transition: none;
}
