/* z-index MUST be negative. As a fixed-position element it is out of flow,
   so at z-index 0 it painted in the positioned-descendant group — i.e. ON
   TOP of the in-flow nav and headings, which are static. The carousel only
   escaped because its slides carry a transform, which gives them their own
   stacking context. At -1 it paints below every sibling, and the app
   wrapper's own stacking context (.viewer-app / .editor-app, z-index: 1)
   keeps it from sliding behind the page background entirely. */
.background-layer-stack {
  position: fixed;
  inset: 0;
  z-index: -1;
}

.background-layer {
  position: absolute;
  inset: 0;
  transition: background-color 600ms var(--ease-out-expo);
}

.viewer-app {
  position: relative;
  z-index: 1;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.viewer-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  padding: 1.25rem clamp(1.25rem, 4vw, 3rem);
  flex-wrap: wrap;
}

.viewer-nav__brand {
  font-family: var(--font-display);
  font-size: 1.25rem;
  letter-spacing: 0.02em;
}

/* The project names and the progress hairline move together. min-width:0 is
   what lets the row actually shrink inside the header's flex — without it a
   flex item refuses to go below its content width and the row pushes the
   share button off the screen instead of scrolling. */
.viewer-nav__browse {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 9px;
}

.viewer-nav__projects {
  display: flex;
  gap: 1.5rem;
  /* One line, always. Wrapping is what turned ten projects into three rows and
     31% of a phone screen. */
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  scroll-behavior: smooth;
}

.viewer-nav__projects::-webkit-scrollbar { display: none; }

.viewer-nav__link { flex: 0 0 auto; }

/* The hairline: how wide a window of the list is on screen, and where in it.
   Deliberately NOT the accent. Gold already means "this is the project you are
   looking at", and a second gold bar six pixels under a short gold underline
   did not read as a scroll position — it read as the active item having an
   absurdly long underline. One colour, one meaning; this is a scrollbar and
   should look like one. */
.viewer-nav__rail {
  position: relative;
  /* A short track rather than the full row width. The proportion is the
     information, not the span — stretched across the whole screen a 42% fill
     became a bar half the phone wide, which is a lot of weight for "there is
     more to the right". Same reading, a fraction of the presence.
     Right-aligned so it never sits under the active name's underline, wherever
     the row happens to be scrolled to. */
  width: 72px;
  margin-left: auto;
  height: 2px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.07);
  overflow: hidden;
}

.viewer-nav__rail-thumb {
  position: absolute;
  top: 0;
  height: 100%;
  min-width: 12%;
  border-radius: 2px;
  background: rgba(243, 242, 239, 0.34);
  transition: left var(--dur-quick) var(--ease-out-quart),
              width var(--dur-quick) var(--ease-out-quart);
}

:root.is-light-bg .viewer-nav__rail { background: rgba(0, 0, 0, 0.07); }
:root.is-light-bg .viewer-nav__rail-thumb { background: rgba(0, 0, 0, 0.32); }

@media (prefers-reduced-motion: reduce) {
  .viewer-nav__projects { scroll-behavior: auto; }
  .viewer-nav__rail-thumb { transition: none; }
}

.viewer-nav__link {
  font-size: 0.9rem;
  color: var(--color-text-muted);
  padding-bottom: 0.2rem;
  border-bottom: 1px solid transparent;
  transition: color 200ms, border-color 200ms;
}

.viewer-nav__link:hover,
.viewer-nav__link.is-active {
  color: var(--color-text);
  border-color: var(--color-accent);
}

.viewer-nav__actions {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

/* Shown to everyone. The owner uses it to hand a client the link after
   checking a change on the phone; navigator.share opens the native sheet. */
.viewer-nav__share {
  font-size: 0.8rem;
  padding: 0.4rem 0.85rem;
  border-radius: var(--radius-pill);
  border: 1px solid var(--color-border);
  color: var(--color-text-muted);
  white-space: nowrap;
}

.viewer-nav__share:hover {
  color: var(--color-text);
  border-color: var(--color-text-muted);
}

/* Only ever rendered when a valid editor token already sits in this
   device's localStorage (see hasEditorToken()) — a client viewing the site
   from their own device never has that token, so they never see this. */
.viewer-nav__edit {
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  color: #1a1408;
  background: var(--color-accent);
  padding: 0.4rem 0.85rem;
  border-radius: 999px;
}

.viewer-nav__edit:hover {
  background: var(--color-accent-strong);
}

/* See .upload-primary-btn — the accent is dark gold on a light background. */
:root.is-light-bg .viewer-nav__edit:hover {
  background: #f0c064;
}

.viewer-main {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  padding: 1rem clamp(1.25rem, 4vw, 3rem) 3rem;
}

/* --- Carousel --- */

.carousel {
  position: relative;
}

/* No scroll-snap: the strip is a continuously drifting conveyor (see
   Carousel.js) — mandatory snap would fight the rAF-driven auto-scroll.
   No negative-margin bleed either: the old ±8% hack overflowed the BODY on
   phones (page could be panned sideways ~7px). */
.carousel-track {
  display: flex;
  gap: 0;
  overflow-x: auto;
  padding: 0.5rem 0 1rem;
  scrollbar-width: none;
}

.carousel-track::-webkit-scrollbar {
  display: none;
}

/* Fixed strip height, per-image width: every slide's frame carries an
   inline aspect-ratio matching its own picture, so nothing is ever cropped
   and there is no repositioning crutch. */
.carousel {
  --strip-h: min(60vh, 620px);
}

/* Phones: the whole page (nav + title + strip + page previews) fits one
   viewport height, and nothing forces sideways body panning. */
@media (max-width: 640px) {
  /* The work takes whatever is left, instead of a fixed slice of the screen.
     A flat 44vh left the pictures small AND a third of the phone empty; once
     the thumbnails moved to the bottom that emptiness just moved to the middle,
     which is no better. Letting the frames stretch means the images grow into
     it and the page still fits one screen with no scrolling. Widths follow the
     per-item aspect-ratio, so a taller frame simply shows a bigger picture. */
  .carousel-track {
    flex: 1;
    min-height: 0;
    /* Centred, not top-aligned. A portrait image on a phone is limited by the
       screen's WIDTH, not its height — filling 619px would need 423px of width
       on a 375px screen — so there is always some height it cannot use. Left
       at the top that remainder pooled into one slab above the thumbnails,
       which is the same dead space complained about, just relocated. Split
       evenly it reads as margin. */
    align-items: center;
  }

  .media-frame {
    height: 100%;
  }

  .viewer-nav {
    padding: 0.9rem 1.1rem;
    gap: 0.75rem;
  }

  /* Its own full-width line under the wordmark and 分享, rather than competing
     with them for the same row. order pushes it past .viewer-nav__actions,
     which sits earlier in the DOM. */
  .viewer-nav__browse {
    order: 3;
    flex-basis: 100%;
    width: 100%;
  }

  .viewer-main {
    padding: 0.5rem 1.1rem calc(1.5rem + env(safe-area-inset-bottom, 0px));
    gap: 0.9rem;
  }

  /* Page thumbnails sit at the bottom of the screen, not directly under the
     picture. The strip is a fixed 44vh, so on a tall phone everything below it
     was empty and the thumbnails floated in the middle of that gap with a
     third of the screen doing nothing beneath them.
     .carousel takes the height .viewer-main was already being given, and the
     thumbnails are pushed to the end of it. Where the content IS taller than
     the screen the auto margin collapses to nothing and the order is
     unchanged, so this cannot push anything out of reach. */
  .viewer-app {
    /* dvh, not vh: in Safari 100vh is the tall viewport measured as though the
       address bar were hidden, so a bottom-anchored row would sit behind it. */
    min-height: 100dvh;
  }

  .carousel {
    flex: 1;
    display: flex;
    flex-direction: column;
    min-height: 0;
  }


  .carousel-preview {
    height: 44px;
  }
}

.carousel-slide {
  flex: 0 0 auto;
}

.media-frame {
  position: relative;
  overflow: hidden;
  background: var(--color-surface);
  height: var(--strip-h);
  /* width follows the inline aspect-ratio set per item */
  /* Lets a text layer's font-size:Ncqw scale with the frame's own width
     rather than the viewport's — the same unit the editor's canvas-preview
     already uses, so a layer looks the same size in both places. */
  container-type: inline-size;
}

/* Carries the entrance transition (and clips it). The frame itself never
   transforms, so the seamless strip's layout can't develop gaps. */
.frame-content {
  position: absolute;
  inset: 0;
}

/* A 自動去背 page: the PNG's transparent background shows the site-wide
   background straight through, so the subject floats on the page instead of
   sitting on a dark card. */
.media-frame--cutout {
  background: transparent;
}

.media-frame--cutout .media-shimmer {
  display: none;
}

/* Freely positioned extra images/video on a page (see LayerStack.js).
   Centered on their x/y so dragging feels like holding the picture itself.
   Inert on the published page; the editor's 圖層 preview re-enables pointer
   events. overflow:hidden clips the layer's OWN motion (kenburns/drift/etc
   all scale past 100%) to its own box — layers animate now, the canvas
   doesn't, so this box is the thing that needs its own clip, the way
   .media-frame already clips the canvas. drop-shadow is applied to this
   same element; a filter's drop-shadow is NOT clipped by that element's own
   overflow (it composites after), so the two don't fight. */
.canvas-layer {
  position: absolute;
  transform: translate(-50%, -50%);
  max-width: none;
  overflow: hidden;
  pointer-events: none;
  filter: drop-shadow(0 4px 14px rgba(0, 0, 0, 0.35));
}

/* The layer's rendered content (MediaLayer's img/video, or DepthParallax's
   canvas) fills the layer box exactly; the box's own aspect-ratio (set
   inline from the layer's media dimensions) is what actually sizes it. */
.canvas-layer__media {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* A free-floating text layer (see LayerStack.js) — same free positioning as
   an image layer, just text instead of a picture, so a page is not limited
   to the single fixed 文字 caption. font-size in cqw scales with the frame
   (see .media-frame's container-type above), not the viewport. */
.canvas-layer--text {
  max-width: 80%;
  font-family: var(--font-display);
  line-height: 1.25;
  font-weight: 600;
  text-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
  white-space: pre-wrap;
  text-align: center;
}

.item-media {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 400ms var(--ease-out-expo), filter 500ms ease;
}

.media-frame.is-loaded .item-media {
  opacity: 1;
}

/* Lives ON the canvas (inside .media-frame), not below it — a freely
   positioned layer over the image, like a real design-tool text element,
   not a caption stuck under a photo. x/y are percentages of the frame,
   set by dragging in the editor's "文字" tab. */
.text-overlay {
  position: absolute;
  max-width: 80%;
  font-family: var(--font-display);
  font-size: 1.1rem;
  line-height: 1.3;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6), 0 2px 12px rgba(0, 0, 0, 0.35);
  pointer-events: none;
}

/* Per-page preview thumbnails — replaces the ‹ › arrow buttons and the dot
   row. Each page of the strip gets a small live preview; tap to jump. */
.carousel-previews {
  display: flex;
  justify-content: center;
  gap: 0.5rem;
  margin-top: 1rem;
  flex-wrap: wrap;
}

/* Placed AFTER the rule above, not with the other phone overrides higher up:
   both selectors are a single class, so source order decides, and up there the
   auto margin lost to the 1rem below it and the thumbnails stayed put. */
@media (max-width: 640px) {
  .carousel-previews {
    margin-top: auto;
    padding-top: 1rem;
  }
}

/* Switching projects used to be a hard cut — one set of work replaced by
   another between frames, with nothing to say a change had happened. Keyed on
   the slug in ViewerApp so the wrapper is a NEW element per project, which is
   what makes the animation replay; the rise is small because the artwork is
   the subject and a big movement would upstage it. */
@keyframes carousel-swap-in {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: translateY(0); }
}

.carousel-swap {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  animation: carousel-swap-in 320ms var(--ease-out-quart) both;
}

.carousel-preview {
  height: 56px;
  /* width follows the inline per-image aspect-ratio */
  border-radius: 8px;
  overflow: hidden;
  padding: 0;
  background: var(--color-surface);
  opacity: 0.5;
  transition: opacity var(--dur-quick) var(--ease-out-quart),
              transform var(--dur-quick) var(--ease-out-quart),
              box-shadow var(--dur-quick) var(--ease-out-quart);
  /* The jump-to-page thumbnails render the page's 圖層 as well as its canvas
     (a page's picture normally IS a layer now, so a canvas-only thumbnail
     would be a blank swatch). .canvas-layer is absolutely positioned and text
     layers size themselves in cqw, so the button has to be both the
     positioning context and the container-query container. */
  position: relative;
  container-type: inline-size;
}

.carousel-preview:hover {
  opacity: 0.85;
  transform: translateY(-2px);
}

/* An outline cannot be transitioned from "none", so selection snapped on. A
   box-shadow ring can be, and inset keeps it inside the same box the outline
   occupied — no reflow, and the thumbnails do not shift when one is picked. */
.carousel-preview.is-active {
  opacity: 1;
  box-shadow: inset 0 0 0 2px var(--color-accent);
  transform: translateY(-2px);
}

@media (prefers-reduced-motion: reduce) {
  .carousel-swap { animation-duration: 1ms; }
  .carousel-preview { transition: none; }
  .carousel-preview.is-active { transform: none; }
}

.carousel-preview__media {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

/* A video layer with no poster frame (e.g. picked from the cloud, where R2
   holds the mp4 but no captured still). Fills its layer box so it reads as
   the video's placeholder rather than a stray glyph. */
.media-layer__empty--video {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  background: var(--color-surface-strong);
  color: var(--color-text-muted);
  font-size: 1.4rem;
}
