/* ==========================================================================
   QR design-and-download preview
   --------------------------------------------------------------------------
   Twelve beats over seven layers: find a QR code in the list, open it, scroll
   its details, edit its design, watch the design apply, then download it.

   Self-contained, like hc.css. Three preview shapes now share only their cursor
   primitives; factoring a common base is worth doing next, but not while this
   one is still being tuned.

   Beats — 14s
   --------------------------------------------------------------------------
      0.00 -  3.21%   hold the QR Codes list
      3.21 -  8.21%   cursor travels to the 'qrpage1' row
      8.21 -  9.64%   press + click ring
      8.93 - 12.86%   crossfade to the QR details page
     12.86 - 16.79%   hold — scans chart and stat cards
     16.79 - 24.64%   scroll down: main column 856px, right rail 588px
     24.64 - 28.21%   hold scrolled
     28.21 - 32.86%   zoom in 1.9x toward 'Edit Design'
     32.86 - 36.79%   cursor travels to it
     36.79 - 38.21%   press + click ring
     38.21 - 42.86%   zoom back out
     41.79 - 45.71%   crossfade to the design editor
     45.71 - 49.64%   hold the plain QR
     49.64 - 55.00%   the design applies — crossfade to the styled QR
     55.00 - 61.43%   hold the designed QR
     61.43 - 66.07%   zoom in 1.9x toward 'Download'
     66.07 - 69.64%   cursor travels to it
     69.64 - 71.07%   press + click ring
     71.07 - 75.71%   zoom back out
     74.64 - 78.93%   the download modal opens
     78.93 - 82.86%   hold the modal
     82.86 - 87.14%   cursor sweeps down to the modal's Download button
     87.14 - 88.57%   press + click ring
     88.57 - 92.86%   hold
     92.86 - 95.71%   modal closes
     95.71 - 99.29%   crossfade back to the list
     99.29 -  100 %   hold, loop

   Two things here that no other preview needed
   --------------------------------------------------------------------------
   1. The details page's main column and right rail scroll by DIFFERENT amounts
      — 856px and 588px. The rail is sticky and hits its stop partway, lagging
      the body by 268px. So there are two clipped windows side by side, split at
      75.90% (inside the real gutter between them), each translating its own
      strip by its own measured distance. One full-width strip would drag the
      rail 268px too far.

   2. The app pages and the design editor are cropped from different x-offsets
      at the same output size — the app pages carry two macOS scrollbars that
      must go, the editor's right-hand toggles must stay, and no single window
      does both. Identical output size means identical rendered scale, so the
      crossfades between scenes don't jump.

   Layer order, bottom to top
   --------------------------------------------------------------------------
     .qd-a        the QR Codes list. Always opaque; the loop returns to it by
                  fading everything above away.
     .qd-details  sticky header + the two scrolling windows
     .qd-d        design editor, plain QR
     .qd-e        design editor, styled QR (fades in over .qd-d)
     .qd-f        download modal, with the editor dimmed behind it
     cursor
   ========================================================================== */

.qd-preview {
  /* ---- from build.py ---- */
  --qd-duration: 14s;
  --qd-aspect: 1200 / 856;
  --qd-sticky-h: 18.5030%;       /* sticky header layer, 4px taller than the real
                                    boundary so it covers the windows' top edge */
  --qd-window-top: 18.2635%;     /* first row that actually scrolls */
  --qd-split: 75.8974%;          /* main column | right rail, inside their gutter */
  --qd-main-scroll: -38.5412%;   /* 856px of the main strip's own 2221px */
  --qd-rail-scroll: -30.1075%;   /* 588px of the rail strip's own 1953px */

  --qd-row-x: 28.12%;            /* the 'qrpage1' row name, frame A */
  --qd-row-y: 39.34%;
  --qd-edit-x: 83.25%;           /* 'Edit Design', frame C (scrolled) */
  --qd-edit-y: 50.84%;
  --qd-dl-x: 82.09%;             /* editor's 'Download', frame D */
  --qd-dl-y: 3.17%;
  --qd-modal-dl-x: 79.91%;       /* the modal's primary button, frame F */
  --qd-modal-dl-y: 95.87%;

  --qd-zoom1-x: 83.25%;          /* Edit Design: its own centre frames it fine */
  --qd-zoom1-y: 50.84%;
  --qd-zoom2-x: 82.09%;          /* the editor's Download sits 1.4% from the top,
                                    so anchoring on its centre would clip it */
  --qd-zoom2-y: 2.60%;
  --qd-park-x: 55%;              /* idle; inside both zoom windows */
  --qd-park-y: 40%;
  --qd-zoom: 1.9;
  --qd-zoom-inv: 0.5263;

  --qd-cursor-size: 18px;
  --qd-ring-size: 30px;
  --qd-ring-color: 216 60 45;
  --qd-radius: 12px;

  position: relative;
  width: 100%;
  aspect-ratio: var(--qd-aspect);
  overflow: hidden;
  border-radius: var(--qd-radius);
  background: #0b0e13;
  box-shadow:
    0 0 0 1px rgb(0 0 0 / 30%),
    0 12px 32px -12px rgb(0 0 0 / 45%);
  contain: paint;
}

.qd-stage {
  position: absolute;
  inset: 0;
  transform-origin: var(--qd-zoom1-x) var(--qd-zoom1-y);
  animation: qd-zoom var(--qd-duration) infinite;
  will-change: transform;
}

/* --- flat full-frame layers ---------------------------------------------- */

.qd-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.qd-d { opacity: 0; animation: qd-editor  var(--qd-duration) infinite; will-change: opacity; }
.qd-e { opacity: 0; animation: qd-styled  var(--qd-duration) infinite; will-change: opacity; }
.qd-f { opacity: 0; animation: qd-modal   var(--qd-duration) infinite; will-change: opacity; }

/* --- the details page: sticky header + two independently scrolling columns -- */

.qd-details {
  position: absolute;
  inset: 0;
  opacity: 0;
  animation: qd-details var(--qd-duration) infinite;
  will-change: opacity;
}

.qd-window {
  position: absolute;
  top: var(--qd-window-top);
  bottom: 0;
  overflow: hidden;   /* this is what makes each scroll a scroll */
}

.qd-window-main { left: 0; width: var(--qd-split); }
.qd-window-rail { left: var(--qd-split); right: 0; }

.qd-strip {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: auto;       /* natural; the translate below is a % of that height */
  display: block;
  will-change: transform;
}

.qd-strip-main { animation: qd-scroll-main var(--qd-duration) infinite; }
.qd-strip-rail { animation: qd-scroll-rail var(--qd-duration) infinite; }

.qd-sticky {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: var(--qd-sticky-h);
  object-fit: fill;
  display: block;
}

/* --- cursor -------------------------------------------------------------- */

.qd-cursor-track {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  animation: qd-cursor var(--qd-duration) infinite;
  will-change: transform;
  pointer-events: none;
}

.qd-cursor-scale {
  position: absolute;
  top: 0;
  left: 0;
  animation: qd-counterscale var(--qd-duration) infinite;
  transform-origin: 0 0;
}

.qd-arrow {
  display: block;
  width: var(--qd-cursor-size);
  height: auto;
  transform-origin: 0 0;
  animation: qd-press var(--qd-duration) infinite;
  filter: drop-shadow(0 1px 3px rgb(0 0 0 / 55%));
  overflow: visible;   /* the tip is at viewBox 0,0 — don't clip its stroke */
}

.qd-ring {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--qd-ring-size);
  height: var(--qd-ring-size);
  margin: calc(var(--qd-ring-size) / -2) 0 0 calc(var(--qd-ring-size) / -2);
  border-radius: 50%;
  border: 2.5px solid rgb(var(--qd-ring-color) / 90%);
  background: radial-gradient(circle,
    rgb(var(--qd-ring-color) / 30%) 0%,
    rgb(var(--qd-ring-color) / 0%) 70%);
  box-shadow: 0 0 0 2px rgb(255 255 255 / 22%);
  opacity: 0;
  animation: qd-ring var(--qd-duration) infinite;
}

/* ==========================================================================
   Keyframes
   ========================================================================== */

@keyframes qd-zoom {
  0%       { transform: scale(1);             transform-origin: var(--qd-zoom1-x) var(--qd-zoom1-y);
             animation-timing-function: linear; }
  28.214%  { transform: scale(1);             transform-origin: var(--qd-zoom1-x) var(--qd-zoom1-y);
             animation-timing-function: cubic-bezier(.32,0,.24,1); }
  32.857%  { transform: scale(var(--qd-zoom)); transform-origin: var(--qd-zoom1-x) var(--qd-zoom1-y);
             animation-timing-function: linear; }
  38.214%  { transform: scale(var(--qd-zoom)); transform-origin: var(--qd-zoom1-x) var(--qd-zoom1-y);
             animation-timing-function: cubic-bezier(.32,0,.24,1); }
  42.857%  { transform: scale(1);             transform-origin: var(--qd-zoom1-x) var(--qd-zoom1-y); }

  /* origin walks to the second anchor — scale is 1, so nothing moves */
  49.643%  { transform: scale(1);             transform-origin: var(--qd-zoom2-x) var(--qd-zoom2-y); }

  61.429%  { transform: scale(1);             transform-origin: var(--qd-zoom2-x) var(--qd-zoom2-y);
             animation-timing-function: cubic-bezier(.32,0,.24,1); }
  66.071%  { transform: scale(var(--qd-zoom)); transform-origin: var(--qd-zoom2-x) var(--qd-zoom2-y);
             animation-timing-function: linear; }
  71.071%  { transform: scale(var(--qd-zoom)); transform-origin: var(--qd-zoom2-x) var(--qd-zoom2-y);
             animation-timing-function: cubic-bezier(.32,0,.24,1); }
  75.714%  { transform: scale(1);             transform-origin: var(--qd-zoom2-x) var(--qd-zoom2-y); }
  92.857%  { transform: scale(1);             transform-origin: var(--qd-zoom2-x) var(--qd-zoom2-y); }

  /* and back, so 100% matches 0% and the loop is seamless */
  100%     { transform: scale(1);             transform-origin: var(--qd-zoom1-x) var(--qd-zoom1-y); }
}

@keyframes qd-counterscale {
  0%       { scale: 1;                  animation-timing-function: linear; }
  28.214%  { scale: 1;                  animation-timing-function: cubic-bezier(.32,0,.24,1); }
  32.857%  { scale: var(--qd-zoom-inv); animation-timing-function: linear; }
  38.214%  { scale: var(--qd-zoom-inv); animation-timing-function: cubic-bezier(.32,0,.24,1); }
  42.857%  { scale: 1;                  animation-timing-function: linear; }
  61.429%  { scale: 1;                  animation-timing-function: cubic-bezier(.32,0,.24,1); }
  66.071%  { scale: var(--qd-zoom-inv); animation-timing-function: linear; }
  71.071%  { scale: var(--qd-zoom-inv); animation-timing-function: cubic-bezier(.32,0,.24,1); }
  75.714%  { scale: 1; }
  100%     { scale: 1; }
}

/* Holds opaque until the editor has fully covered it, then CUTS.
   It used to fade 1 -> 0 across exactly the window qd-d faded 0 -> 1. Two layers
   crossing like that are never jointly opaque, so the base list showed through the
   seam — 25% of the composite at the midpoint. Fading the new layer in over an
   opaque old one, then cutting the old one, has no seam. */
@keyframes qd-details {
  0%,
  8.929%   { opacity: 0; animation-timing-function: cubic-bezier(.4,0,.2,1); }
  12.857%  { opacity: 1; animation-timing-function: linear; }
  45.714%  { opacity: 1; animation-timing-function: steps(1, start); }
  45.814%  { opacity: 0; }
  100%     { opacity: 0; }
}

/* Both columns move on the same beats; only the distances differ. */
@keyframes qd-scroll-main {
  0%,
  16.786%  { transform: translateY(0);                     animation-timing-function: cubic-bezier(.36,0,.22,1); }
  24.643%  { transform: translateY(var(--qd-main-scroll)); }
  45.714%  { transform: translateY(var(--qd-main-scroll)); animation-timing-function: linear; }
  49.643%  { transform: translateY(0); }   /* reset while the layer is already hidden */
  100%     { transform: translateY(0); }
}

@keyframes qd-scroll-rail {
  0%,
  16.786%  { transform: translateY(0);                     animation-timing-function: cubic-bezier(.36,0,.22,1); }
  24.643%  { transform: translateY(var(--qd-rail-scroll)); }
  45.714%  { transform: translateY(var(--qd-rail-scroll)); animation-timing-function: linear; }
  49.643%  { transform: translateY(0); }
  100%     { transform: translateY(0); }
}

/* The plain editor stays opaque underneath the styled one, so the design
   transition never lets the layers below bleed through. */
@keyframes qd-editor {
  0%,
  41.786%  { opacity: 0; animation-timing-function: cubic-bezier(.4,0,.2,1); }
  45.714%  { opacity: 1; }
  55.500%  { opacity: 1; animation-timing-function: steps(1, start); }
  55.600%  { opacity: 0; }
  100%     { opacity: 0; }
}

@keyframes qd-styled {
  0%,
  49.643%  { opacity: 0; animation-timing-function: cubic-bezier(.4,0,.2,1); }
  55.000%  { opacity: 1; }
  95.714%  { opacity: 1; animation-timing-function: cubic-bezier(.4,0,.2,1); }
  99.286%  { opacity: 0; }
  100%     { opacity: 0; }
}

@keyframes qd-modal {
  0%,
  74.643%  { opacity: 0; animation-timing-function: cubic-bezier(.32,0,.24,1); }
  78.929%  { opacity: 1; }
  92.857%  { opacity: 1; animation-timing-function: cubic-bezier(.4,0,.2,1); }
  95.714%  { opacity: 0; }
  100%     { opacity: 0; }
}

@keyframes qd-cursor {
  0%,
  3.214%   { transform: translate(var(--qd-park-x), var(--qd-park-y));           animation-timing-function: cubic-bezier(.42,0,.22,1); }
  8.214%   { transform: translate(var(--qd-row-x), var(--qd-row-y));             animation-timing-function: linear; }
  12.857%  { transform: translate(var(--qd-row-x), var(--qd-row-y));             animation-timing-function: cubic-bezier(.4,0,.3,1); }
  16.786%  { transform: translate(var(--qd-park-x), var(--qd-park-y));           animation-timing-function: linear; }
  /* still through the scroll and the first zoom-in */
  32.857%  { transform: translate(var(--qd-park-x), var(--qd-park-y));           animation-timing-function: cubic-bezier(.42,0,.22,1); }
  36.786%  { transform: translate(var(--qd-edit-x), var(--qd-edit-y));           animation-timing-function: linear; }
  42.857%  { transform: translate(var(--qd-edit-x), var(--qd-edit-y));           animation-timing-function: cubic-bezier(.4,0,.3,1); }
  49.643%  { transform: translate(var(--qd-park-x), var(--qd-park-y));           animation-timing-function: linear; }
  /* still through the design transition and the second zoom-in */
  66.071%  { transform: translate(var(--qd-park-x), var(--qd-park-y));           animation-timing-function: cubic-bezier(.42,0,.22,1); }
  69.643%  { transform: translate(var(--qd-dl-x), var(--qd-dl-y));               animation-timing-function: linear; }
  82.857%  { transform: translate(var(--qd-dl-x), var(--qd-dl-y));               animation-timing-function: cubic-bezier(.42,0,.22,1); }
  /* sweeps down the open modal to its primary action */
  87.143%  { transform: translate(var(--qd-modal-dl-x), var(--qd-modal-dl-y));   animation-timing-function: linear; }
  92.857%  { transform: translate(var(--qd-modal-dl-x), var(--qd-modal-dl-y));   animation-timing-function: cubic-bezier(.4,0,.3,1); }
  99.286%  { transform: translate(var(--qd-park-x), var(--qd-park-y)); }
  100%     { transform: translate(var(--qd-park-x), var(--qd-park-y)); }
}

@keyframes qd-press {
  0%,      7.857%  { scale: 1; }
  8.214%           { scale: 0.82; }
  9.286%           { scale: 1; }
  36.429%          { scale: 1; }
  36.786%          { scale: 0.82; }
  37.857%          { scale: 1; }
  69.286%          { scale: 1; }
  69.643%          { scale: 0.82; }
  70.714%          { scale: 1; }
  86.786%          { scale: 1; }
  87.143%          { scale: 0.82; }
  88.214%          { scale: 1; }
  100%             { scale: 1; }
}

@keyframes qd-ring {
  0%,      8.100%  { opacity: 0;    scale: 0.30; animation-timing-function: cubic-bezier(.2,.7,.3,1); }
  8.214%           { opacity: 0.85; scale: 0.30; }
  11.786%          { opacity: 0;    scale: 1; }
  36.670%          { opacity: 0;    scale: 0.30; animation-timing-function: cubic-bezier(.2,.7,.3,1); }
  36.786%          { opacity: 0.85; scale: 0.30; }
  40.357%          { opacity: 0;    scale: 1; }
  69.530%          { opacity: 0;    scale: 0.30; animation-timing-function: cubic-bezier(.2,.7,.3,1); }
  69.643%          { opacity: 0.85; scale: 0.30; }
  73.214%          { opacity: 0;    scale: 1; }
  87.030%          { opacity: 0;    scale: 0.30; animation-timing-function: cubic-bezier(.2,.7,.3,1); }
  87.143%          { opacity: 0.85; scale: 0.30; }
  90.714%          { opacity: 0;    scale: 1; }
  100%             { opacity: 0;    scale: 1; }
}

/* --- paused, set by the page's IntersectionObserver ---------------------- */

.qd-preview.is-paused .qd-stage,
.qd-preview.is-paused .qd-details,
.qd-preview.is-paused .qd-strip,
.qd-preview.is-paused .qd-d,
.qd-preview.is-paused .qd-e,
.qd-preview.is-paused .qd-f,
.qd-preview.is-paused .qd-cursor-track,
.qd-preview.is-paused .qd-cursor-scale,
.qd-preview.is-paused .qd-arrow,
.qd-preview.is-paused .qd-ring {
  animation-play-state: paused;
}

/* --- reduced motion: hold the designed QR in the editor as a still ------- */

@media (prefers-reduced-motion: reduce) {
  .qd-stage, .qd-details, .qd-strip, .qd-d, .qd-e, .qd-f,
  .qd-cursor-track, .qd-cursor-scale, .qd-arrow, .qd-ring {
    animation: none !important;
  }
  .qd-details      { opacity: 0; }
  .qd-d, .qd-e     { opacity: 1; }
  .qd-f            { opacity: 0; }
  .qd-cursor-track { display: none; }
}
