:root {
  --banner-w: 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;
  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: "Sorts Mill Goudy", "Helvetica Neue", Arial, serif;
}

body {
  display: flex;
  flex-direction: column;
}

/* 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 {
  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); }

.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;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  position: relative;
  overflow: hidden;
}

.scroll-hint {
  position: absolute;
  bottom: clamp(0.5rem, 2vh, 1.5rem);
  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 0 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-title {
  margin: 0 0 0.15em;
  font-size: 2rem; /* 32px */
  font-weight: 700;
  line-height: 1.5; /* 48px */
}

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

.product-detail-bullets {
  margin: 0 0 1.4em;
  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;
}

.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: 108%;
  /* 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;
}

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

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

.caption-block {
  flex: 0 0 auto;
  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);
  z-index: 5;
}

.caption-year {
  display: flex;
  gap: 0.14em;
  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.56em;
  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);
  padding: 0.4em 1.2em;
  font-size: var(--text-size);
  line-height: 1;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  border-radius: 2px;
  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); }

.sold-out,
.enlarge-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: 1 / 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px dashed rgba(28, 26, 23, 0.25);
  background: rgba(28, 26, 23, 0.03);
  color: var(--muted);
}

.about-image-placeholder svg {
  width: 28%;
  height: 28%;
}

.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 {
  flex: 0 0 auto;
  text-align: left;
  padding: 0 clamp(1.25rem, 4vw, 3rem) 0.5rem;
}

.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(150px, 46vw, 220px);
  }
  .site-header { padding: 0.55rem 1rem 0; }

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

  /* 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;
  }

  .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;
}
