/* pb_public/css/motion.css
   ═══════════════════════════════════════════════════════════════════════════
   Motion. Loaded after pages.css.

   The site is quiet and the art is the focus, so the rules are few:
     1. Nothing moves unless it helps you keep your place.
     2. Fades before movement. Opacity only; no bounce, no overshoot, no scale.
     3. Pages change with a short crossfade; the header stays put.
     4. Only a few things travel: the search bar (it unfolds out of its button),
        an artwork (card → page, peek → page, page ⇄ fullscreen viewer) and the
        cards of a list when its filter, sort or layout changes.
     5. Images arrive with a soft fade over the artwork's own colour.
     6. Everything is zero under prefers-reduced-motion.

   Tokens
     --motion-nav     page-to-page crossfade
     --motion-morph   things that travel (search bar, artwork, header dock)
     --motion-settle  content arriving after a fetch, footer fade-in
     --motion-image   image fade-in
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  --motion-ease: var(--ease-calm);
  --motion-nav: 200ms;
  --motion-morph: 300ms;
  --motion-settle: 360ms;
  --motion-image: 320ms;
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --motion-nav: 0ms;
    --motion-morph: 0ms;
    --motion-settle: 0ms;
    --motion-image: 0ms;
  }
  /* Page transitions stay ON but nothing moves or fades (pages.css / base.css zero every
     animation): the browser still keeps the previous page on screen until the next one is
     ready, so there is no blank flash between pages. Turning them off would bring it back. */
}

/* ══ 1. Page-to-page (cross-document view transitions) ════════════════════ */

/* The whole page: one short, even crossfade. */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: var(--motion-nav);
  animation-timing-function: var(--motion-ease);
}

/* The header never fades or moves with the page. It's captured on its own
   layer and the new one simply takes over. */
.site-header { view-transition-name: site-header; }
::view-transition-group(site-header) { animation: none; }
::view-transition-old(site-header)   { display: none; }
::view-transition-new(site-header)   { animation: none; }

/* The control cluster glides between its home and docked positions. The two
   snapshots can differ in width (the search button comes and goes), so they
   are pinned right and cross-faded instead of being stretched to fit. */
::view-transition-group(nav-dock) {
  animation-duration: var(--motion-morph);
  animation-timing-function: var(--motion-ease);
}
::view-transition-old(nav-dock),
::view-transition-new(nav-dock) {
  width: 100%;
  height: 100%;
  object-fit: none;
  object-position: right center;
}

/* Artwork: the clicked card's image carries into the page's hero. */
::view-transition-group(*.artwork) {
  animation-duration: var(--motion-morph);
  animation-timing-function: var(--motion-ease);
}
::view-transition-old(*.artwork),
::view-transition-new(*.artwork) {
  height: 100%;
  object-fit: cover;
}

/* Reflow: archive / journal cards glide to their new places when a filter,
   sort, layout or search changes. Same size → a pure slide; different size
   (layout switch) → the two snapshots cross-fade inside the moving box. */
::view-transition-group(*.reflow) {
  animation-duration: var(--motion-morph);
  animation-timing-function: var(--motion-ease);
}
::view-transition-old(*.reflow),
::view-transition-new(*.reflow) {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top left;
}

/* ══ 2. Search: the bar unfolds out of its button ═════════════════════════ */

/* components.css names the button permanently, which makes every page change
   try to pair two buttons. It is named only for the length of the morph (see
   components/search-view.js), so switch the permanent name off. */
.site-header__controls [data-search-open] { view-transition-name: none; }

::view-transition-group(search-bar) {
  animation-duration: var(--motion-morph);
  animation-timing-function: var(--motion-ease);
  overflow: clip;
  border-radius: var(--radius-md);
}
/* Default behaviour scales the 44px icon snapshot up to bar width, giving a
   huge blurred magnifier. Instead each snapshot keeps its natural size, pinned
   to the right edge where both icons live, and the group's growing box
   reveals the bar. */
::view-transition-old(search-bar),
::view-transition-new(search-bar) {
  width: 100%;
  height: 100%;
  object-fit: none;
  object-position: right center;
}
::view-transition-old(search-bar) { animation-duration: calc(var(--motion-morph) * 0.5); }

/* ══ 3. Content arriving ══════════════════════════════════════════════════ */

/* Pages whose content comes from a fetch hide the footer until it's rendered
   (see pageReady() in core/shell.js). */
.site-footer { transition: opacity var(--motion-settle) var(--motion-ease); }
body[data-loading] .site-footer { opacity: 0; visibility: hidden; transition: none; }

/* Content that arrived after first paint fades in as one piece. */

/* ══ 4. Images ════════════════════════════════════════════════════════════ */

/* The box is tinted with the artwork's own colour (set inline as --ph) while
   the image loads. The image itself is never hidden: it paints as its bytes
   arrive, exactly as the browser would do without any of this. */
.piece-card__image,
.update-card__image { background-color: var(--ph, var(--surface-sunken)); }

/* ══ 5. Scroll-driven: the home dock ══════════════════════════════════════ */

/* The control cluster rises from the bottom-right to the top as the hero
   scrolls away. The browser drives it from the scroll position (compositor,
   no scroll handler). core/pages/home.js sets body[data-dock="css"] when the
   browser supports it, and --dock-distance on resize. */
@keyframes dock-rise {
  to { transform: translate3d(0, calc(var(--dock-distance, 0px) * -1), 0); }
}
body[data-dock="css"][data-page="home"] .site-header__controls {
  animation: dock-rise linear both;
  animation-timeline: scroll(root block);
  animation-range: 0px var(--dock-distance, 0px);
}

/* ══ 6. content-visibility ════════════════════════════════════════════════ */
/* Skip rendering work for things far below the viewport. `auto <size>`
   remembers the real size once seen, so the scrollbar doesn't jump.
   NOT applied to masonry grids: core/masonry.js measures card heights, which
   a skipped subtree can't report. */
.site-footer            { content-visibility: auto; contain-intrinsic-size: auto 520px; }
.home-section--updates  { content-visibility: auto; contain-intrinsic-size: auto 720px; }
.archive-index-row      { content-visibility: auto; contain-intrinsic-size: auto 72px; }
.journal-entry-row      { content-visibility: auto; contain-intrinsic-size: auto 150px; }
