/* =========================================================================
   UBS Charitable Foundation — motion layer
   Cinematic, not flashy. A memorial should feel like it breathes, not perform.

   This file is entirely additive. Remove the <link> to it and the site returns
   to the quiet static version with nothing broken.

   Built on CSS scroll-driven animations (animation-timeline) where the browser
   supports them — those run on the compositor, off the main thread, and cost
   nothing in JavaScript. motion.js provides an equivalent for browsers that
   don't yet support them, and does nothing where they do.
   ========================================================================= */

/* Registered here at the top level — @property is invalid inside @media. */
@property --sweep {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 100%;
}

@property --lit {
  syntax: '<number>';
  inherits: true;
  initial-value: 0;
}

/* Everything below is inside this guard. Reduced motion gets the static site. */
@media (prefers-reduced-motion: no-preference) {

/* =========================================================================
   1. Scroll progress — a thin gold thread that fills as you read
   (Positioning lives in site.css; this only paints and fills it.)
   ========================================================================= */
.progress { background: linear-gradient(90deg, var(--gold), var(--gold-bright)); }
@supports (animation-timeline: scroll()) {
  .progress {
    animation: progressGrow linear both;
    animation-timeline: scroll(root block);
  }
}
@keyframes progressGrow { to { transform: scaleX(1); } }

/* =========================================================================
   3. Headline words rising behind their own mask
   ========================================================================= */
/* Scoped to headings only. The tribute quote also uses .w spans, but it is
   lit rather than raised — if the mask and its translate applied there too,
   the quote would depend on overriding a transform back out, and any rule
   that failed to land would leave the text sitting a full line below its own
   box. Never make legible layout depend on an override winning. */
h1 .w, h2 .w, h3 .w {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  padding-bottom: .08em;   /* room for descenders inside the mask */
  margin-bottom: -.08em;
}
h1 .w > span, h2 .w > span, h3 .w > span {
  display: inline-block;
  transform: translateY(105%);
  animation: wordRise .95s cubic-bezier(.19,1,.22,1) both;
  animation-delay: calc(var(--i) * 65ms + 120ms);
}
.w[data-em] > span { color: var(--blue); }

@keyframes wordRise { to { transform: translateY(0); } }

/* Headlines below the fold wait for the scroll instead of firing on load.
   Scoped to .js so that without JavaScript they are never held back — a
   paused animation on a masked word means permanently invisible text. */
.js .reveal-words .w > span { animation-play-state: paused; }
.js .reveal-words.is-in .w > span { animation-play-state: running; }

/* =========================================================================
   4. Hero — depth
   ========================================================================= */
.hero { position: relative; }

/* The hero photograph is deliberately NOT animated. Any Ken Burns push scales
   it past its frame, which crops the edges off — and this particular photograph
   is a wide group shot where the people at the edges are the point. */



/* Scroll cue */
.scroll-cue i::after {
  content: '';
  position: absolute;
  inset: 0 auto auto 0;
  width: 1px;
  height: 14px;
  background: var(--gold);
  animation: cueDrop 2.4s cubic-bezier(.65,0,.35,1) infinite;
}
@keyframes cueDrop {
  0%   { transform: translateY(-16px); opacity: 0; }
  35%  { opacity: 1; }
  100% { transform: translateY(44px); opacity: 0; }
}
@supports (animation-timeline: scroll()) {
  .scroll-cue { animation: cueFade linear both; animation-timeline: scroll(root block); animation-range: 0 40vh; }
  @keyframes cueFade { to { opacity: 0; } }
}

/* =========================================================================
   5. The tribute — the emotional centre of the homepage
   Structure is in site.css. This dims the quote and lets the scroll light it.
   ========================================================================= */
.tribute-portrait img { will-change: transform; }

.lit .w > span {
  color: rgba(250,246,239,.22);
  transition: color .5s var(--ease);
}
@supports (animation-timeline: view()) {
  .lit .w > span {
    animation: lightUp linear both;
    animation-timeline: view();
    /* Each word lights slightly later than the one before it */
    animation-range: entry 42% calc(entry 92% + var(--i) * 1.1%);
  }
  @keyframes lightUp { to { color: rgba(250,246,239,.97); } }
}
/* JS fallback adds .lit-on progressively */
.lit .w.lit-on > span { color: rgba(250,246,239,.97); }

/* =========================================================================
   6. Figures — numbers that count, and actually work
   ========================================================================= */
.figure-num { font-variant-numeric: tabular-nums; }
.figure-item {
  --line-scale: 0;
  position: relative;
}
.figure-item::before {
  content: '';
  position: absolute;
  inset: -1px 0 auto 0;
  height: 1px;
  background: var(--gold-bright);
  transform: scaleX(0);
  transform-origin: 0 50%;
}
@supports (animation-timeline: view()) {
  .figure-item::before {
    animation: ruleDraw linear both;
    animation-timeline: view();
    animation-range: entry 10% entry 85%;
  }
  @keyframes ruleDraw { to { transform: scaleX(1); } }
}
.figure-item.is-drawn::before { transform: scaleX(1); transition: transform .9s var(--ease); }

/* =========================================================================
   7. Programme images — a wipe, then a slow parallax inside the frame
   ========================================================================= */

@supports (animation-timeline: view()) {
  .ph {
    animation: wipeUp cubic-bezier(.33,1,.68,1) both;
    animation-timeline: view();
    animation-range: entry 4% entry 78%;
  }
  @keyframes wipeUp {
    from { clip-path: inset(100% 0 0 0); }
    to   { clip-path: inset(0 0 0 0); }
  }
  .programme-figure img,
  .story img,
  .gallery img {
    animation: driftUp linear both;
    animation-timeline: view();
    animation-range: entry 0% exit 100%;
  }
  @keyframes driftUp {
    from { transform: translateY(-4.5%) scale(1.12); }
    to   { transform: translateY(4.5%) scale(1.12); }
  }
  /* The gallery keeps its hover zoom, so exclude it from the drift transform */
  .gallery img { animation-name: driftUpSmall; }
  @keyframes driftUpSmall {
    from { transform: translateY(-2.5%) scale(1.07); }
    to   { transform: translateY(2.5%) scale(1.07); }
  }
  .gallery a:hover img { transform: scale(1.1); animation-play-state: paused; }
}

/* Banner parallax on inner pages */
@supports (animation-timeline: view()) {
  .banner img {
    animation: bannerPar linear both;
    animation-timeline: view();
    animation-range: entry 0% exit 100%;
  }
  @keyframes bannerPar {
    from { transform: translateY(-7%) scale(1.16); }
    to   { transform: translateY(7%) scale(1.16); }
  }
  .banner-text {
    animation: bannerText linear both;
    animation-timeline: view();
    animation-range: entry 60% exit 60%;
  }
  @keyframes bannerText {
    from { transform: translateY(0); opacity: 1; }
    to   { transform: translateY(-38px); opacity: .25; }
  }
}

/* =========================================================================
   8. Pillars — the accent rule draws itself in as each column arrives
/* The cards arrive from alternating sides — the first from the left, the next
   from the right, the third from the left — so the section reads as a zig-zag
   on the way down while every card keeps its photograph on the same side.
   This replaces the earlier stacking effect, where each card scaled down,
   dimmed and blurred as the next slid over it. */
@supports (animation-timeline: view()) {
  @media (min-width: 900px) {
    .pillar-card {
      animation: cardArrive cubic-bezier(.33, 1, .68, 1) both;
      animation-timeline: view();
      animation-range: entry 4% entry 62%;
      --from-x: -5vw;
    }
    .pillar-card:nth-child(even) { --from-x: 5vw; }

    @keyframes cardArrive {
      from { opacity: 0; transform: translate3d(var(--from-x), 1.5rem, 0); }
      to   { opacity: 1; transform: none; }
    }

    /* The photograph still drifts gently inside the card. */
    .pillar-media img {
      animation: pillarPan linear both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
    }
    @keyframes pillarPan {
      from { transform: scale(1.06) translateY(-1.6%); }
      to   { transform: scale(1.06) translateY(1.6%); }
    }
  }
}

/* =========================================================================
   9. Pointer glow on the dark bands
   ========================================================================= */
@media (hover: hover) and (pointer: fine) {
  .glow-surface { position: relative; isolation: isolate; }
  .glow-surface::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    opacity: 0;
    transition: opacity .5s var(--ease);
    background: radial-gradient(
      420px circle at var(--mx, 50%) var(--my, 50%),
      rgba(227, 169, 60, .14),
      transparent 68%
    );
  }
  .glow-surface:hover::after { opacity: 1; }
  .glow-surface > * { position: relative; z-index: 1; }
}

/* =========================================================================
   10. Magnetic buttons and link arrows
   ========================================================================= */
@media (hover: hover) and (pointer: fine) {
  .btn {
    transition: transform .45s var(--ease), background .3s var(--ease), color .3s var(--ease), box-shadow .45s var(--ease);
  }
  .btn:hover { box-shadow: 0 10px 30px -12px rgba(27,31,42,.45); }
  .btn--gold:hover { box-shadow: 0 10px 34px -10px rgba(198,139,38,.6); }
}

/* =========================================================================
   11. Section headings drift in
   ========================================================================= */
@supports (animation-timeline: view()) {
  .eyebrow {
    animation: eyebrowIn cubic-bezier(.33,1,.68,1) both;
    animation-timeline: view();
    animation-range: entry 6% entry 62%;
  }
  @keyframes eyebrowIn {
    from { opacity: 0; transform: translateX(-14px); }
    to   { opacity: 1; transform: none; }
  }
  .eyebrow::before {
    animation: numIn cubic-bezier(.33,1,.68,1) both;
    animation-timeline: view();
    animation-range: entry 6% entry 70%;
  }
  @keyframes numIn {
    from { opacity: 0; transform: translateY(8px) rotate(-8deg); }
    to   { opacity: 1; transform: none; }
  }
}

/* =========================================================================
   12. Feed and council cards stagger in
   ========================================================================= */
@supports (animation-timeline: view()) {
  .feed-item, .council-item, .offer, .amount {
    animation: cardIn cubic-bezier(.33,1,.68,1) both;
    animation-timeline: view();
    animation-range: entry 5% entry 60%;
  }
  @keyframes cardIn {
    from { opacity: 0; transform: translateY(26px); }
    to   { opacity: 1; transform: none; }
  }
}




/* =========================================================================
   The sketch draws itself
   Three tonal layers of the same drawing — faint construction, mid tones,
   then the dark strokes — sweep in one after another so the portrait builds
   the way a pencil drawing actually does.

   The layers are injected by motion.js and only exist while the animation
   runs. The real <img> underneath is never removed and never depends on any
   of this: if the layers fail to load, if the animation clock is frozen, or
   if anything else goes wrong, the portrait is simply already there.
   ========================================================================= */
.sketch-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  /* Initial value is 100% — fully revealed — so a stalled animation shows the
     drawing rather than hiding it. */
  --sweep: 100%;
  -webkit-mask-image: linear-gradient(var(--angle, 155deg), #000 0%, #000 var(--sweep), transparent calc(var(--sweep) + 16%));
          mask-image: linear-gradient(var(--angle, 155deg), #000 0%, #000 var(--sweep), transparent calc(var(--sweep) + 16%));
}
.sketch-layer[data-layer="light"] { --angle: 150deg; }
.sketch-layer[data-layer="mid"]   { --angle: 325deg; }
.sketch-layer[data-layer="dark"]  { --angle: 105deg; }

.hero-portrait.is-drawing .sketch-layer {
  animation: sketchSweep 1250ms cubic-bezier(.42,0,.3,1) both;
}
.hero-portrait.is-drawing .sketch-layer[data-layer="light"] { animation-delay: 0ms; }
.hero-portrait.is-drawing .sketch-layer[data-layer="mid"]   { animation-delay: 300ms; }
.hero-portrait.is-drawing .sketch-layer[data-layer="dark"]  { animation-delay: 620ms; }

@keyframes sketchSweep {
  from { --sweep: 0%; }
  to   { --sweep: 106%; }
}

/* The finished image sits underneath and fades up as the last layer lands */
.hero-portrait.is-drawing > img { opacity: 0; }
.hero-portrait > img { transition: opacity 500ms var(--ease); }

/* =========================================================================
   14. His words — crossfade between quotes
   ========================================================================= */
/* The outgoing quote is hidden instantly and the incoming one plays a short
   entrance ANIMATION rather than a delayed transition.

   The entrance animates TRANSFORM ONLY — never opacity. Opacity and visibility
   come from static rules in site.css.

   Two earlier attempts failed the same way. A delayed crossfade leaves nothing
   visible if the state changes faster than the delay. An opacity animation
   leaves the quote invisible anywhere the animation clock does not advance —
   a backgrounded tab, a throttled renderer — because it sits on frame 0
   forever, with or without a fill-mode. Animating only the offset means the
   worst case is text sitting 7px low, not text nobody can read. */
/* No fill-mode. The visible state comes from the static rule in site.css, and
   this animation only plays the entrance on top of it. With `both` the final
   frame would HOLD the state — so anywhere CSS animations do not advance (a
   backgrounded tab, a throttled renderer) the quote would stay invisible
   forever. Visibility must never depend on an animation actually running. */
.js .quote[data-active] { animation: quoteIn .45s var(--ease); }
@keyframes quoteIn {
  from { transform: translateY(7px); }
  to   { transform: none; }
}

/* The sketch portrait settles in on load */
.hero-portrait img { animation: sketchIn 1.4s cubic-bezier(.22,1,.36,1) both; }
@keyframes sketchIn {
  from { opacity: 0; transform: scale(1.04); filter: blur(6px); }
  to   { opacity: 1; transform: none; filter: blur(0); }
}

/* =========================================================================
   15. Hero curtain
   The photograph opens from the centre outward on load.
   ========================================================================= */
.hero-band .frame {
  animation: curtain 1.4s cubic-bezier(.66,0,.2,1) .5s both;
}
@keyframes curtain {
  from { clip-path: inset(0 50% 0 50%); }
  to   { clip-path: inset(0 0 0 0); }
}

/* =========================================================================
   16. Light sweep across the gold buttons
   ========================================================================= */
@media (hover: hover) and (pointer: fine) {
  .btn { position: relative; overflow: hidden; }
  .btn::before {
    content: '';
    position: absolute;
    inset: 0 auto 0 -120%;
    width: 55%;
    background: linear-gradient(100deg, transparent, rgba(255,255,255,.42), transparent);
    transform: skewX(-18deg);
    pointer-events: none;
  }
  .btn:hover::before { animation: sweep .85s cubic-bezier(.33,1,.68,1); }
  @keyframes sweep { to { left: 130%; } }
}

/* =========================================================================
   17. Gallery — a soft focus-pull as each frame arrives
   ========================================================================= */
@supports (animation-timeline: view()) {
  .gallery a {
    animation: focusPull cubic-bezier(.33,1,.68,1) both;
    animation-timeline: view();
    animation-range: entry 4% entry 52%;
  }
  @keyframes focusPull {
    from { opacity: 0; transform: translateY(28px) scale(.96); filter: blur(8px); }
    to   { opacity: 1; transform: none; filter: blur(0); }
  }
}

/* =========================================================================
   18. Marquee ribbon — the programme names drifting past
   ========================================================================= */
.ribbon-track { animation: drift 46s linear infinite; }
/* The names are links now, so the band must also hold still for the keyboard —
   a target that drifts out from under the caret cannot be activated. */
.ribbon:hover .ribbon-track,
.ribbon:focus-within .ribbon-track { animation-play-state: paused; }
@keyframes drift { to { transform: translateX(-50%); } }

/* =========================================================================
   13. Cross-page transitions
   ========================================================================= */
@view-transition { navigation: auto; }

::view-transition-old(root) {
  animation: vtOut .28s cubic-bezier(.4,0,1,1) both;
}
::view-transition-new(root) {
  animation: vtIn .42s cubic-bezier(0,0,.2,1) both;
}
@keyframes vtOut { to { opacity: 0; transform: translateY(-10px); } }
@keyframes vtIn { from { opacity: 0; transform: translateY(16px); } }

/* The header should stay put across a navigation rather than fade with the page */
.site-header { view-transition-name: site-header; }
::view-transition-old(site-header),
::view-transition-new(site-header) { animation: none; mix-blend-mode: normal; }

} /* end prefers-reduced-motion: no-preference */


/* =========================================================================
   Reduced motion: words are simply visible, nothing moves.
   ========================================================================= */
@media (prefers-reduced-motion: reduce) {
  /* Structure all lives in site.css, so this only has to stop things moving
     and make sure nothing is left dimmed or hidden. */
  .w > span { transform: none; animation: none; }
  .lit .w > span { color: rgba(250,246,239,.97); animation: none; }
  .progress, .scroll-cue { display: none; }
}
