/* ==========================================================================
   Paris International Exchange — motion layer
   --------------------------------------------------------------------------
   Every effect in this file follows one rule set:
     · only transform / opacity are animated (never width, height, top, left)
     · hover effects live behind @media (hover: hover) and (pointer: fine),
       so touch devices get the static layout
     · nothing is hidden until JS sets <html data-motion="on">, so the site is
       fully readable with JavaScript disabled
     · prefers-reduced-motion: reduce switches everything off and leaves a
       static, complete page

   To disable the whole layer: delete the motion.css <link>, motion.js and
   forms.js <script> tags from the page. Nothing else depends on them.
   ========================================================================== */

/* --- tokens: durations and easings from the handbook's parameter table ---- */
:root {
  --t-fast: .15s;
  --t-base: .25s;
  --t-slow: .4s;
  --t-in: .6s;
  --ease-out: cubic-bezier(.22, .61, .36, 1);      /* 先快后慢 */
  --ease-in: cubic-bezier(.55, .06, .68, .19);     /* 先慢后快 */
  --ease-inout: cubic-bezier(.45, .05, .55, .95);
  --spring: cubic-bezier(.34, 1.56, .64, 1);       /* 弹性回弹 */
  --stagger: 80ms;
}

/* The ambient colour field and the cursor-following grid/glow that used to live here
   (two endless full-viewport animations on body::before/::after, fed by 18 custom
   properties the pointer engine rewrote every frame) are gone: the page background is
   a flat token colour now. */

/* ==========================================================================
   2 · 入场与加载
   ========================================================================== */
/* Stagger + Fade Up: only active once JS is ready, so nothing is invisible
   without it. --i is set per element by motion.js (capped at 8 steps).
   `will-change` is deliberately NOT set on the element while it waits: on the home
   page the reveals plus the word-per-span scene headings added up to 312 permanently
   promoted layers, which is exactly the compositor load this layer is supposed to
   avoid. The promotion during the transition is automatic anyway. */
html[data-motion="on"] [data-reveal] {
  opacity: 0;
  transform: translate3d(0, 20px, 0);
  transition: opacity var(--t-in) var(--ease-out), transform var(--t-in) var(--ease-out);
  transition-delay: calc(var(--i, 0) * var(--stagger));
}
html[data-motion="on"] [data-reveal].is-in {
  opacity: 1;
  transform: none;
}
html[data-motion="on"] [data-reveal="fade"] { transform: none; }
html[data-motion="on"] [data-reveal="scale"] { transform: translate3d(0, 12px, 0) scale(.98); }

/* Skeleton screen: a soft breathing placeholder while a photo loads */
.skeleton { position: relative; overflow: hidden; background: var(--bg-alt); }
.skeleton::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, transparent 20%, color-mix(in srgb, var(--card) 70%, transparent) 50%, transparent 80%);
  transform: translate3d(-100%, 0, 0);
  animation: skeletonSweep 1.6s var(--ease-inout) infinite;
}
@keyframes skeletonSweep { to { transform: translate3d(100%, 0, 0); } }
.photo-card img { transition: opacity var(--t-slow) var(--ease-out); }
.photo-card.is-loading img { opacity: 0; }

/* ==========================================================================
   3 · 滚动触发
   ========================================================================== */
/* Sticky header: state change once the page has scrolled */
.site-header { transition: background var(--t-base) var(--ease-out), box-shadow var(--t-base) var(--ease-out), border-color var(--t-base) var(--ease-out); }
.brand__mark { transition: transform var(--t-base) var(--spring); }
html[data-scrolled="true"] .brand__mark { transform: scale(.9); }
html[data-scrolled="true"] .site-header {
  background: color-mix(in srgb, var(--card) 96%, transparent);
  box-shadow: 0 1px 2px rgba(16, 24, 40, .05), 0 10px 24px -18px rgba(16, 24, 40, .35);
}
html[data-scrolled="true"] .site-header__bar { padding-block: .35rem; }
/* padding is not transitioned: the header snaps, the surface and the logo animate */

/* Scroll progress: 3px tricolore bar, scaleX only (no layout animation) */
.scroll-progress { position: fixed; top: 0; left: 0; right: 0; height: 3px; z-index: 60; background: transparent; pointer-events: none; }
.scroll-progress > span {
  display: block; height: 100%; transform-origin: 0 50%; transform: scaleX(0);
  background: linear-gradient(90deg, var(--bleu) 0 33%, var(--rouge) 33% 100%);
}

/* Back to top: fades in after one screen */
.to-top {
  position: fixed; right: 18px; bottom: 18px; z-index: 55;
  width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--line);
  background: var(--card); color: var(--ink); font-size: 1.05rem; cursor: pointer;
  box-shadow: var(--shadow); opacity: 0; transform: translate3d(0, 12px, 0) scale(.9);
  transition: opacity var(--t-base) var(--ease-out), transform var(--t-base) var(--spring);
  pointer-events: none;
}
.to-top.is-visible { opacity: 1; transform: none; pointer-events: auto; }
.to-top:hover { background: var(--bg-alt); }

/* Smooth anchors must clear the sticky header */
:target, [id] { scroll-margin-top: 84px; }

/* ==========================================================================
   4 · 悬停与指针反馈 (desktop pointers only)
   ========================================================================== */
@media (hover: hover) and (pointer: fine) {
  /* No hover lift any more, and no `transition: transform` on the cards: their
     transform belongs to the wind, which writes it every frame. A 220ms easing on
     the same property made the browser chase a moving target, so the card lagged
     the physics and the re-timing between frames read as a flicker. */
  .card, .post, .voice, .photo-card, .group-card { transition: box-shadow var(--t-base) var(--ease-out), border-color var(--t-base) var(--ease-out); }
  a.card:hover, .card--event:hover, .post:hover, .voice:hover, .photo-card:hover, .cal-month:hover {
    box-shadow: var(--shadow);
    border-color: color-mix(in srgb, var(--bleu) 28%, var(--line));
  }
  /* image zoom inside the frame */
  .photo-card button { overflow: hidden; }
  .photo-card img { transition: transform var(--t-slow) var(--ease-out); }
  .photo-card:hover img { transform: scale(1.05); }

  /* underline slide on navigation links */
  .nav a { position: relative; }
  .nav a::after {
    content: ""; position: absolute; left: .6rem; right: .6rem; bottom: .28rem; height: 2px;
    background: currentColor; transform: scaleX(0); transform-origin: 0 50%;
    transition: transform .2s var(--ease-out);
  }
  .nav a:hover::after { transform: scaleX(1); }
  .nav a[aria-current="page"]::after { transform: scaleX(1); }
}

/* Magnetic button + tilt: transform is written by motion.js, this is the
   spring return when the pointer leaves. */
.btn--magnetic { transition: transform var(--t-base) var(--spring), box-shadow var(--t-base) var(--ease-out), background var(--t-base) var(--ease-out); }
.tilt { transform-style: preserve-3d; transition: transform var(--t-slow) var(--ease-out), box-shadow var(--t-base) var(--ease-out); }
.tilt > * { transform: translateZ(18px); }

/* Calendar day tiles: a clear 2D lift on hover, the same idea as .cal-month
   below but a touch stronger so it reads on a dense grid. No rotateX here on
   purpose: the tile sits on a perspective-ed parent and the entrance flip
   (style.css) owns its transform, so a 3D hover transform makes the browser
   interpolate two mismatched transform lists and the tile washes out
   mid-flight. Only translate/scale, so the grid never reflows. */
@media (hover: hover) and (pointer: fine) {
  .calendar-tile {
    transition: transform var(--t-base) var(--ease-out), box-shadow var(--t-base) var(--ease-out);
  }
  .calendar-tile:hover, .calendar-tile:focus-within {
    transform: translate3d(0, -5px, 0) scale(1.035);
    box-shadow: 0 14px 26px -12px rgba(16, 24, 40, .5), 0 2px 5px rgba(16, 24, 40, .16), inset 0 1px rgba(255, 255, 255, .45);
    z-index: 3;
  }
}

/* Tooltip: dark bubble with an arrow, after a 0.3s hover delay, also on focus */
.tip { position: relative; }
.tip__bubble {
  position: absolute; left: 50%; bottom: calc(100% + 9px); transform: translate3d(-50%, 4px, 0);
  background: #16181d; color: #fff; font-size: .78rem; line-height: 1.35; white-space: nowrap;
  padding: .34rem .55rem; border-radius: 7px; box-shadow: 0 8px 22px -12px #000;
  opacity: 0; pointer-events: none; transition: opacity .15s var(--ease-out), transform .15s var(--ease-out);
  transition-delay: 0s; z-index: 70;
}
.tip__bubble::after {
  content: ""; position: absolute; top: 100%; left: 50%; margin-left: -5px;
  border: 5px solid transparent; border-top-color: #16181d;
}
.tip:hover .tip__bubble, .tip:focus-within .tip__bubble {
  opacity: 1; transform: translate3d(-50%, 0, 0); transition-delay: .3s;
}

/* ==========================================================================
   5 · 点击与状态反馈
   ========================================================================== */
.btn { transition: transform var(--t-fast) var(--ease-out), box-shadow var(--t-base) var(--ease-out), background var(--t-base) var(--ease-out), border-color var(--t-base) var(--ease-out), color var(--t-base) var(--ease-out); }
.btn:active { transform: scale(.96); }
.btn.is-pressed { transform: scale(.96); }

/* ripple: a circle that grows from the click point and fades (0.5s) */
.ripple-host { position: relative; overflow: hidden; }
.ripple {
  position: absolute; border-radius: 50%; transform: translate(-50%, -50%) scale(0);
  background: currentColor; opacity: .28; pointer-events: none;
  animation: rippleOut .5s var(--ease-out) forwards;
}
@keyframes rippleOut { to { transform: translate(-50%, -50%) scale(1); opacity: 0; } }

/* like burst: tick pops to 1.3 then settles, six dots fly out */
.burst { position: relative; display: inline-block; }
.burst__icon { display: inline-block; animation: burstPop .45s var(--spring); }
@keyframes burstPop { 0% { transform: scale(1); } 45% { transform: scale(1.3); } 100% { transform: scale(1); } }
.burst__dot {
  position: absolute; left: 50%; top: 50%; width: 5px; height: 5px; border-radius: 50%;
  background: currentColor; pointer-events: none; animation: burstDot .6s var(--ease-out) forwards;
}
@keyframes burstDot { from { opacity: .9; transform: translate(-50%, -50%) scale(1); } to { opacity: 0; transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) scale(.4); } }

/* loading button: spinner inside, no double submit */
.btn.is-loading { pointer-events: none; }
.btn__spin {
  display: inline-block; width: 14px; height: 14px; margin-right: .4rem; vertical-align: -2px;
  border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%;
  animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ==========================================================================
   6 · 弹层与浮层
   ========================================================================== */
.modal { animation: backdropIn .2s var(--ease-out); }
.modal__box { animation: modalIn var(--t-base) var(--spring); }
@keyframes backdropIn { from { opacity: 0; } }
@keyframes modalIn { from { opacity: 0; transform: translate3d(0, 8px, 0) scale(.95); } }
.modal__box, .dropdown__panel, .toast, .lightbox__bar .btn { backdrop-filter: blur(12px) saturate(160%); -webkit-backdrop-filter: blur(12px) saturate(160%); }
.modal__box { border: 1px solid color-mix(in srgb, var(--line) 70%, transparent); }

.dropdown__panel { animation: panelIn .18s var(--ease-out); }
@keyframes panelIn { from { opacity: 0; transform: translate3d(0, -6px, 0) scale(.98); } }

.toast { animation: toastIn var(--t-base) var(--spring); position: relative; overflow: hidden; }
@keyframes toastIn { from { opacity: 0; transform: translate3d(0, 10px, 0) scale(.97); } }
.toast::after {
  content: ""; position: absolute; left: 0; bottom: 0; height: 2px; width: 100%;
  background: currentColor; opacity: .25; transform-origin: 0 50%; animation: toastLife 4.2s linear forwards;
}
@keyframes toastLife { to { transform: scaleX(0); } }

/* mobile navigation behaves like a drawer; items fade in one after another */
@media (max-width: 1080px) {
  html[data-motion="on"] .nav { animation: drawerIn .3s var(--ease-out); }
  html[data-motion="on"] .nav > a, html[data-motion="on"] .nav__group { animation: navItemIn .32s var(--ease-out) backwards; }
  html[data-motion="on"] .nav > *:nth-child(1) { animation-delay: .03s; }
  html[data-motion="on"] .nav > *:nth-child(2) { animation-delay: .06s; }
  html[data-motion="on"] .nav > *:nth-child(3) { animation-delay: .09s; }
  html[data-motion="on"] .nav > *:nth-child(4) { animation-delay: .12s; }
  html[data-motion="on"] .nav > *:nth-child(5) { animation-delay: .15s; }
  html[data-motion="on"] .nav > *:nth-child(6) { animation-delay: .18s; }
  html[data-motion="on"] .nav > *:nth-child(7) { animation-delay: .21s; }
  html[data-motion="on"] .nav > *:nth-child(8) { animation-delay: .24s; }
  html[data-motion="on"] .nav > *:nth-child(9) { animation-delay: .27s; }
  html[data-motion="on"] .nav > *:nth-child(10) { animation-delay: .3s; }
}
@keyframes drawerIn { from { opacity: 0; transform: translate3d(0, -8px, 0); } }
@keyframes navItemIn { from { opacity: 0; transform: translate3d(-6px, 0, 0); } }

/* hamburger morphs into a cross */
.nav-toggle__bars { position: relative; display: inline-block; width: 18px; height: 12px; }
.nav-toggle__bars i {
  position: absolute; left: 0; width: 100%; height: 2px; border-radius: 2px; background: currentColor;
  transition: transform var(--t-base) var(--ease-out), opacity var(--t-fast) var(--ease-out);
}
.nav-toggle__bars i:nth-child(1) { top: 0; }
.nav-toggle__bars i:nth-child(2) { top: 5px; }
.nav-toggle__bars i:nth-child(3) { top: 10px; }
[aria-expanded="true"] .nav-toggle__bars i:nth-child(1) { transform: translate3d(0, 5px, 0) rotate(45deg); }
[aria-expanded="true"] .nav-toggle__bars i:nth-child(2) { opacity: 0; }
[aria-expanded="true"] .nav-toggle__bars i:nth-child(3) { transform: translate3d(0, -5px, 0) rotate(-45deg); }

/* ==========================================================================
   7 · 展开与收起 (smooth height without animating height)
   ========================================================================== */
details > summary { cursor: pointer; }
details > summary::-webkit-details-marker { display: none; }
details > summary::after {
  content: "▾"; float: right; margin-left: .5rem; color: var(--muted);
  transition: transform var(--t-base) var(--ease-out);
}
details[open] > summary::after { transform: rotate(180deg); }
.details-body { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .3s var(--ease-out); }
details[open] .details-body { grid-template-rows: 1fr; }
.details-body > div { overflow: hidden; }
.month-table details summary::after, .event-card details summary::after { float: none; }

/* read more: clamp, then expand smoothly */
.clamp { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: var(--clamp-lines, 3); line-clamp: var(--clamp-lines, 3); overflow: hidden; }
.clamp.is-open { -webkit-line-clamp: unset; line-clamp: unset; }
.read-more { margin-top: .3rem; }

/* ==========================================================================
   8 · 导航与切换
   ========================================================================== */
/* segmented control: a pill that slides between the options */
.segmented { position: relative; display: inline-flex; gap: .15rem; padding: .2rem; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-alt); }
.segmented__pill {
  position: absolute; top: .2rem; bottom: .2rem; left: 0; border-radius: 999px; background: var(--card);
  box-shadow: var(--shadow-sm); transition: transform var(--t-base) var(--ease-out);
  z-index: 0; pointer-events: none;
}
.segmented button {
  position: relative; z-index: 1; border: 0; background: none; font: inherit; font-size: .88rem; font-weight: 600;
  color: var(--ink-2); padding: .38rem .85rem; border-radius: 999px; cursor: pointer; white-space: nowrap;
}
.segmented button[aria-pressed="true"] { color: var(--bleu); }

/* admin tabs: the indicator slides to the active tab */
.ad-tabs { position: relative; }
.ad-tabs__ink {
  position: absolute; bottom: -2px; left: 0; height: 3px; border-radius: 3px; background: var(--bleu);
  transition: transform var(--t-base) var(--ease-out); z-index: 2;
}
.ad-tabs button { position: relative; }
.ad-tabs button[aria-selected="true"] { box-shadow: none; }

/* carousel in the photo viewer */
.lightbox__stage { position: relative; overflow: hidden; }
.lightbox.is-sliding-next .lightbox img { animation: slideNext var(--t-base) var(--ease-out); }
.lightbox.is-sliding-prev .lightbox img { animation: slidePrev var(--t-base) var(--ease-out); }
@keyframes slideNext { from { opacity: .2; transform: translate3d(24px, 0, 0); } }
@keyframes slidePrev { from { opacity: .2; transform: translate3d(-24px, 0, 0); } }
.lightbox__dots { display: flex; gap: .3rem; justify-content: center; margin-top: .4rem; }
.lightbox__dots i { width: 7px; height: 7px; border-radius: 50%; background: rgba(255,255,255,.4); transition: background var(--t-base) var(--ease-out), transform var(--t-base) var(--spring); }
.lightbox__dots i.is-on { background: #fff; transform: scale(1.25); }

/* ==========================================================================
   10 · 表单
   ========================================================================== */
.field { position: relative; }
.field--float > label {
  position: absolute; left: .72rem; top: .58rem; margin: 0; padding: 0 .25rem;
  background: var(--card); color: var(--muted); font-weight: 500; pointer-events: none;
  transition: transform var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out), font-size var(--t-fast) var(--ease-out);
  transform-origin: 0 0;
}
.field--float.is-filled > label,
.field--float:focus-within > label { transform: translate3d(0, -.95rem, 0) scale(.82); color: var(--bleu); }
.field--float > input, .field--float > textarea, .field--float > select { padding-top: .78rem; }
.field--float > input::placeholder, .field--float > textarea::placeholder { color: transparent; }

/* focus highlight: border colour plus a soft outer glow */
input:focus, select:focus, textarea:focus { box-shadow: 0 0 0 4px color-mix(in srgb, var(--bleu) 16%, transparent); transition: box-shadow var(--t-base) var(--ease-out), border-color var(--t-base) var(--ease-out); }
.field.is-invalid input, .field.is-invalid textarea { border-color: var(--rouge); }
.field.is-invalid input:focus { box-shadow: 0 0 0 4px color-mix(in srgb, var(--rouge) 18%, transparent); }
.field.is-valid input { border-color: #2e9e5b; }
.field__check { position: absolute; right: .6rem; top: 2.45rem; color: #2e9e5b; font-weight: 700; opacity: 0; transform: scale(.7); transition: opacity var(--t-base) var(--ease-out), transform var(--t-base) var(--spring); }
.field.is-valid .field__check { opacity: 1; transform: none; }

/* password visibility toggle */
.input-wrap { position: relative; display: block; }
.field--password .input-wrap input { padding-right: 2.6rem; }
.password-toggle {
  position: absolute; right: .35rem; top: 50%; transform: translateY(-50%);
  border: 0; background: none; cursor: pointer; line-height: 1;
  font-size: 1rem; color: var(--muted); padding: .3rem .35rem; border-radius: 7px;
}
.password-toggle:hover { background: var(--bg-alt); color: var(--ink); }

/* stepper: − / + with a disabled edge */
.stepper-input { display: inline-flex; align-items: center; border: 1px solid var(--line); border-radius: var(--r-sm); overflow: hidden; background: var(--card); }
.stepper-input button { border: 0; background: none; font: inherit; font-size: 1.05rem; font-weight: 700; color: var(--ink); padding: .4rem .7rem; cursor: pointer; transition: background var(--t-fast) var(--ease-out); }
.stepper-input button:hover:not(:disabled) { background: var(--bg-alt); }
.stepper-input button:disabled { color: var(--muted); opacity: .45; cursor: not-allowed; }
.stepper-input input { width: 3.4rem; text-align: center; border: 0; border-inline: 1px solid var(--line); border-radius: 0; }

/* ==========================================================================
   11 · 数据与进度
   ========================================================================== */
/* poll bars grow with scaleX, never width */
.poll-bar > span { transform-origin: 0 50%; transform: scaleX(0); transition: transform .8s var(--ease-out); }
.poll-row.is-in .poll-bar > span { transform: scaleX(var(--v, 0)); }

/* steps bar (organiser console publish flow) */
.steps { display: flex; gap: .5rem; align-items: center; margin: 1rem 0 0; padding: 0; list-style: none; }
.steps li { display: flex; align-items: center; gap: .5rem; font-size: .88rem; color: var(--muted); }
.steps li::before {
  content: counter(step); counter-increment: step; width: 24px; height: 24px; border-radius: 50%;
  display: grid; place-items: center; background: var(--bg-alt); color: var(--ink-2); font-size: .78rem; font-weight: 700;
  border: 2px solid transparent; transition: transform var(--t-base) var(--spring), background var(--t-base) var(--ease-out);
}
.steps { counter-reset: step; }
.steps li.is-now { color: var(--ink); font-weight: 600; }
.steps li.is-now::before { background: var(--bleu); color: #fff; border-color: color-mix(in srgb, var(--bleu) 40%, transparent); transform: scale(1.06); }
.steps li.is-done::before { content: "✓"; background: #eaf6ee; color: #1f7a45; }
.steps li + li::after { content: ""; }

/* ==========================================================================
   12 · 装饰 (kept to two per page, per the handbook)
   ========================================================================== */
.kicker {
  background: linear-gradient(96deg, var(--bleu), var(--rouge));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
/* The drifting gradient veil behind the hero is gone with the rest of the ambient
   decoration; `.hero > .wrap` keeps its stacking context for the content. */
.hero > .wrap { position: relative; z-index: 1; }

/* ==========================================================================
   13 · 页面转场与主题
   ========================================================================== */
html[data-motion="on"] body { animation: pageIn var(--t-slow) var(--ease-out); }
@keyframes pageIn { from { opacity: 0; transform: translate3d(0, 6px, 0); } }
html.is-leaving body { opacity: 0; transform: translate3d(0, -4px, 0); transition: opacity .18s var(--ease-in), transform .18s var(--ease-in); }

/* dark mode: colours cross-fade instead of jumping */
html[data-theme-anim="on"] body,
html[data-theme-anim="on"] .card, html[data-theme-anim="on"] .site-header, html[data-theme-anim="on"] .site-footer,
html[data-theme-anim="on"] .btn, html[data-theme-anim="on"] input, html[data-theme-anim="on"] select,
html[data-theme-anim="on"] textarea, html[data-theme-anim="on"] .chip, html[data-theme-anim="on"] .badge,
html[data-theme-anim="on"] a, html[data-theme-anim="on"] h1, html[data-theme-anim="on"] h2, html[data-theme-anim="on"] h3 {
  transition: background-color .3s var(--ease-out), color .3s var(--ease-out), border-color .3s var(--ease-out);
}
.theme-btn__icon { display: inline-block; transition: transform var(--t-slow) var(--spring); }
html[data-theme="dark"] .theme-btn__icon { transform: rotate(-20deg); }

/* ==========================================================================
   12b · 高阶装饰
   The parallax veil, the pointer spotlight, the footer waves and the marquee were
   removed: every one of them was decoration that ran continuously or followed the
   cursor, and that was the bulk of the page's paint cost.
   ========================================================================== */

/* --- 事实气泡 fact bubbles: three big pills holding the key facts ----------- */
.hero__bubbles {
  list-style: none; margin: clamp(2rem, 6vh, 4.2rem) 0 0; padding: 0;
  display: flex; flex-wrap: wrap; align-items: flex-start;
  gap: clamp(.9rem, 2vh, 1.5rem) clamp(.8rem, 1.6vw, 1.6rem);
}
/* Each pill carries its own accent, tilt and offset. All three are static: a tidy
   row of identical pills reads as a row of buttons, so they are nudged out of line
   and tilted a little — shallow angles only (≤2°), enough to look settled rather
   than arranged, without making the text harder to read.
   The pill is the <li> itself. It used to be an inner span built at runtime by the
   pointer engine's wrapper step; that engine is gone, and with it that step, so the
   styles live on the element that exists in the markup. */
.bubble {
  position: relative; list-style: none;
  display: inline-flex; align-items: center; gap: .7rem;
  padding: clamp(.85rem, 1.8vh, 1.15rem) clamp(1.4rem, 2.6vw, 2.1rem);
  border-radius: 999px;
  font-size: clamp(1rem, 1.5vw, 1.3rem); font-weight: 600; line-height: 1.2;
  color: var(--ink); white-space: nowrap;
  --accent: var(--bleu);
  --tilt: -1.4deg;
  --drop: 0rem;
  border: 1px solid color-mix(in srgb, var(--accent) 38%, transparent);
  background: color-mix(in srgb, var(--accent) 12%, var(--card));
  box-shadow: 0 2px 4px rgba(16, 24, 40, .05), 0 22px 40px -24px color-mix(in srgb, var(--accent) 60%, transparent);
  transform: translate3d(0, var(--drop), 0) rotate(var(--tilt));
}
.bubble::before {
  content: ""; width: .65rem; height: .65rem; border-radius: 50%; flex: 0 0 auto;
  background: radial-gradient(circle at 32% 28%, #fff 6%, color-mix(in srgb, var(--accent) 60%, #fff) 38%, var(--accent));
}
/* 每条一个色系：蓝 → 青 → 玫；倾角与落差各不相同。
   错落只用垂直落差，不做横向平移：横向位移会把窄屏下的第一条推出容器。 */
.bubble--a { --accent: #2563eb; --tilt: -1.6deg; --drop: 0; }
.bubble--b { --accent: #0d9488; --tilt: 1.2deg;  --drop: .9rem; }
.bubble--c { --accent: #db2777; --tilt: -0.8deg; --drop: 1.8rem; }
/* 深色模式：底色更暗，靠 accent 的混合比例和描边把颜色撑出来 */
[data-theme="dark"] .bubble {
  background: color-mix(in srgb, var(--accent) 26%, var(--card));
  border-color: color-mix(in srgb, var(--accent) 52%, transparent);
}
/* 窄屏：落差收窄、字号降一档，避免三条换行后错落看起来很乱 */
@media (max-width: 900px) {
  :where(.bubble--b) { --drop: .5rem; }
  :where(.bubble--c) { --drop: 1rem; }
  :where(.bubble) { font-size: 1rem; padding: .7rem 1.15rem; }
}

/* --- 页脚 / 伙伴条 --------------------------------------------------- */
/* The two drifting SVG wave layers are gone. The partner band scrolls. */
.site-footer { position: relative; overflow: hidden; }
.site-footer__grid, .site-footer__bottom { position: relative; z-index: 1; }

/* The partner band scrolls sideways: the wrapper holds two identical tracks and
   travels exactly half its own width, so the loop has no seam. Only `transform` is
   animated (never `left`), so the compositor carries it without repainting, and it
   is the one loop on the page that is meant to run for ever. */
.marquee-band { border-block: 1px solid var(--line); background: color-mix(in srgb, var(--card) 60%, transparent); }
.marquee {
  position: relative; overflow: hidden; padding-block: .7rem;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}
.marquee__inner { display: flex; width: max-content; gap: var(--t-base); animation: marquee 36s linear infinite; }
.marquee__track { display: flex; align-items: center; gap: .5rem; width: max-content; margin: 0; padding: 0; list-style: none; }
.marquee .chip { flex: 0 0 auto; }                /* never squeeze a partner name */
.marquee a { text-decoration: none; }
.marquee:hover .marquee__inner, .marquee:focus-within .marquee__inner { animation-play-state: paused; }
@keyframes marquee { to { transform: translate3d(-50%, 0, 0); } }
/* Nothing is hidden when the band is the only moving thing left: without JS there is
   no duplicate track, so the list simply sits still and stays readable. */
@media (prefers-reduced-motion: reduce) {
  .marquee__inner { animation: none !important; width: auto !important; }
  .marquee__track { flex-wrap: wrap; justify-content: center; }
  .marquee__dupe { display: none; }
}

/* ========================================================================== 
   Home narrative scenes: a soft scene change, word stagger and scroll snap.
   Snap is desktop-only; every snapped section is sized to the band that is
   actually left below the 64px sticky header, so a snapped view is never cut
   off and the page always rests centred on a block.
   ========================================================================== */
.home-scene-backdrop {
  position: fixed; z-index: -1; inset: -18vmax; overflow: hidden;
  pointer-events: none; opacity: .56;
}
/* The three blurred scene orbs (48vmax blurs, animated on every scene change) are
   gone with the rest of the ambient background. */
[data-theme="dark"] .home-scene-backdrop { opacity: .72; }

body[data-page="home"] #main > [data-home-scene] {
  position: relative; isolation: isolate;
}
/* The word spans below are decorative; the readable text of a scene heading or
   lead lives in its aria-label (set by motion.js), so nothing has to be hidden
   here and assistive technology reads each heading exactly once. */
body[data-page="home"] [data-scene-copy] .scene-word {
  display: inline-block; opacity: 0;
  transform: translate3d(0, .72em, 0) rotate(1.2deg);
  transition: opacity .52s var(--ease-out), transform .68s var(--ease-out);
  transition-delay: 0ms;
}
body[data-page="home"] .is-scene-active [data-scene-copy] .scene-word {
  opacity: 1; transform: none;
  transition-delay: calc(70ms + var(--word-i, 0) * 34ms);
}
body[data-page="home"] .is-scene-seen:not(.is-scene-active) [data-scene-copy] .scene-word {
  opacity: .72; transform: none; transition-delay: 0ms;
}

.scene-rail {
  position: fixed; z-index: 35; right: 18px; top: 50%;
  display: grid; gap: 7px; padding: 9px 7px;
  transform: translate3d(0, -50%, 0);
  border: 1px solid color-mix(in srgb, var(--line) 82%, transparent);
  border-radius: 999px; background: color-mix(in srgb, var(--card) 76%, transparent);
  box-shadow: var(--shadow-sm); backdrop-filter: blur(12px);
}
.scene-rail button {
  width: 7px; height: 7px; padding: 0; border: 0; border-radius: 50%;
  background: var(--muted); opacity: .38; cursor: pointer;
  transition: transform var(--t-base) var(--spring), opacity var(--t-base) var(--ease-out);
}
.scene-rail button:hover, .scene-rail button:focus-visible { opacity: .82; transform: scale(1.45); }
.scene-rail button[aria-current="step"] { background: var(--bleu); opacity: 1; transform: scale(1.55); }
.scene-rail-label {
  position: fixed; z-index: 36; right: 48px; top: var(--scene-label-y, 50%);
  max-width: min(260px, calc(100vw - 90px)); padding: .46rem .72rem;
  border: 1px solid var(--line); border-radius: 999px;
  background: color-mix(in srgb, var(--card) 92%, transparent); color: var(--ink);
  box-shadow: var(--shadow-sm); backdrop-filter: blur(14px);
  font-size: .78rem; font-weight: 700; line-height: 1.25; white-space: nowrap;
  opacity: 0; pointer-events: none;
  transform: translate3d(8px, -50%, 0) scale(.96);
  transition: opacity var(--t-fast) var(--ease-out), transform var(--t-base) var(--spring);
}
.scene-rail-label.is-visible { opacity: 1; transform: translate3d(0, -50%, 0) scale(1); }

@media (min-width: 1100px) and (min-height: 760px) and (prefers-reduced-motion: no-preference) {
  /* The snap band is measured from the sticky header, so its real height is a
     layout constant here: 64px bar + 4px tricolore hairline + 1px border.
     scroll-padding-top must equal that header, otherwise every snap rests short
     by the leftover and the block below the fold stays half visible */
  html.home-scenes-on {
    --snap-band: 69px;
    scroll-snap-type: y mandatory;
    scroll-padding-top: var(--snap-band);
  }
  body[data-page="home"] #main > [data-home-scene] {
    scroll-snap-align: start; scroll-snap-stop: normal;
  }
  /* A snapped section is exactly as tall as the band left below the header, so
     it is fully visible and never leaks a strip of the next block.

     Its content is laid out from the top instead of being centred: centring a
     640px block inside an 831px band opened a ~60px empty stripe above the
     heading, which read as a hole at the top of the screen. The main block now
     grows to absorb the leftover space instead, so the section still fills the
     band while nothing is left hanging above the heading. */
  body[data-page="home"] #main > .hero[data-home-scene],
  body[data-page="home"] #main > .section:not(.section--tight)[data-home-scene] {
    box-sizing: border-box;
    min-height: calc(100svh - var(--snap-band));
    display: block;
  }
  body[data-page="home"] #main > [data-home-scene] > .wrap {
    display: flex; flex-direction: column; align-items: stretch;
  }
  /* .split is the two-column carrier itself (hero text + aside, benefits grid +
     fee card). The rule above outranks the plain `.split` rule in style.css, so
     without this the columns silently collapse into one. A column flex cannot
     describe two columns, so it has to go back to grid. */
  body[data-page="home"] #main > [data-home-scene] > .wrap.split {
    display: grid; grid-template-columns: 1.15fr .85fr; align-items: start;
  }
  /* the hero has no aside any more: with one child the two-track split would
     keep reserving an empty second column, so the text block gets the width */
  body[data-page="home"] #main > [data-home-scene] > .wrap.split:has(> :only-child) {
    grid-template-columns: minmax(0, 1fr);
  }
  body[data-page="home"] #main > [data-home-scene] > .wrap > :first-child { margin-top: 0; }
  body[data-page="home"] #main > [data-home-scene] > .wrap > .grid,
  body[data-page="home"] #main > [data-home-scene] > .wrap > .split,
  body[data-page="home"] #main > [data-home-scene] > .wrap > #social-list,
  body[data-page="home"] #main > [data-home-scene] > .wrap > #benefits-list {
    flex: 1 1 auto;
  }
  body[data-page="home"] #main > [data-home-scene] > .wrap > .grid > * { align-self: stretch; }
  /* a section whose content is taller than the band just grows and scrolls:
     it must not keep the band height, or it would clip its own last row */
  body[data-page="home"] #main > .section:not(.section--tight)[data-home-scene].is-scene-tall {
    min-height: 0;
  }
  body[data-page="home"] #main > [data-home-scene] .mt3 { margin-top: clamp(.85rem, 2vh, 1.45rem); }
  body[data-page="home"] #main > [data-home-scene] .grid { gap: clamp(.6rem, 1.25vh, .95rem); }
  body[data-page="home"] #benefits .benefits-scene-layout {
    grid-template-columns: minmax(0, 1.55fr) minmax(250px, .45fr);
    gap: clamp(.8rem, 1.6vw, 1.35rem);
    align-items: start;
  }
  /* the fee column is a grid item of the split, and the split is a grid item of
     the centred section: both must be told to align to the top, otherwise the
     card is stretched to the height of the benefit grid */
  body[data-page="home"] #benefits .benefits-scene-layout,
  body[data-page="home"] #benefits .benefits-scene-layout > aside,
  body[data-page="home"] #benefits .benefits-scene-layout > aside > #fee-card {
    align-self: start; height: auto; min-height: 0;
  }
  body[data-page="home"] #benefits .benefits-scene-layout > aside {
    display: grid; align-content: start;
  }
  body[data-page="home"] #benefits .benefits-scene-layout > aside > #fee-card {
    display: grid; align-content: start;
  }
  body[data-page="home"] #benefits-list { grid-template-columns: repeat(3, minmax(0, 1fr)); grid-auto-rows: 1fr; }
  body[data-page="home"] #benefits-list .card {
    padding: clamp(.65rem, 1.2vh, .95rem);
    display: flex; flex-direction: column; height: 100%;
  }
  body[data-page="home"] #benefits-list h3 { font-size: clamp(.96rem, 1.7vh, 1.13rem); margin-bottom: .32em; }
  body[data-page="home"] #benefits-list p { line-height: 1.38; margin-bottom: .42rem; }
  /* the flexible line absorbs the difference between cards, so every card in the
     grid ends on the same height instead of ragged 1-row/2-row blocks */
  body[data-page="home"] #benefits-list .card > p { flex: 1 1 auto; }
  body[data-page="home"] #benefits-list .benefit-saving { margin-top: auto; padding-top: .46rem; }
  body[data-page="home"] #fee-card { padding: clamp(.8rem, 1.5vh, 1.1rem); }
  body[data-page="home"] #fee-card .price-big { font-size: clamp(2.15rem, 5vh, 3rem); margin-bottom: .35rem; }
  body[data-page="home"] #fee-card p { margin-bottom: clamp(.28rem, .75vh, .65rem); line-height: 1.35; }
  /* the fee block is a small card: it must not stretch into a tall billboard
     next to the short benefit grid */
  body[data-page="home"] #fee-card .fee-card {
    display: grid; gap: clamp(.3rem, .9vh, .6rem); text-align: center;
  }
  body[data-page="home"] #fee-card .fee-card > * { margin: 0; }
  body[data-page="home"] #fee-card .fee-card .price-big { margin: .1rem 0 .12rem; }
  body[data-page="home"] #fee-card .fee-card .btn { margin-top: clamp(.35rem, 1.1vh, .7rem); }
  body[data-page="home"] #fee-card .fee-card .badge { justify-self: center; }
  body[data-page="home"] .site-footer {
    margin-top: 0; scroll-snap-align: end; scroll-snap-stop: normal;
  }
}
/* The scene rail is shown independently of the snap. Snapping needs a tall window —
   in a 610px-tall one most sections are taller than the visible band and would jam
   half-visible — but the rail is just a jump list, so it is useful at any height.
   Hidden by default: the rail element exists as soon as the scene controller runs,
   so a window that matches none of the rules below must not be left with a stray
   strip of dots. */
.scene-rail, .scene-rail-label { display: none; }
@media (min-width: 1000px) and (min-height: 560px) and (prefers-reduced-motion: no-preference) {
  .scene-rail { display: grid; }
  .scene-rail-label { display: block; }
}
/* The rail is fixed to the right edge, so between 1000px and the point where the
   content column stops growing it would sit on top of the text (measured: the wrap
   ran to within 21px of the dots). Reserve its lane for every width that can show
   it, including the short windows where it is hidden — a slightly narrower column
   is better than dots over a heading. */
@media (min-width: 1000px) {
  body[data-page="home"] .wrap { padding-right: 64px; }
}
/* On a shorter window centre the rail a little higher: at 560-700px the dots should
   not sit so low that they crowd the fold. */
@media (min-width: 1000px) and (max-height: 699px) {
  .scene-rail { top: 46%; }
}

@media (max-width: 999px), (max-height: 559px) {
  .scene-rail, .scene-rail-label { display: none; }
  .home-scene-backdrop { opacity: .32; }
}

/* ========================================================================== 
   Reduced motion: everything off, page fully static and readable
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  html[data-motion="on"] [data-reveal] { opacity: 1 !important; transform: none !important; transition: none !important; }
  .skeleton::after { animation: none; }
  .toast, .modal, .modal__box, .dropdown__panel, .nav, .nav > *, html[data-motion="on"] body { animation: none !important; }
  .toast::after { animation: none; }
  .poll-bar > span { transition: none; transform: scaleX(var(--v, 0)); }
  .photo-card img, .card, .btn, .tilt, .btn--magnetic { transition: none !important; }
  .month-calendar tbody td, .calendar-tile { animation: none !important; transition: none !important; }
  .calendar-tile:hover, .calendar-tile:focus-within { transform: none !important; }
  .btn:active { transform: none; }
  .ripple, .burst__dot { display: none; }
  .lightbox img { animation: none !important; }
  /* the fact bubbles are static by design, so reduced motion has nothing to undo */
  html.home-scenes-on { scroll-snap-type: none; }
  .home-scene-backdrop, .scene-rail, .scene-rail-label { display: none; }
  body[data-page="home"] [data-scene-copy] .scene-word { opacity: 1 !important; transform: none !important; transition: none !important; }
}

/* printing never needs motion */
@media print {
  .scroll-progress, .to-top, .home-scene-backdrop, .scene-rail, .scene-rail-label { display: none !important; }
  html[data-motion="on"] [data-reveal] { opacity: 1 !important; transform: none !important; }
  body[data-page="home"] [data-scene-copy] .scene-word { opacity: 1 !important; transform: none !important; }
}
