/* Gracie in Color — the motion layer (2026-09-24, "make the site pop").

   Loaded after styles.css on every page, and kept in its own file on purpose:
   styles.css is the layout and the quiet design system, this is the fun on
   top of it. Deleting the <link> to this file (and the <script> for
   js/motion.js) takes the whole layer off and leaves the site exactly as it
   was, which is the fastest way to answer "did the new motion break this?".

   Rules this file keeps, because the README records what happened when they
   were broken:

   - Nothing here animates `transform` on an element that already animates it.
     The wordmark letters wave on `transform`, the carousel riders bob on it,
     the gallery card lifts on it, and a second transform animation on the
     same element replaces the first. Everything added here moves the
     individual `translate` / `scale` / `rotate` properties instead, which
     compose with `transform` rather than fighting it.
   - Nothing here rotates a pixel glyph at rest (the wordmark's letters go to
     mush), and nothing rotates a gallery card at rest (the hover caption's
     overflow clips it). The toss below is transient: it lands at 0.
   - Everything is off under reduced motion, switched off at the source rather
     than left to the blanket rule in styles.css, which only collapses
     durations and freezes an infinite animation on a random frame.
   - Arrival animations wait for `.gic-pending` to leave <html> (render.js,
     "Arrival"), so they play on the page the visitor sees, not on the hidden
     one underneath the hold. */

/* ---------- 1. The hero arrives ----------
   The wordmark's letters drop in one at a time and land with a bounce, then
   the wave they already had carries on. The canopy unrolls, the animals swing
   in on their poles, and the motto pops last, so the first screen assembles
   itself in about a second and a half instead of simply being there.

   Keyed on `html:not(.gic-pending)`: the animation starts the moment render.js
   lifts the hold, which is the moment <main> becomes visible. Before that the
   rules don't match, so nothing plays behind the hold and gets wasted. The
   wave is restated in the same list because `animation` is one property: the
   hero-wave entry keeps its name, so Chrome keeps the running animation rather
   than restarting it. */
@media (prefers-reduced-motion: no-preference) {
  html:not(.gic-pending) .hero-pixel .hero-name .hn-word > span {
    animation:
      gic-letter-drop 820ms cubic-bezier(0.34, 1.56, 0.64, 1) calc(var(--i, 0) * 55ms + 120ms) both,
      hero-wave var(--wave-dur) linear calc(var(--i, 0) * var(--wave-dur) / -8) infinite;
  }
  html:not(.gic-pending) .hero-pixel .hero-motto {
    animation:
      gic-motto-pop 700ms cubic-bezier(0.34, 1.56, 0.64, 1) 900ms both,
      motto-drift 6s ease-in-out infinite;
  }
  html:not(.gic-pending) .hero-pixel .carousel-canopy {
    animation: gic-canopy-unroll 800ms cubic-bezier(0.65, 0, 0.35, 1) 450ms both;
  }
  html:not(.gic-pending) .hero-pixel .mount {
    animation: gic-mount-swing 900ms cubic-bezier(0.34, 1.56, 0.64, 1) calc(var(--m, 0) * 70ms + 750ms) both;
  }
}
.mount:nth-child(1) { --m: 0; }
.mount:nth-child(2) { --m: 1; }
.mount:nth-child(3) { --m: 2; }
.mount:nth-child(4) { --m: 3; }
.mount:nth-child(5) { --m: 4; }
.mount:nth-child(6) { --m: 5; }

/* translate and scale only (see the top of the file). The squash on landing is
   what makes it read as a drop rather than a fade: stretched while it falls,
   flattened for a beat when it lands, then itself. Transient, so the pixel
   blocks are only ever resampled mid-flight. */
@keyframes gic-letter-drop {
  0%   { translate: 0 -1.6em; scale: 0.7 1.3; opacity: 0; }
  45%  { opacity: 1; }
  70%  { translate: 0 0.08em; scale: 1.15 0.85; }
  100% { translate: 0 0; scale: 1; opacity: 1; }
}
@keyframes gic-motto-pop {
  0%   { scale: 0.4; opacity: 0; }
  100% { scale: 1; opacity: 1; }
}
/* Left to right, the way an awning is pulled across. The bottom inset is
   negative so the scallops (::after, hanging below the box) and the shadow
   are not clipped off while it runs. */
@keyframes gic-canopy-unroll {
  0%   { clip-path: inset(0 100% -1.5rem 0); }
  100% { clip-path: inset(0 0 -1.5rem 0); }
}
/* On .mount, the <li>. The rider inside bobs on transform and the img lifts
   on hover, so the li is the one element on that stack with nothing moving it
   already. */
@keyframes gic-mount-swing {
  0%   { translate: 0 -3rem; rotate: -8deg; opacity: 0; }
  60%  { opacity: 1; }
  100% { translate: 0 0; rotate: 0deg; opacity: 1; }
}

/* ---------- 2. The hero leaves as you scroll ----------
   Scroll-driven, so it costs no script and it is exactly as fast as the
   visitor's thumb. The wordmark drifts down and away slower than the page
   (parallax), and the carousel follows a little behind it. `translate` does not
   move layout, which matters: render.js's anchor jumps measure where #gallery
   is, and nothing here changes that. Browsers without scroll timelines skip
   the block and the hero scrolls away normally. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .hero-pixel .hero-copy {
      animation: gic-hero-exit linear both;
      animation-timeline: view();
      animation-range: exit 0% exit 100%;
    }
    .hero-pixel .carousel {
      animation: gic-carousel-exit linear both;
      animation-timeline: view();
      animation-range: exit 0% exit 100%;
    }
  }
}
@keyframes gic-hero-exit {
  to { translate: 0 45%; scale: 0.92; opacity: 0.1; }
}
/* A fixed distance, not a percentage: on a phone the carousel wraps into three
   rows and 20% of that height slid its bottom labels under the ribbon while
   they were still on screen. Phones skip it (below) for the same reason. */
@keyframes gic-carousel-exit {
  to { translate: 0 3rem; opacity: 0.35; }
}
@media (max-width: 999px) {
  .hero-pixel .carousel { animation: none; }
}

/* ---------- 3. The ribbon ----------
   Two crossed tapes of her words scrolling in opposite directions at the seam
   between the hero and the gallery. The words are site.json > ticker (Home page
   tab), drawn by js/motion.js, which also speeds the tapes up while the page
   is being scrolled and turns them round when the scroll does.

   Silkscreen is display type, and this is display: a handful of short words,
   big, repeated. letter-spacing is reset to 0 as the design notes require for
   any new Silkscreen setting.

   The crossed tapes are rotated, so they overhang the viewport. The wrapper
   clips that with overflow: clip (never hidden, which would make it a scroll
   container) or the page gains a horizontal scrollbar at every width. */
.ticker {
  position: relative;
  overflow: clip;
  padding: clamp(1.75rem, 4vw, 3rem) 0;
  margin-top: calc(-1 * var(--space-3));
}
.ticker[hidden] { display: none; }
/* The band is what is rotated, and it is only a little wider than the screen:
   rotating the tape itself (several screens wide) swung its visible end tens
   of pixels off the line, so the two never crossed where they should. The
   words slide inside the band instead. */
.ticker-band {
  position: relative;
  width: 110vw;
  margin-left: -5vw;
  overflow: clip;
}
.ticker-band.is-front {
  background: var(--pink-600);
  color: #FFFFFF;
  rotate: -2.2deg;
  box-shadow: 0 6px 18px rgba(122, 30, 90, 0.22);
  z-index: 1;
}
.ticker-band.is-back {
  position: absolute;
  top: 50%;
  left: 0;
  translate: 0 -50%;
  background: var(--pink-200);
  color: var(--pink-600);
  rotate: 2.4deg;
}
.ticker-tape {
  display: flex;
  width: max-content;
  font-family: var(--font-pixel);
  font-weight: 700;
  font-size: clamp(0.95rem, 2.4vw, 1.5rem);
  letter-spacing: 0;
  text-transform: uppercase;
  line-height: 1;
  white-space: nowrap;
  user-select: none;
}
.ticker-run {
  display: flex;
  flex: none;
  list-style: none;
  margin: 0;
  padding: 0.7em 0;
}
.ticker-run li {
  display: flex;
  align-items: center;
}
/* A little pixel star between the words, drawn rather than typed: Silkscreen
   has no dingbats, and a fallback glyph would change face mid-line. */
.ticker-run li::after {
  content: "";
  width: 0.55em;
  height: 0.55em;
  margin: 0 1.1em;
  background: currentColor;
  clip-path: polygon(50% 0, 62% 38%, 100% 50%, 62% 62%, 50% 100%, 38% 62%, 0 50%, 38% 38%);
}
/* The loop: two identical runs side by side, the pair slid left by exactly one
   run's width, so the seam never shows. js/motion.js adds the second run and
   drives playbackRate; the animation itself is plain CSS so it keeps going
   even if the script stops caring. */
@media (prefers-reduced-motion: no-preference) {
  .is-front .ticker-tape { animation: gic-ticker 38s linear infinite; }
  .is-back .ticker-tape  { animation: gic-ticker 46s linear infinite reverse; }
}
@keyframes gic-ticker {
  from { transform: translateX(0); }
  to   { transform: translateX(var(--run-w, -50%)); }
}
@media (max-width: 640px) {
  .ticker { margin-top: 0; padding: 1.25rem 0; }
}

/* ---------- 4. Headings get a scribble ----------
   One long unbroken wave (1200 units, 120 humps, about 32em at this height),
   left-aligned and cut off by the heading's width, never a repeating tile.
   The first version tiled a short wave, and every tile seam showed as a
   break in the line every few humps (Carson, 2026-09-24): the tile ended at
   a different height from where it began, and even a periodic tile leaves a
   faint anti-aliasing seam at fractional pixel positions.
   A hand-drawn pink underline under the home page's section headings, drawn
   left to right as the heading scrolls in. Pink is the home page's energy
   (design notes), so this is scoped to index-only sections.

   It rides the existing scroll reveal rather than adding a fifth observer:
   the undrawn state is `.reveal:not(.is-visible)`, which only ever exists while
   JS has armed it, so with no JS, reduced motion, or a heading already on
   screen at arrival, the underline is simply there. When the reveal strips its
   classes afterwards the resting state is "drawn", so nothing jumps. */
.gallery > h2,
.signup > h2,
.requests-inner > h2,
.shop-callout > h2 {
  width: fit-content;
  max-width: 100%;
}
.signup > h2,
.shop-callout > h2 { margin-left: auto; margin-right: auto; }
.gallery > h2::after,
.signup > h2::after,
.requests-inner > h2::after,
.shop-callout > h2::after {
  content: "";
  display: block;
  height: 0.32em;
  margin-top: 0.08em;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 12'%3E%3Cpath d='M0 6 Q5 0 10 6 T20 6 T30 6 T40 6 T50 6 T60 6 T70 6 T80 6 T90 6 T100 6 T110 6 T120 6 T130 6 T140 6 T150 6 T160 6 T170 6 T180 6 T190 6 T200 6 T210 6 T220 6 T230 6 T240 6 T250 6 T260 6 T270 6 T280 6 T290 6 T300 6 T310 6 T320 6 T330 6 T340 6 T350 6 T360 6 T370 6 T380 6 T390 6 T400 6 T410 6 T420 6 T430 6 T440 6 T450 6 T460 6 T470 6 T480 6 T490 6 T500 6 T510 6 T520 6 T530 6 T540 6 T550 6 T560 6 T570 6 T580 6 T590 6 T600 6 T610 6 T620 6 T630 6 T640 6 T650 6 T660 6 T670 6 T680 6 T690 6 T700 6 T710 6 T720 6 T730 6 T740 6 T750 6 T760 6 T770 6 T780 6 T790 6 T800 6 T810 6 T820 6 T830 6 T840 6 T850 6 T860 6 T870 6 T880 6 T890 6 T900 6 T910 6 T920 6 T930 6 T940 6 T950 6 T960 6 T970 6 T980 6 T990 6 T1000 6 T1010 6 T1020 6 T1030 6 T1040 6 T1050 6 T1060 6 T1070 6 T1080 6 T1090 6 T1100 6 T1110 6 T1120 6 T1130 6 T1140 6 T1150 6 T1160 6 T1170 6 T1180 6 T1190 6 T1200 6' fill='none' stroke='%23F06BC0' stroke-width='3.2' stroke-linecap='round'/%3E%3C/svg%3E") left center / auto 100% no-repeat;
  clip-path: inset(0 0 0 0);
  transition: clip-path 1100ms cubic-bezier(0.65, 0, 0.35, 1) 350ms;
}
.gallery > h2.reveal:not(.is-visible)::after,
.signup > h2.reveal:not(.is-visible)::after,
.requests-inner.reveal:not(.is-visible) > h2::after,
.shop-callout > h2.reveal:not(.is-visible)::after {
  clip-path: inset(0 100% 0 0);
}

/* ---------- 5. Gallery pieces are tossed onto the table ----------
   The reveal every grid item already gets, made bigger: each card comes in
   from lower down, a little small and tipped one way or the other, and lands
   square with a bounce. :not(.is-visible) because this file loads after
   styles.css and `.gallery-item.reveal` would otherwise outrank the resting
   `.reveal.is-visible { transform: none }`.

   The tip is a transient rotation of .gallery-item itself, the element with
   the overflow, so nothing inside it is clipped; at rest it is square, as the
   design notes require. */
@media (prefers-reduced-motion: no-preference) {
  .gallery-item.reveal {
    transition:
      opacity 500ms ease,
      transform 950ms cubic-bezier(0.34, 1.56, 0.64, 1);
    transition-delay: var(--reveal-delay, 0ms);
  }
  .gallery-item.reveal:not(.is-visible) {
    transform: translateY(70px) rotate(var(--toss, -4deg)) scale(0.86);
  }
  .gallery-item:nth-child(3n + 2) { --toss: 3deg; }
  .gallery-item:nth-child(3n + 3) { --toss: -2deg; }
  .gallery-item:nth-child(4n + 4) { --toss: 5deg; }
}

/* ---------- 6. A sheen follows the pointer over a piece ----------
   Light catching the surface of the print. It moves nothing, so it doesn't
   compete with the lift the card already does on hover (the design notes rule
   out tilt on the gallery). --gx/--gy come from js/motion.js. Fine pointers
   only: a touch has no hover to catch the light with. */
@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  .gallery-item::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 2;
    border-radius: 0.5rem;
    pointer-events: none;
    background: radial-gradient(
      circle at var(--gx, 50%) var(--gy, 30%),
      rgba(255, 255, 255, 0.55),
      rgba(255, 255, 255, 0) 42%);
    mix-blend-mode: soft-light;
    opacity: 0;
    transition: opacity 250ms ease;
  }
  .gallery-item:hover::after { opacity: 1; }
}

/* ---------- 6b. Each piece glows in its frame colour ----------
   (2026-09-25, with the wash in section 7.) A soft shadow in the card's own
   --frame colour, breathing slowly and out of step with its neighbours, and
   brighter while the card is lifted. It replaces the plain pink-ink hover
   shadow from styles.css.

   Which element carries it depends on the caption setting. With the caption
   under the piece, the mat (.gallery-open) casts it, because the item also
   holds the caption and a glow round that would frame the words too. With the
   title-on-hover overlay, .reveal-hover .gallery-item is overflow: hidden and
   would clip a shadow cast by anything inside it, so the item casts it: its
   own shadow is outside its own clip.

   The breathing is two registered numbers, not a box-shadow keyframe, so the
   hover can add to it without fighting the running animation. Without
   @property support the glow is simply steady. Reduced motion keeps the
   steady glow and the hover step, and drops only the breathing. */
@property --glow {
  syntax: "<number>";
  inherits: true;
  initial-value: 0.45;
}
@property --glow-lift {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}
.gallery-item {
  --glow-shadow: 0 14px 32px -14px
    color-mix(in srgb, var(--frame, var(--pink-200)) calc((var(--glow) + var(--glow-lift)) * 100%), transparent);
  transition: transform 220ms ease, --glow-lift 300ms ease;
}
.gallery-item:hover,
.gallery-item:has(.gallery-open:focus-visible) { --glow-lift: 0.5; }
.gallery-open,
.gallery-item:hover .gallery-open,
.gallery-open:focus-visible { box-shadow: var(--glow-shadow); }
/* The same gate as the overlay itself in styles.css: under it, the caption
   sits below the piece and the mat keeps the glow. */
@media (min-width: 640px) and (hover: hover) {
  .reveal-hover .gallery-item { box-shadow: var(--glow-shadow); }
  .reveal-hover .gallery-item .gallery-open { box-shadow: none; }
}
@media (prefers-reduced-motion: no-preference) {
  .gallery-item { animation: gic-glow 6.5s ease-in-out infinite alternate; }
  .gallery-item:nth-child(3n + 2) { animation-delay: -2.2s; }
  .gallery-item:nth-child(3n + 3) { animation-delay: -4.4s; }
  .gallery-item:nth-child(4n + 4) { animation-duration: 8s; }
}
@keyframes gic-glow {
  from { --glow: 0.3; }
  to   { --glow: 0.75; }
}

/* ---------- 7. Skies: stars and floating doodles ----------
   Any section carrying data-sky gets a decorative layer built by js/motion.js
   behind its content: data-sky="stars" is the night sky in the newsletter
   band, data-sky="doodles" is hearts, flowers and sparkles drifting up
   through the requests band and the shop nudge, in the gallery frames' six
   colours, and data-sky="wash" is the soft colour behind the gallery grid
   (its own note is by .sky-blob below). data-sky-density scales how many.

   The layer is aria-hidden, pointer-events: none and z-index -1 inside an
   isolated section, so it can never cover a field or take a click.

   Three ways it moves, on three properties that don't fight:
   - The pointer parallax moves `.sky-depth`, three wrappers (far, middle,
     near) that motion.js sorts the bits into by their --z, eased in script
     so near stars swing further than far ones. Never the bits themselves:
     restyling a few dozen running animations on every mouse move is what
     made the clouds stutter under the pointer (2026-09-25).
   - `transform` on a doodle is its drift up the band; on a star, `opacity`
     and `scale` are its twinkle.
   - The whole layer rides a scroll timeline, so the sky moves at a different
     speed from the page. The layer overhangs the section by 8% top and
     bottom so that shift never shows an edge.

   Offscreen, motion.js adds .is-idle and every animation pauses. Under
   reduced motion the bits are still placed (the band still has its stars)
   but nothing twinkles, drifts or follows the pointer. */
[data-sky] {
  position: relative;
  overflow: clip;
  isolation: isolate;
}
/* The gallery's wash runs the full width of the page, past the section's
   centred column (js/motion.js sizes the layer to clientWidth), so that
   section clips top and bottom only. Clip, unlike hidden, may pair with
   visible on the other axis. The layer clips itself instead, at exactly the
   page's edges, so a blob can run off the side of the screen without
   scrolling the page sideways. */
[data-sky="wash"] {
  overflow-x: visible;
  overflow-y: clip;
}
.sky-wash { overflow: clip; }
.sky {
  position: absolute;
  inset: -8% 0;
  z-index: -1;
  pointer-events: none;
}
.sky-depth {
  position: absolute;
  inset: 0;
}
.sky-bit {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: var(--s);
  height: var(--s);
  background: var(--c);
  opacity: var(--o, 0.8);
  -webkit-mask: var(--shape) center / contain no-repeat;
          mask: var(--shape) center / contain no-repeat;
}
.shape-star   { --shape: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Cpath d='M5 0 6.2 3.8 10 5 6.2 6.2 5 10 3.8 6.2 0 5 3.8 3.8Z'/%3E%3C/svg%3E"); }
.shape-dot    { --shape: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Ccircle cx='5' cy='5' r='5'/%3E%3C/svg%3E"); }
.shape-heart  { --shape: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 21s-7.5-4.6-9.6-9.2C.8 8.3 2.9 4 6.9 4c2.2 0 3.7 1.2 5.1 3 1.4-1.8 2.9-3 5.1-3 4 0 6.1 4.3 4.5 7.8C19.5 16.4 12 21 12 21z'/%3E%3C/svg%3E"); }
.shape-flower { --shape: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='6' r='4.6'/%3E%3Ccircle cx='17.7' cy='10.2' r='4.6'/%3E%3Ccircle cx='15.5' cy='17' r='4.6'/%3E%3Ccircle cx='8.5' cy='17' r='4.6'/%3E%3Ccircle cx='6.3' cy='10.2' r='4.6'/%3E%3C/svg%3E"); }

@media (prefers-reduced-motion: no-preference) {
  .sky-stars .sky-bit {
    animation: gic-twinkle var(--t, 3s) ease-in-out var(--d, 0s) infinite;
  }
  .sky-doodles .sky-bit {
    animation: gic-drift var(--t, 20s) linear var(--d, 0s) infinite;
  }
  .sky-wash .sky-blob {
    animation: gic-wash var(--t, 30s) ease-in-out var(--d, 0s) infinite alternate;
  }
  .sky.is-idle .sky-bit,
  .sky.is-idle .sky-blob { animation-play-state: paused; }
  @supports (animation-timeline: view()) {
    .sky {
      animation: gic-sky-scroll linear both;
      animation-timeline: view();
    }
  }
}
@keyframes gic-twinkle {
  0%, 100% { opacity: calc(var(--o, 0.8) * 0.25); scale: 0.6; }
  50%      { opacity: var(--o, 0.8); scale: 1; }
}
/* Up the band from below its bottom edge to above its top, swaying and
   turning, fading at both ends so the loop's restart is never seen.
   --rise is the band's height in px (motion.js). */
@keyframes gic-drift {
  0%   { transform: translate(0, var(--rise)) rotate(0deg); opacity: 0; }
  12%  { opacity: var(--o, 0.7); }
  50%  { transform: translate(var(--sway, 20px), calc(var(--rise) * 0.5)) rotate(calc(var(--spin, 90deg) * 0.5)); }
  88%  { opacity: var(--o, 0.7); }
  100% { transform: translate(0, calc(var(--s) * -1.5)) rotate(var(--spin, 90deg)); opacity: 0; }
}
/* The gallery's watercolour wash (data-sky="wash", 2026-09-25, Carson: the
   gallery sat on flat white). Big soft blobs of the frame colours behind the
   grid, drifting and swelling over half a minute, so the mats read as part of
   the page rather than cards laid on paper. Faint on purpose: the design notes
   keep a quiet canvas behind the artwork, and this is a tint, not a colour
   field. The gradient fades to nothing inside the blob's own box, so the only
   hard cut a blob can meet is the screen's edge (section 7, above). No blur
   filter: a radial gradient is already soft, and it rasterises once, after
   which the drift is a compositor transform. */
.sky-blob {
  position: absolute;
  left: 0;
  top: 0;
  width: var(--s);
  height: var(--s);
  margin-top: calc(var(--s) / -2);
  border-radius: 50%;
  background: radial-gradient(closest-side, var(--c) 20%, color-mix(in srgb, var(--c) 55%, transparent) 60%, transparent);
  opacity: 0.85;
}
@keyframes gic-wash {
  from { transform: translate(0, 0) scale(1); }
  to   { transform: translate(var(--dx, 0), var(--dy, 0)) scale(var(--grow, 1.08)); }
}
@keyframes gic-sky-scroll {
  from { transform: translateY(-6%); }
  to   { transform: translateY(6%); }
}
/* A shooting star now and then (stars only; motion.js spawns and removes
   them with the Web Animations API). */
.sky-shoot {
  position: absolute;
  width: 140px;
  height: 2px;
  border-radius: 2px;
  background: linear-gradient(90deg, rgba(255, 255, 255, 0), #FFFFFF 70%, var(--pink-200));
  rotate: 24deg;
  transform-origin: right center;
  filter: drop-shadow(0 0 4px rgba(240, 107, 192, 0.9));
}

/* ---------- 8. Buttons have some spring ----------
   Every main button leans toward the pointer (js/motion.js sets --mag-x/-y,
   fine pointers only) and springs back when it leaves; a press squashes it.
   `translate` and `scale`, so the old `transform: scale(0.98)` press in
   styles.css and the tilt on shop cards both keep working underneath. The
   transition list restates the colours because `transition` is one property
   and this would otherwise drop them; .btn-art restates `filter` for the same
   reason. */
.btn {
  translate: var(--mag-x, 0) var(--mag-y, 0);
  transition:
    background 200ms ease,
    color 200ms ease,
    translate 450ms cubic-bezier(0.34, 1.56, 0.64, 1),
    scale 300ms cubic-bezier(0.34, 1.56, 0.64, 1),
    box-shadow 200ms ease;
}
.btn.btn-art {
  transition:
    filter 200ms ease,
    translate 450ms cubic-bezier(0.34, 1.56, 0.64, 1),
    scale 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.btn.is-magnetic { transition-duration: 200ms, 200ms, 120ms, 300ms, 200ms; }
.btn.btn-art.is-magnetic { transition-duration: 200ms, 120ms, 300ms; }
/* The hover shadow is ink everywhere (the quiet pages stay near-black on
   white) and pink only in the home page's own sections, where pink is the
   energy the design notes put there. */
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .btn-primary:hover:not(.btn-art) {
    box-shadow: 0 5px 0 -1px rgba(24, 24, 27, 0.28);
  }
  .signup .btn-primary:hover,
  .requests .btn-primary:hover,
  .shop-callout .btn-primary:hover { box-shadow: 0 6px 0 -1px var(--pink-400); }
}
@media (prefers-reduced-motion: no-preference) {
  .btn:active { scale: 0.94; }
}

/* ---------- 9. Learn: a landscape that breathes (2026-09-25) ----------
   The Learn page's scene and each note's hero carry data-sky="nature", and
   js/motion.js fills them with four kinds of bit, each keeping to its own
   stretch of the landscape: clouds and the odd bird cross the sky, leaves fall
   through the trees, fireflies hang over the dirt. The static scenery (the
   wavy edges, the sun, the grass) is in styles.css, "Learn page", so it stays
   when this layer comes off; this section only moves it.

   Transform-property bookkeeping, as everywhere in this file:
   - A bit's `transform` is its travel (cross, fall, wander); the pointer
     parallax is on its `.sky-depth` wrapper (section 7), not on the bit. A
     bird's `scale` is its flap; a firefly's `scale` and `opacity` are its
     twinkle.
   - A cut-out's <li> takes the pointer parallax on `translate` and the
     reveal's toss on `transform`. The link keeps its tilt and hover lift on
     `transform`. The <img> bobs on `translate` and `rotate`. The tag swings on
     `rotate` alone, around its tape.
   - A fact card leans on `rotate` at rest (styles.css), so its reveal stays on
     `transform` and the two never meet. */
.shape-cloud { --shape: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 33'%3E%3Ccircle cx='17' cy='21' r='11'/%3E%3Ccircle cx='31' cy='14' r='13'/%3E%3Ccircle cx='45' cy='20' r='10'/%3E%3Crect x='8' y='20' width='46' height='13' rx='6.5'/%3E%3C/svg%3E"); }
.shape-bird  { --shape: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 9'%3E%3Cpath d='M0 3.5 Q5 0 10 5 Q15 0 20 3.5 Q15 2.2 10 8.5 Q5 2.2 0 3.5Z'/%3E%3C/svg%3E"); }
.shape-leaf  { --shape: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 1C5 6 3.5 13 6.5 19.5L5 23l1.6.5 1.3-3C10 22.8 14 22.8 17.5 19.5 20.5 13 19 6 12 1z'/%3E%3C/svg%3E"); }
.nat-cloud { height: calc(var(--s) * 0.55); }
.nat-bird  { height: calc(var(--s) * 0.45); }
/* A firefly is light, not a shape: no mask, a soft glow drawn as a gradient
   (a filter or box-shadow on a few dozen moving bits would cost far more). */
.nat-glow {
  -webkit-mask: none;
          mask: none;
  border-radius: 50%;
  background: radial-gradient(circle, #FFF7C9 0 16%, var(--c) 30%, rgba(246, 213, 92, 0) 68%);
}

@media (prefers-reduced-motion: no-preference) {
  .sky-nature .nat-cloud {
    animation: gic-cross var(--t, 90s) linear var(--d, 0s) infinite;
  }
  .sky-nature .nat-bird {
    animation:
      gic-cross var(--t, 30s) linear var(--d, 0s) infinite,
      gic-flap var(--flap, 0.45s) ease-in-out infinite alternate;
  }
  .sky-nature .nat-leaf {
    animation: gic-fall var(--t, 16s) linear var(--d, 0s) infinite;
  }
  .sky-nature .nat-glow {
    animation:
      gic-wander var(--wt, 10s) ease-in-out var(--d, 0s) infinite alternate,
      gic-twinkle var(--t, 3s) ease-in-out var(--d, 0s) infinite;
  }
  /* The sun breathes and the grass leans in the wind. The grass is one strip
     sheared from its roots, which reads as a breeze at this size; per-blade
     motion would be a pile of elements for a line most people glance at. */
  .learn-scene::before { animation: gic-sun 9s ease-in-out infinite alternate; }
  .learn-scene::after  { animation: gic-breeze 5.5s ease-in-out infinite alternate; }
}
@keyframes gic-cross {
  from { transform: translateX(calc(var(--s) * -1)); }
  to   { transform: translateX(var(--across, 100vw)); }
}
@keyframes gic-flap {
  from { scale: 1 1; }
  to   { scale: 1 0.3; }
}
/* Down from the trees, swinging out and back as it goes, turning over, and
   fading at both ends so the restart at the top is never seen. */
@keyframes gic-fall {
  0%   { transform: translate(0, 0) rotate(0deg); opacity: 0; }
  10%  { opacity: var(--o, 0.8); }
  35%  { transform: translate(var(--sway, 30px), calc(var(--fall, 300px) * 0.33)) rotate(calc(var(--spin, 360deg) * 0.35)); }
  65%  { transform: translate(calc(var(--sway, 30px) * -0.5), calc(var(--fall, 300px) * 0.66)) rotate(calc(var(--spin, 360deg) * 0.65)); }
  88%  { opacity: var(--o, 0.8); }
  100% { transform: translate(calc(var(--sway, 30px) * 0.3), var(--fall, 300px)) rotate(var(--spin, 360deg)); opacity: 0; }
}
@keyframes gic-wander {
  0%   { transform: translate(0, 0); }
  50%  { transform: translate(var(--wx, 20px), calc(var(--wy, 10px) * -1)); }
  100% { transform: translate(calc(var(--wx, 20px) * -0.6), var(--wy, 10px)); }
}
@keyframes gic-sun {
  from { scale: 1; opacity: 0.85; }
  to   { scale: 1.1; opacity: 1; }
}
@keyframes gic-breeze {
  from { transform: skewX(-4deg); }
  to   { transform: skewX(5deg); }
}

/* The cut-outs. Each one bobs on its own clock, as if hung from a thread in a
   breeze, and leans away from the pointer by its own depth: js/motion.js
   writes `translate` on each <li> from the same eased loop as the sky's depth
   wrappers (section 7), never through a variable the bob would have to
   re-read. Mouse only. */
@media (prefers-reduced-motion: no-preference) {
  .cutout:nth-child(3n+1) { --bt: 4.6s; --bd: -1.2s; }
  .cutout:nth-child(3n+2) { --bt: 5.8s; --bd: -3.1s; }
  .cutout:nth-child(3n)   { --bt: 5.1s; --bd: -0.4s; }
  .cutout-link img {
    animation: gic-bob var(--bt, 5s) ease-in-out var(--bd, 0s) infinite alternate;
  }
  /* The tag swings on its tape when the cut-out is pointed at, and settles. */
  .cutout-link:hover .cutout-name,
  .cutout-link:focus-visible .cutout-name {
    animation: gic-tag-swing 1100ms cubic-bezier(0.34, 1.2, 0.64, 1);
  }

  /* Tossed onto the scene as they scroll in, like the gallery cards (section
     5), from lower down and further round, since a cut-out has no mat to
     keep square. The reveal is on the <li>, so the tilt on the link and the
     bob on the img carry on underneath it. */
  .cutout.reveal {
    transition:
      opacity 500ms ease,
      transform 1000ms cubic-bezier(0.34, 1.56, 0.64, 1);
    transition-delay: var(--reveal-delay, 0ms);
  }
  .cutout.reveal:not(.is-visible) {
    transform: translateY(90px) rotate(var(--toss, -8deg)) scale(0.8);
  }
  .cutout:nth-child(even) { --toss: 7deg; }

  /* Arrival (render.js "Arrival" lifts .gic-pending): the heading rises, its
     scribble is drawn, and the intro follows. */
  html:not(.gic-pending) .learn-heading {
    animation: gic-rise 700ms cubic-bezier(0.22, 1, 0.36, 1) both;
  }
  html:not(.gic-pending) .learn-heading::after {
    animation: gic-draw 1100ms cubic-bezier(0.65, 0, 0.35, 1) 350ms both;
  }
  html:not(.gic-pending) .learn-intro {
    animation: gic-rise 700ms cubic-bezier(0.22, 1, 0.36, 1) 120ms both;
  }
}
@keyframes gic-bob {
  from { translate: 0 0;     rotate: -1.2deg; }
  to   { translate: 0 -12px; rotate: 1.4deg; }
}
@keyframes gic-tag-swing {
  0%   { rotate: var(--tag-r, 0deg); }
  20%  { rotate: calc(var(--tag-r, 0deg) + 9deg); }
  45%  { rotate: calc(var(--tag-r, 0deg) - 6deg); }
  70%  { rotate: calc(var(--tag-r, 0deg) + 3deg); }
  100% { rotate: var(--tag-r, 0deg); }
}
@keyframes gic-rise {
  from { translate: 0 24px; opacity: 0; }
  to   { translate: 0 0;    opacity: 1; }
}
@keyframes gic-draw {
  from { clip-path: inset(0 100% 0 0); }
  to   { clip-path: inset(0 0 0 0); }
}

/* The page heading's scribble, in leaf green: the home page's pink one would
   be pink on Learn, which keeps the nature palette (design notes). Drawn once
   on arrival rather than on a reveal: the heading is always on the first
   screen. The resting state is drawn, so no JS or reduced motion shows it. */
.learn-heading { width: fit-content; max-width: 100%; }
.learn-heading::after {
  content: "";
  display: block;
  height: 0.16em;
  margin-top: 0.06em;
  background: #5E9E6E;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 12'%3E%3Cpath d='M0 6 Q5 0 10 6 T20 6 T30 6 T40 6 T50 6 T60 6 T70 6 T80 6 T90 6 T100 6 T110 6 T120 6 T130 6 T140 6 T150 6 T160 6 T170 6 T180 6 T190 6 T200 6 T210 6 T220 6 T230 6 T240 6 T250 6 T260 6 T270 6 T280 6 T290 6 T300 6 T310 6 T320 6 T330 6 T340 6 T350 6 T360 6 T370 6 T380 6 T390 6 T400 6 T410 6 T420 6 T430 6 T440 6 T450 6 T460 6 T470 6 T480 6 T490 6 T500 6 T510 6 T520 6 T530 6 T540 6 T550 6 T560 6 T570 6 T580 6 T590 6 T600 6 T610 6 T620 6 T630 6 T640 6 T650 6 T660 6 T670 6 T680 6 T690 6 T700 6 T710 6 T720 6 T730 6 T740 6 T750 6 T760 6 T770 6 T780 6 T790 6 T800 6 T810 6 T820 6 T830 6 T840 6 T850 6 T860 6 T870 6 T880 6 T890 6 T900 6 T910 6 T920 6 T930 6 T940 6 T950 6 T960 6 T970 6 T980 6 T990 6 T1000 6 T1010 6 T1020 6 T1030 6 T1040 6 T1050 6 T1060 6 T1070 6 T1080 6 T1090 6 T1100 6 T1110 6 T1120 6 T1130 6 T1140 6 T1150 6 T1160 6 T1170 6 T1180 6 T1190 6 T1200 6' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round'/%3E%3C/svg%3E") left center / auto 100% no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 12'%3E%3Cpath d='M0 6 Q5 0 10 6 T20 6 T30 6 T40 6 T50 6 T60 6 T70 6 T80 6 T90 6 T100 6 T110 6 T120 6 T130 6 T140 6 T150 6 T160 6 T170 6 T180 6 T190 6 T200 6 T210 6 T220 6 T230 6 T240 6 T250 6 T260 6 T270 6 T280 6 T290 6 T300 6 T310 6 T320 6 T330 6 T340 6 T350 6 T360 6 T370 6 T380 6 T390 6 T400 6 T410 6 T420 6 T430 6 T440 6 T450 6 T460 6 T470 6 T480 6 T490 6 T500 6 T510 6 T520 6 T530 6 T540 6 T550 6 T560 6 T570 6 T580 6 T590 6 T600 6 T610 6 T620 6 T630 6 T640 6 T650 6 T660 6 T670 6 T680 6 T690 6 T700 6 T710 6 T720 6 T730 6 T740 6 T750 6 T760 6 T770 6 T780 6 T790 6 T800 6 T810 6 T820 6 T830 6 T840 6 T850 6 T860 6 T870 6 T880 6 T890 6 T900 6 T910 6 T920 6 T930 6 T940 6 T950 6 T960 6 T970 6 T980 6 T990 6 T1000 6 T1010 6 T1020 6 T1030 6 T1040 6 T1050 6 T1060 6 T1070 6 T1080 6 T1090 6 T1100 6 T1110 6 T1120 6 T1130 6 T1140 6 T1150 6 T1160 6 T1170 6 T1180 6 T1190 6 T1200 6' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round'/%3E%3C/svg%3E") left center / auto 100% no-repeat;
}

/* ---------- 10. Learn: one note arrives ----------
   The artwork drops onto its band and bobs, the name rises in after it, and a
   line along the foot of the sticky header fills as the note is read. */
.note-progress { display: none; }
@media (prefers-reduced-motion: no-preference) {
  html:not(.gic-pending) .note-hero .note-figure {
    animation: gic-note-drop 950ms cubic-bezier(0.34, 1.4, 0.64, 1) 120ms both;
  }
  .note-hero .note-figure img {
    animation: gic-bob 5.4s ease-in-out -1.5s infinite alternate;
  }
  html:not(.gic-pending) .note-headline > * {
    animation: gic-rise 700ms cubic-bezier(0.22, 1, 0.36, 1) both;
  }
  html:not(.gic-pending) .note-headline > :nth-child(1) { animation-delay: 250ms; }
  html:not(.gic-pending) .note-headline > :nth-child(2) { animation-delay: 360ms; }
  html:not(.gic-pending) .note-headline > :nth-child(3) { animation-delay: 470ms; }

  /* The link rows nudge toward where they go; the trail cards' cut-outs hop. */
  .note-links a { transition: border-color 200ms ease, background 200ms ease, translate 350ms cubic-bezier(0.34, 1.56, 0.64, 1); }
  .note-links a:hover,
  .note-links a:focus-visible { translate: 5px 0; }
  .note-step-art { transition: translate 400ms cubic-bezier(0.34, 1.56, 0.64, 1), rotate 400ms cubic-bezier(0.34, 1.56, 0.64, 1); }
  .note-step:hover .note-step-art { translate: 0 -5px; rotate: -8deg; }
  .note-step.next:hover .note-step-art { rotate: 8deg; }

  @supports (animation-timeline: scroll()) {
    .note-progress {
      display: block;
      position: absolute;
      left: 0;
      right: 0;
      bottom: -1px;
      height: 3px;
      background: #5E9E6E;
      transform-origin: 0 50%;
      pointer-events: none;
      animation: gic-progress linear both;
      animation-timeline: scroll(root);
    }
  }
}
@keyframes gic-note-drop {
  0%   { translate: 0 -60px; rotate: -10deg; scale: 0.85; opacity: 0; }
  50%  { opacity: 1; }
  100% { translate: 0 0; rotate: 0deg; scale: 1; opacity: 1; }
}
@keyframes gic-progress {
  from { scale: 0 1; }
  to   { scale: 1 1; }
}
