/* ABC Canada 2027 — motion layer.
   Framer-style feel with no framework: spring easing, staggered reveals,
   line-masked headings, a sliding tab indicator, light parallax and counters.

   Everything is gated on the `js` class the head script sets, so with
   JavaScript off the page renders fully and statically. Delete this file and
   its script and the site still works — nothing here carries content.

   Timings follow the brief (§16): micro-interactions 150–250ms, section
   reveals 300–500ms. No scroll hijacking, no infinite loops. */

:root {
  /* A spring, sampled into linear(). Slight overshoot (~1.027) is what gives
     the motion its Framer character — it settles rather than just stopping. */
  --ease-spring: linear(
    0, .0033 .8%, .0263 2.39%, .0896 4.77%, .4285 13.44%, .5714 16.62%,
    .6903 19.8%, .7871 23.07%, .8642 26.44%, .9243 29.99%, .9695 33.87%,
    1.0018 38.16%, 1.0195 42.6%, 1.0266 47.53%, 1.0227 55.68%, 1.0007 76.29%, 1
  );
  --ease-out: cubic-bezier(.22, 1, .36, 1);

  --dur-micro: 200ms;
  --dur-reveal: 460ms;
  --dur-fade: 320ms;
  --stagger: 70ms;
}

/* ── Reveal ─────────────────────────────────────────────────────────────
   JS tags the children of every .reveal group with data-motion and an
   index, so a row of cards arrives one after another rather than at once. */
.js [data-motion] {
  opacity: 0;
  transform: translateY(20px);
  will-change: opacity, transform;
}
.js [data-motion].is-in {
  opacity: 1;
  transform: none;
  transition:
    opacity var(--dur-fade) var(--ease-out),
    transform var(--dur-reveal) var(--ease-spring);
  transition-delay: calc(var(--i, 0) * var(--stagger));
  will-change: auto;
}

/* A split heading animates by its own lines — no container fade on top. */
.js [data-motion][data-split] { opacity: 1; transform: none; }

/* ── Line-masked headings ───────────────────────────────────────────────
   JS splits on <br> and wraps each segment; the mask is the overflow of the
   outer span, and the inner span rises through it.

   These headings run at line-height .85–.94, so the line box is *shorter* than
   the glyphs — an unpadded mask shears the accents off ÉKÉ and the descenders
   off "Prenez-y part.". The padding opens the mask enough to clear them and
   the matching negative margin gives the space back, so line spacing is
   unchanged. The inner still starts fully below the mask, so nothing shows
   early. */
.m-line {
  display: block;
  overflow: hidden;
  padding-top: .22em;
  margin-top: -.22em;
  padding-bottom: .18em;
  margin-bottom: -.18em;
}
.js .m-line-i {
  display: block;
  transform: translateY(105%);
}
.js [data-split].is-in .m-line-i {
  transform: none;
  transition: transform var(--dur-reveal) var(--ease-spring);
  /* --i-base is the heading's own place in its reveal group, so a split
     heading falls in step with the eyebrow and buttons around it instead of
     always starting at zero. */
  transition-delay: calc(var(--i-base, 0) * var(--stagger) + var(--i, 0) * 90ms);
}

/* ── Sliding tab indicator ──────────────────────────────────────────────
   The ink fill travels between tabs instead of cutting, which is the one
   effect that most reads as "Framer" — a shared element moving between two
   layouts. The buttons sit above it so their rules stay visible. */
.js .day-tabs,
.js .stay-tabs { position: relative; }

.js .day-tab,
.js .stay-tab {
  position: relative;
  z-index: 1;
  transition: color var(--dur-micro) var(--ease-out);
}
.js .day-tab[aria-selected="true"],
.js .stay-tab[aria-selected="true"] { background: transparent; }

.m-indicator {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 0;
  background: var(--color-text);
  z-index: 0;
  pointer-events: none;
  transition:
    transform var(--dur-reveal) var(--ease-spring),
    width var(--dur-reveal) var(--ease-spring);
}
/* First placement must not slide in from the left edge. */
.m-indicator[data-init] { transition: none; }

/* ── Panel swap ─────────────────────────────────────────────────────────
   Switching day or parcours crossfades the panel and walks its rows in. */
@keyframes m-panel-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}
.js .is-swapped { animation: m-panel-in var(--dur-fade) var(--ease-out) both; }
.js .is-swapped .session,
.js .is-swapped .flow-step {
  animation: m-panel-in var(--dur-fade) var(--ease-out) both;
  animation-delay: calc(var(--i, 0) * 45ms);
}

/* ── Photography ────────────────────────────────────────────────────────
   The brief allows a 1 → 1.03 scale on hover and light parallax; both are
   set on the image so the slot keeps clipping the overflow. */
.pillar .slot img {
  transition: transform var(--dur-fade) var(--ease-out);
}
.js .pillar:hover .slot img,
.js .pillar:focus-visible .slot img { transform: scale(1.03); }

/* Parallax is driven from JS on these; the transform is written inline. Le
   hero empile deux cadres, chacun avec sa photographie, plus la vidéo. */
.js .hero-media img,
.js .hero-media video,
.js .bleed-media .slot img { will-change: transform; }

/* ── Animated underline ─────────────────────────────────────────────────
   Grows from the left on hover and on keyboard focus, so the affordance is
   not hover-only (§68). */
.u-anim { position: relative; }
.u-anim::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -3px;
  height: 2px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-micro) var(--ease-out);
}
.u-anim:hover::after,
.u-anim:focus-visible::after { transform: scaleX(1); }

/* ── Buttons ────────────────────────────────────────────────────────────  */
.js .btn {
  transition:
    background-color var(--dur-micro) var(--ease-out),
    color var(--dur-micro) var(--ease-out),
    filter var(--dur-micro) var(--ease-out),
    transform var(--dur-micro) var(--ease-out);
}
.js .btn:hover { transform: translateY(-1px); }
.js .btn:active { transform: translateY(0) scale(.985); }

/* ── Header ─────────────────────────────────────────────────────────────
   §18: a very light shadow once the page has scrolled off the top. */
.js .site-header {
  transition: box-shadow var(--dur-fade) var(--ease-out);
}
.js .site-header[data-scrolled] { box-shadow: var(--shadow-sm); }

/* ── Mobile menu ────────────────────────────────────────────────────────  */
.js .mobile-menu {
  transition:
    transform var(--dur-reveal) var(--ease-spring),
    visibility 0s linear var(--dur-reveal);
}
.js .mobile-menu[data-open="true"] {
  transition:
    transform var(--dur-reveal) var(--ease-spring),
    visibility 0s;
}
.js .mobile-menu nav > a,
.js .mobile-menu .mobile-menu-foot {
  opacity: 0;
  transform: translateY(16px);
}
.js .mobile-menu[data-open="true"] nav > a,
.js .mobile-menu[data-open="true"] .mobile-menu-foot {
  opacity: 1;
  transform: none;
  transition:
    opacity var(--dur-fade) var(--ease-out),
    transform var(--dur-reveal) var(--ease-spring);
  transition-delay: calc(120ms + var(--i, 0) * 45ms);
}

/* ── Counters ───────────────────────────────────────────────────────────
   Tabular figures stop the number jittering as digits change width. */
.m-count { font-variant-numeric: tabular-nums; }

/* ── Le ruban de campagne ───────────────────────────────────────────────
   Tout est sous `[data-marquee="on"]`, un attribut que motion.js ne pose
   qu'une fois la course recopiée assez de fois pour boucler sans trou. Pas de
   script, pas d'attribut : la banderole reste immobile et lisible, et le
   bouton de suspension — qui n'aurait rien à suspendre — ne s'affiche pas.

   Quatre mouvements, tous en `transform` ou en `opacity` : la course, la
   feuille qui balance, un reflet qui traverse, et les confettis que motion.js
   sème lui-même. Le ruban est la seule boucle infinie de la page ; §16 la
   tolère à condition qu'on puisse l'arrêter, d'où le bouton, et l'arrêt au
   survol et au focus pour qui veut simplement lire la phrase. */
@keyframes vote-scroll {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-100%, 0, 0); }
}
/* La feuille ne tourne pas sur elle-même — à cette taille ce serait un
   gyrophare. Elle balance, comme un fanion. */
@keyframes vote-leaf {
  0%, 100% { transform: rotate(-14deg); }
  50%      { transform: rotate(14deg) translateY(-.08em); }
}
/* Le reflet passe puis se range : la moitié du cycle est un temps mort, sans
   quoi la bande clignoterait en continu. */
@keyframes vote-sheen {
  0%   { transform: translate3d(-130%, 0, 0); }
  52%  { transform: translate3d(130%, 0, 0); }
  100% { transform: translate3d(130%, 0, 0); }
}
@keyframes vote-fall {
  from { transform: translate3d(0, -20px, 0) rotate(0deg); opacity: 0; }
  12%  { opacity: 1; }
  88%  { opacity: 1; }
  to   { transform: translate3d(var(--drift, 12px), 150px, 0) rotate(var(--spin, 200deg)); opacity: 0; }
}

.vote-band[data-marquee="on"] .vote-run {
  animation: vote-scroll var(--vote-dur, 40s) linear infinite;
}
.vote-band[data-marquee="on"] .vote-leaf {
  animation: vote-leaf 3.4s ease-in-out infinite;
}
.vote-run .vote-leaf:nth-of-type(2) { animation-delay: -1.13s; }
.vote-run .vote-leaf:nth-of-type(3) { animation-delay: -2.27s; }

.vote-band[data-marquee="on"]::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(102deg,
    transparent 36%, rgba(243, 242, 242, .13) 50%, transparent 64%);
  animation: vote-sheen 9s ease-in-out infinite;
}

/* Semés par motion.js : rien dans le HTML, rien à nettoyer si la couche
   motion disparaît. Ils passent derrière le texte et restent assez discrets
   pour ne pas manger le contraste des lettres. */
.vote-confetti {
  position: absolute; inset: -24px 0; overflow: hidden;
  pointer-events: none;
}
.vote-confetti i {
  position: absolute; top: 0; display: block;
  background: currentColor; opacity: .26;
  animation: vote-fall var(--dur, 5s) linear var(--delay, 0s) infinite;
}
/* Deux couleurs plutôt qu'une : sur l'encre, des éclats crème seuls font de la
   poussière, la moitié en rouge en fait des confettis. */
.vote-confetti i.is-red { background: var(--canada-lift); opacity: .55; }

/* Les bords s'estompent dans le rouge : sans cela la phrase est tranchée net
   à la verticale, et l'on voit un mot coupé plutôt qu'un mot qui s'en va.
   Uniquement quand le ruban défile — sur une banderole fixe ce dégradé
   effacerait le premier mot pour rien. */
/* Un fondu en longueur fixe, pas en pourcentage : 4 % d'une bande de 1240px
   effaçaient une lettre entière, et sur téléphone n'auraient rien effacé du
   tout. 22px adoucissent la coupe sans manger de glyphe. */
.vote-band[data-marquee="on"] .vote-viewport {
  -webkit-mask-image: linear-gradient(90deg,
    transparent 0, #000 22px, #000 calc(100% - 22px), transparent 100%);
          mask-image: linear-gradient(90deg,
    transparent 0, #000 22px, #000 calc(100% - 22px), transparent 100%);
}
.vote-band[data-marquee="on"] .vote-toggle { display: inline-flex; }

/* Suspendre, c'est suspendre tout le ruban — la course, les fanions, le
   reflet et les confettis — sinon « en pause » ne veut rien dire. Ces règles
   doivent rester plus spécifiques que les `animation:` ci-dessus : la forme
   raccourcie y remet `animation-play-state: running`. */
.vote-band[data-marquee="on"]:hover .vote-run,
.vote-band[data-marquee="on"]:focus-within .vote-run,
.vote-band[data-marquee="on"][data-user-paused="true"] .vote-run,
.vote-band[data-marquee="on"][data-user-paused="true"] .vote-leaf,
.vote-band[data-marquee="on"][data-user-paused="true"] .vote-confetti i {
  animation-play-state: paused;
}
.vote-band[data-marquee="on"][data-user-paused="true"]::after {
  animation-play-state: paused;
}

/* ── Reduced motion ─────────────────────────────────────────────────────
   Everything above collapses to its end state. Nothing moves, nothing
   fades, and no content depends on a transition having run. */
@media (prefers-reduced-motion: reduce) {
  .js [data-motion],
  .js [data-motion].is-in {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .js .m-line-i,
  .js [data-split].is-in .m-line-i { transform: none; transition: none; }
  .m-line { overflow: visible; }
  .m-indicator { transition: none; }
  .js .is-swapped,
  .js .is-swapped .session,
  .js .is-swapped .flow-step { animation: none; }
  .js .pillar:hover .slot img,
  .js .pillar:focus-visible .slot img { transform: none; }
  .js .btn:hover,
  .js .btn:active { transform: none; }
  .u-anim::after { transition: none; }
  .js .mobile-menu,
  .js .mobile-menu nav > a,
  .js .mobile-menu .mobile-menu-foot {
    transition: none;
    opacity: 1;
    transform: none;
  }
  .js .mobile-menu { transform: translateY(-100%); }
  .js .mobile-menu[data-open="true"] { transform: none; }
  /* motion.js ne pose pas `data-marquee` en mouvement réduit et le retire si
     la préférence change en cours de route ; ces règles sont le filet, pour
     le temps d'une bascule et pour qui poserait l'attribut à la main. */
  .vote-band[data-marquee="on"] .vote-run,
  .vote-band[data-marquee="on"] .vote-leaf { animation: none; }
  .vote-band[data-marquee="on"]::after { display: none; }
  .vote-band[data-marquee="on"] .vote-viewport {
    -webkit-mask-image: none;
            mask-image: none;
  }
  .vote-confetti { display: none; }
}
