/* FloosYo · Onboarding
   Loaded only on /onboarding via app/views/onboarding/show.html.erb.
   Moved verbatim from the inline <style> block that used to live there.
   Rule order matches the original file; do not reorder without re-running
   the byte-diff check described in the refactor plan. */

:root {
  --brand-dark: #1a1a2e;
  --brand-blue: #3D8AFE;
  --brand-blue-light: #60a5fa;
  --brand-blue-pale: #bfdbfe;
  --text-primary: #1a1a2e;
  --text-secondary: #64748b;
  --input-bg: rgba(255,255,255,0.7);
  --input-border: rgba(0,0,0,0.08);
  --input-placeholder: #94a3b8;
  --dot-inactive: #cbd5e1;
  --btn-back-bg: rgba(61,138,254,0.08);
  --feature-bg: rgba(61,138,254,0.06);
  --feature-border: rgba(61,138,254,0.1);
  --safe-top: max(env(safe-area-inset-top, 20px), 20px);
  --safe-bottom: max(env(safe-area-inset-bottom, 20px), 34px);
}

[data-theme="floosyo-dark"] {
  --brand-dark: #eaedf3;
  --text-primary: #eaedf3;
  --text-secondary: #94a3b8;
  --input-bg: rgba(255,255,255,0.08);
  --input-border: rgba(255,255,255,0.12);
  --input-placeholder: #64748b;
  --dot-inactive: #374151;
  --btn-back-bg: rgba(61,138,254,0.15);
  --feature-bg: rgba(61,138,254,0.12);
  --feature-border: rgba(61,138,254,0.2);
}

.onboarding { position: relative; width: 100%; height: 100vh; overflow: hidden; touch-action: pan-x; -webkit-overflow-scrolling: auto; }

.bg-gradient {
  position: fixed; inset: 0; z-index: 0;
  background: var(--color-base-100, #f0f4ff);
}

.progress-container {
  position: fixed; top: 0; inset-inline: 0;
  z-index: 105;
  padding: calc(var(--safe-top) + 10px) 16px 10px;
  background: var(--color-base-100, #f0f4ff);
  pointer-events: none;
  transition: opacity 0.3s ease;
}
.progress-row {
  display: flex;
  align-items: center;
  gap: 12px;
}
.ob-back-btn {
  flex: 0 0 auto;
  width: 24px; height: 24px;
  margin-inline-start: -4px;
  padding: 0;
  border: none;
  background: none;
  color: color-mix(in oklab, var(--color-base-content, #111) 70%, transparent);
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  pointer-events: auto;
  -webkit-tap-highlight-color: transparent;
  transition: transform 0.15s ease, opacity 0.2s ease;
}
.ob-back-btn[hidden] { display: none; }
.ob-back-btn:active { transform: scale(0.88); opacity: 0.6; }
.ob-back-btn svg { width: 24px; height: 24px; }
[dir="rtl"] .ob-back-btn svg { transform: scaleX(-1); }
.progress-bar {
  position: relative;
  flex: 1 1 auto;
  width: 100%; height: 3px;
  border-radius: 999px;
  background: color-mix(in oklab, var(--color-base-content, #111) 12%, transparent);
  overflow: hidden;
}
.progress-bar__fill {
  position: absolute; top: 0; bottom: 0; inset-inline-start: 0;
  width: 0%;
  border-radius: 999px;
  background: var(--brand-blue, #3D8AFE);
  transition: width 0.45s cubic-bezier(0.22, 1, 0.36, 1);
}
.onboarding:has(.slide.active[data-slide="0"]) .progress-container {
  opacity: 0;
}

.welcome-corners {
  position: fixed;
  top: var(--safe-top); inset-inline: 0;
  z-index: 110;
  display: flex; justify-content: flex-end; align-items: center;
  padding: 12px 16px;
  opacity: 0; pointer-events: none;
  transition: opacity 0.35s ease;
}
.onboarding:has(.slide.active[data-slide="0"]) .welcome-corners {
  opacity: 1; pointer-events: auto;
}

.welcome-signin-row {
  display: none;
  margin-top: 12px;
}
.onboarding:has(.slide.active[data-slide="0"]) .welcome-signin-row {
  display: block;
}

/* Reassurance line under the CTA — same show-on-one-slide mechanism as the
   sign-in row above, so the trial slide can close with the note the
   settings-side /billing/trial screen already carries. */
.trial-note-row {
  display: none;
  margin-bottom: 12px;
}
/* Shown on both paywall lead-in slides: trial_intro (12) and trial_reminder
   (13). These indices move whenever a slide is added or cut — they last read
   26/27, from the 29-slide flow, which matched nothing and silently hid the
   note on every screen. */
.onboarding:has(.slide.active[data-slide="12"]) .trial-note-row,
.onboarding:has(.slide.active[data-slide="13"]) .trial-note-row {
  display: block;
}
/* Those two CTAs are the commitment, not a "next" step, so they drop the shared
   forward arrow the other slides use. */
.onboarding:has(.slide.active[data-slide="12"]) .btn-next .btn-icon,
.onboarding:has(.slide.active[data-slide="13"]) .btn-next .btn-icon {
  display: none;
}
.trial-note {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  text-align: center;
  font-size: 14px;
  font-weight: 700;
  color: var(--color-base-content);
  opacity: 0.75;
  margin: 0;
}
.trial-note-check {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
}

/* Simple phone frame for the trial screenshot: a rounded bordered box, no
   notch, no photoreal device.

   The asset is a raw 1320x2868 device capture whose top 210px are the iOS
   status bar — clock, dynamic island, wifi, battery. Rather than crop the file,
   the image is pinned to the BOTTOM of the frame and the frame is shorter than
   the image is tall, so the overflow is taken off the top.

   That makes the two sizes independent knobs, which is the point:
     width  — scales the whole screenshot
     height — how much of it is visible; shrinking it hides more of the TOP and
              never moves the bottom edge
   `aspect-ratio` is deliberately NOT used here: it would tie height to width,
   so reducing the height would shrink the image instead of cropping it.

   At 25.1vh wide the image stands 54.5vh tall, so a 50.6vh frame hides 3.9vh —
   about 34pt on an 874pt phone, which is those 210 source pixels.

   `margin-block: auto` centres the frame in the space left between the
   headline and the CTA, the same way .slide--trial .trial-timeline did. */
/* Wall geometry for the whole slide, in one place.

   The floaters' outer edge must land the same 16px from the screen edge as the
   steps indicator (.progress-container padding). Two things follow from that:

   1. --trial-float-out: how far a card hangs past the frame. Walking outwards,
      the shell is 24px in from each screen edge (.slide padding) and the frame
      is centred inside it, so shell edge to frame edge is (shell - frame) / 2.
      Deriving it rather than hardcoding a vh value matters because the shell
      caps at 380px, so that gap changes with screen width.

   2. .slide-body has to stop clipping. It is the vertical scroll host, and CSS
      computes overflow-x: visible to auto when overflow-y is auto, so it clips
      horizontally at its own box. Matching that box to the cards' resting edge
      is not enough: the rise peaks at scale(1.02), so a card momentarily grows
      past its resting edge and gets sliced mid-animation, and its drop shadow
      is cut even at rest. So the box bleeds out over the slide's full 24px
      padding instead, putting its edge on the screen edge — from there .slide's
      own overflow: hidden is the only clipper, and nothing visible is ever cut.
      Bleeding the box rather than setting overflow: visible keeps the vertical
      scrolling this slide may still need on a short phone. Horizontal overflow
      cannot appear either, since the cards rest 16px inside the box. */
.slide--trial {
  --trial-wall: 16px;
  --trial-bleed: 24px;
  --trial-overshoot: calc(var(--trial-bleed) - var(--trial-wall));
  --trial-shell: min(380px, 100vw - 48px);
  --trial-float-out: calc(var(--trial-overshoot) + (var(--trial-shell) - 25.1vh) / 2);
}
.slide--trial .slide-shell.slide-shell--anchored > .slide-body {
  width: calc(100% + var(--trial-bleed) * 2);
  margin-inline: calc(-1 * var(--trial-bleed));
}

.trial-shot {
  position: relative;
  flex: 0 0 auto;
  margin-block: auto;
  width: 25.1vh;
  height: 50.6vh;
  border: 5px solid var(--text-primary);
  border-radius: 26px;
  overflow: hidden;
  line-height: 0;
  box-shadow: 0 18px 40px -18px rgba(15, 23, 42, 0.35);
}
.trial-shot-img {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  display: block;
  width: 100%;
  height: auto;
}
/* Light capture by default, dark capture under the dark theme. mobile.html.erb
   always resolves data-theme to a concrete value — including for the "system"
   setting, which it re-resolves on prefers-color-scheme changes — so this swaps
   live with the theme and needs no JS. */
.trial-shot-img--dark { display: none; }
[data-theme="floosyo-dark"] .trial-shot-img--light { display: none; }
[data-theme="floosyo-dark"] .trial-shot-img--dark { display: block; }

/* ── Floaters ──────────────────────────────────────────────────────────────
   Three cards hung off the frame, one per topic: goals, subscriptions, month
   recap. The stage wraps the frame so they can overlap its edge — .trial-shot
   clips its own overflow, so they cannot live inside it.

   Everything is sized in vh, the same unit as the frame — including the type.
   That is what keeps each title on ONE line at every screen size: card width
   and font size scale together, so the fit is the same ratio on a 320pt phone
   as on a 440pt one. A fixed px font against a vh card would wrap on small
   phones. On a 402x874 phone: column 354pt, frame 219pt, 67pt clear each side;
   a 23vh card hanging 6.5vh out lands its far edge 11pt inside the column.

   Motion: they rise into place one after another, 0.42s apart, and then stay.
   `both` holds the final frame so nothing ever disappears. The bob that
   follows runs at a different period per card (5.2s / 5.9s / 6.4s) so three
   separate reassurances do not breathe in lockstep and read as one rigid
   object. Gated on BOTH .is-ready (set by the trial-floaters controller once
   the screenshot has decoded and the document is complete, so a slow load
   cannot swallow the entrance) and .active (so it plays when the slide is
   reached and replays on every return). */
.trial-stage {
  position: relative;
  flex: 0 0 auto;
  margin-block: auto;

}
.trial-stage .trial-shot {
  margin-block: 0;
}

.trial-float {
  position: absolute;
  /* Content width, not a fixed one. Each chip is anchored by its OUTER edge
     (inset-inline-end / -start), so shrinking takes the width off the INNER
     side: a shorter chip pulls away from the middle of the phone and reveals
     more of the screen, instead of shrinking about its own centre. */
  width: max-content;
  max-width: 26vh;
  padding: 1.2vh 1.5vh;
  border-radius: 14px;
  background: color-mix(in srgb, var(--color-base-200, #fff) 88%, transparent);
  backdrop-filter: blur(16px);
  box-shadow: 0 16px 34px -14px rgba(15, 23, 42, 0.3),
              0 2px 6px -2px rgba(15, 23, 42, 0.1);
  opacity: 0;
  will-change: transform, opacity;
}
/* 1px gradient hairline, drawn as a masked border so it can follow the radius. */
.trial-float::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 14px;
  padding: 1.2px;
  background: linear-gradient(135deg, #3B7DD8 0%, #5DA0F7 50%, #22C55E 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  pointer-events: none;
}
/* Two stacked lines, centred, no icon: the label carries the whole message now
   so the icon tile was just width. The <br> in the locale string owns the break,
   and nowrap stops either line wrapping anywhere else. */
.trial-float-head {
  margin: 0;
  font-size: 1.42vh;
  font-weight: 900;
  letter-spacing: -0.01em;
  line-height: 1.22;
  text-align: center;
  white-space: nowrap;
}

/* ── Trial reminder slide (25) ─────────────────────────────────────────────
   Standard anatomy: .slide-shell--anchored with the mascot above .slide-body,
   exactly as _cadence does it, so the mascot's size, top gap and breathe come
   from .slide-mascot rather than anything bespoke. The headline uses the same
   .slide-title + .sub-count-title recipe as _sub_count.

   The body fills the shell so the bell's auto margins have space to distribute;
   without that the bell parks under the copy and the slack all falls below. */
/* The base anchored rule gives .slide-body `flex: 0 1 auto` and
   `margin-bottom: auto`, so the shell's free space lands in that margin — below
   the body, where the bell cannot reach it. Letting the body grow and dropping
   that auto margin moves the space inside the body, which is what lets the
   bell's own auto margins centre it between the copy and the CTA. */
.slide--trial-reminder .slide-shell.slide-shell--anchored > .slide-body {
  flex: 1 1 auto;
  margin-bottom: 0;
  min-height: 0;
}
/* The bell is the middle element: auto margins centre it between the copy above
   and the CTA below. Motion is the shared enable-bell-ring, so there is one bell
   in the app rather than two, with the badge popping on the ring's crest. */
.trial-reminder-bell {
  position: relative;
  margin-block: auto;
  width: 18vh;
  max-width: 60%;
  color: color-mix(in srgb, var(--ocean, #3B7DD8) 16%, transparent);
  transform-origin: 50% 10%;
  animation: enable-bell-ring 3.4s cubic-bezier(0.32, 0.72, 0, 1) infinite;
}
[data-theme="floosyo-dark"] .trial-reminder-bell {
  color: color-mix(in srgb, var(--ocean, #3B7DD8) 30%, transparent);
}
.trial-reminder-bell svg {
  display: block;
  width: 100%;
  height: auto;
}
.trial-reminder-badge {
  position: absolute;
  top: 4%;
  inset-inline-end: 4%;
  min-width: 30%;
  aspect-ratio: 1;
  border-radius: 9999px;
  background: var(--color-error, #ef4444);
  color: #fff;
  font-size: 2.4vh;
  font-weight: 900;
  line-height: 1;
  display: grid;
  place-items: center;
  animation: trial-reminder-badge-pop 3.4s cubic-bezier(0.32, 0.72, 0, 1) infinite;
}
@keyframes trial-reminder-badge-pop {
  0%, 58%, 100% { transform: scale(1); }
  64%           { transform: scale(1.18); }
  74%           { transform: scale(0.97); }
  82%           { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .trial-reminder-bell,
  .trial-reminder-badge { animation: none; }
}

/* Placement — top to bottom: goal, then recap, then subs. Each card sits just
   ABOVE the row it relates to so that row stays visible underneath rather than
   being covered, and sides alternate so no two bands can touch. The goal card
   is the one pinned to real content: it clears the Emergency fund row and its
   mascot, which starts ~31.5% down the visible frame. Recap clears the "Your
   spending pace" block, which starts ~45% down — a card stands ~12% of the
   frame tall, so 30% puts its bottom edge just above that header.

   Chips are content-width, so each one is only as wide as its own label. */
.trial-float--goal  { top: calc(16% + 54px); inset-inline-end: calc(-1 * var(--trial-float-out)); }
.trial-float--recap { top: calc(30% + 84px); inset-inline-start: calc(-1 * var(--trial-float-out)); }
.trial-float--subs  { top: calc(73% + 5px); inset-inline-end: calc(-1 * var(--trial-float-out)); }

@keyframes trial-float-rise {
  0%   { opacity: 0; transform: translateY(30px) scale(0.93); }
  68%  { opacity: 1; transform: translateY(-5px) scale(1.02); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes trial-float-bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-5px); }
}

.slide--trial.active .trial-stage.is-ready .trial-float {
  animation: trial-float-rise 0.72s cubic-bezier(0.22, 1, 0.36, 1) both,
             trial-float-bob 5.2s ease-in-out infinite;
}
/* Delays follow the on-screen order, top to bottom, so the set reads as
   accumulating rather than jumping around. */
.slide--trial.active .trial-stage.is-ready .trial-float--goal {
  animation-delay: 0.15s, 0.87s;
}
.slide--trial.active .trial-stage.is-ready .trial-float--recap {
  animation-duration: 0.72s, 5.9s;
  animation-delay: 0.57s, 1.29s;
}
.slide--trial.active .trial-stage.is-ready .trial-float--subs {
  animation-duration: 0.72s, 6.4s;
  animation-delay: 0.99s, 1.71s;
}

/* Reduced motion: land them immediately, no rise, no bob. */
@media (prefers-reduced-motion: reduce) {
  .slide--trial.active .trial-stage.is-ready .trial-float,
  .slide--trial.active .trial-stage.is-ready .trial-float--goal,
  .slide--trial.active .trial-stage.is-ready .trial-float--subs,
  .slide--trial.active .trial-stage.is-ready .trial-float--recap {
    animation: none;
    opacity: 1;
  }
}

.welcome-signin {
  text-align: center;
  font-size: 13px;
  color: var(--color-base-content);
  opacity: 0.6;
  margin: 0;
}
.welcome-signin-link {
  color: var(--brand-blue);
  font-weight: 700;
  text-decoration: none;
  margin-inline-start: 4px;
}
.welcome-corner-btn {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 6px 10px;
  border-radius: 999px;
  background: color-mix(in oklab, var(--color-base-100, #fff) 65%, transparent);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  border: 1px solid color-mix(in oklab, var(--color-base-content, #111) 8%, transparent);
  font-size: 12px; font-weight: 600;
  color: var(--color-base-content, #111);
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
  transition: transform 0.15s ease, background 0.2s ease;
}
.welcome-corner-btn:active { transform: scale(0.96); }
.welcome-corner-btn svg { width: 14px; height: 14px; }
.welcome-corner-btn--muted {
  color: color-mix(in oklab, var(--color-base-content, #111) 55%, transparent);
}
/* CalAI-style locale picker (welcome screen, top-left). */
.welcome-locale-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  letter-spacing: 0.04em;
  padding: 5px 9px;
  line-height: 1;
}
.welcome-locale-flag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  line-height: 1;
  height: 14px;
  font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
}
.welcome-locale-code {
  display: inline-block;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  transform: translateY(0.5px); /* optical balance against the flag glyph */
}
.welcome-locale-chevron {
  width: 12px;
  height: 12px;
  opacity: 0.55;
  transition: transform 0.2s ease;
}

.welcome-locale-menu {
  margin-top: 6px;
  min-width: 200px;
  padding: 6px;
  border-radius: 14px;
  background: color-mix(in oklab, var(--color-base-100, #fff) 92%, transparent);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  border: 1px solid color-mix(in oklab, var(--color-base-content, #111) 8%, transparent);
  box-shadow: 0 8px 24px -8px color-mix(in oklab, var(--color-base-content, #111) 25%, transparent);
  display: none;
  flex-direction: column;
  gap: 2px;
  list-style: none;
  z-index: 120;
}
.welcome-locale:focus-within .welcome-locale-menu,
.welcome-locale.dropdown-open .welcome-locale-menu {
  display: flex;
  animation: welcomeLocaleMenuIn 0.16s ease-out both;
}
.welcome-locale-menu > li { display: contents; }
@keyframes welcomeLocaleMenuIn {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}
.welcome-locale-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 10px;
  border-radius: 10px;
  font-size: 14px;
  font-weight: 500;
  color: var(--color-base-content, #111);
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
  line-height: 1.1;
}
.welcome-locale-item:active {
  background: color-mix(in oklab, var(--color-base-content, #111) 8%, transparent);
}
.welcome-locale-item.is-active {
  background: color-mix(in oklab, var(--brand-blue, #3D8AFE) 12%, transparent);
  color: var(--brand-blue, #3D8AFE);
}
.welcome-locale-label { flex: 1 1 auto; }
.welcome-locale-label:lang(ar) {
  font-family: "Almarai", "Nunito Sans", ui-sans-serif, system-ui, sans-serif;
}
.welcome-locale-short {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  opacity: 0.55;
}
.welcome-locale-item.is-active .welcome-locale-short { opacity: 0.85; }
.welcome-locale-check { width: 14px; height: 14px; }

/* ── Uniform slide shell (Option A · self-balancing) ──────────────────────
   Three zones: top progress (slide padding-top), middle shell, bottom CTA
   (slide padding-bottom). Inside the shell, mascot uses margin-top: auto and
   body uses margin-bottom: auto so the pair centers as a unit when content
   is short, and the mascot snaps to the top with a fixed gap when content
   grows. Mascot scales with viewport height. */
.slide-shell {
  flex: 1 1 auto;
  width: 100%;
  max-width: 380px;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
}
/* When content is moderate but the mascot should pull to the top (no scroll,
   no body fade) — keeps the centered shell's natural sizing but flips its
   alignment so the pair reads top-down rather than vertically centered. */
.slide-shell.slide-shell--top { justify-content: flex-start; }

/* When a slide opts into "anchored" mode (heavy content), the shell
   self-balances with auto margins: when the mascot + body fit the viewport
   the pair centers as a unit (same optical feel as the statement slides);
   when content overflows the auto margins collapse to 0, the mascot pins to
   the top and the body becomes the scroll host — the original anchored
   behavior on small phones. */
.slide-shell.slide-shell--anchored {
  justify-content: flex-start;
  /* Minimum gap from the progress bar lives on the shell (not the mascot
     margin) so the mascot's margin-top can be `auto` without losing the
     guaranteed 40px breathing room when content overflows. */
  padding-top: 40px;
  /* Anchored mode skips the optical-balance bias since the mascot is already
     pinned to the top — every pixel below should go to the body/scroll area. */
  padding-bottom: 0;
}
.slide-shell.slide-shell--anchored > .slide-mascot {
  margin-top: auto;
  margin-bottom: clamp(8px, 1.5vh, 16px);
}
.slide-shell.slide-shell--anchored > .slide-body {
  flex: 0 1 auto;
  margin-bottom: auto;
  min-height: 0;
  justify-content: flex-start;
  /* Body becomes the scroll host while the mascot stays pinned above. */
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* Bottom peek-fade — iOS-native hint that more options exist below the fold.
     The last row visually fades into the page so scrollability is obvious
     without extra chrome. */
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 56px), rgba(0,0,0,0) 100%);
          mask-image: linear-gradient(to bottom, #000 calc(100% - 56px), rgba(0,0,0,0) 100%);
  padding-bottom: 24px;
}
.slide-shell.slide-shell--anchored > .slide-body::-webkit-scrollbar { display: none; }

/* Slide 24 (trial explainer) carries more copy than any other slide plus a
   docked CTA, so it opts out of the self-balancing margins: the mascot pins to
   the very top and every spare pixel goes to the timeline below it. Without
   this the `margin-top: auto` banked the slack above the mascot instead. */
.slide--trial .slide-shell.slide-shell--anchored {
  padding-top: 12px;
}
.slide--trial .slide-shell.slide-shell--anchored > .slide-mascot,
.slide--trial-reminder .slide-shell.slide-shell--anchored > .slide-mascot {
  margin-top: 0;
}
/* Mascot sized so the middle band lands at ~24px above and below the timeline
   (measured at 852pt). The vh term keeps short phones from pushing the timeline
   into a scroll. */
.slide--trial .slide-mascot {
  width: clamp(120px, 21vh, 170px);
}

/* The timeline floats in the middle band: mascot + hero copy sit above it, the
   docked CTA below. The body stretches to the slide's content edge and the list
   takes auto block margins, so the gap above the timeline equals the gap below
   it — self-balancing on every screen height rather than a fixed number.

   For those two gaps to read as equal they have to be measured against the same
   thing, so every asymmetric bit of chrome below the list is removed: the body's
   24px bottom padding, and the 6px by which the slide's bottom padding overshot
   the CTA sheet (this slide's sheet is taller than the others' — it carries the
   reassurance note). Measured result: 24.1px above, 24.6px below. */
.slide.slide--trial {
  padding-bottom: calc(var(--safe-bottom) + 114px);
}
.slide--trial .slide-shell.slide-shell--anchored > .slide-body {
  flex: 1 1 auto;
  margin-bottom: 0;
  padding-bottom: 0;
  /* No peek-fade here: with the list centred the gradient washed out the last
     line of the day-7 sentence instead of hinting at scrollable content. */
  -webkit-mask-image: none;
          mask-image: none;
}
.slide--trial .trial-explainer {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}
/* Pure auto margins on both sides — the hero subtitle drops its fixed bottom
   margin so nothing competes with the split. */
.slide--trial .trial-hero-sub {
  margin-bottom: 0;
}
.slide--trial .trial-timeline {
  margin-block: auto;
}

.slide-mascot {
  width: clamp(140px, 22vh, 200px);
  aspect-ratio: 1;
  /* Static minimum gap from the progress bar — applied in every slide so
     the mascot never crowds the top, regardless of content weight. */
  margin-top: 40px;
  margin-bottom: clamp(28px, 6vh, 56px);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.slide-mascot .mascot-img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  filter: drop-shadow(0 12px 28px rgba(61,138,254,0.1));
  animation: mascot-breathe 4.5s cubic-bezier(0.32, 0.72, 0, 1) infinite;
}
/* Per-slide mascot size modifiers — must come AFTER the base .slide-mascot
   width rule so the cascade picks them up. Use sparingly when the artwork's
   optical weight needs adjusting (e.g. surrounded by extra elements). */
.slide-mascot.slide-mascot--lg { width: clamp(160px, 25vh, 228px); }   /* +14% (was +20%, trimmed by 5%) */
.slide-mascot.slide-mascot--xl {
  width: clamp(182px, 28.6vh, 260px);   /* +30% — for artwork with extra props (cash piles, receipts) */
  /* Larger mascot needs less breathing room below — content should hug it
     tighter so the pair still feels balanced inside the same shell. */
  margin-bottom: clamp(4px, 1vh, 12px);
}
.slide-body {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
}

/* Welcome slide — vertically centered, two-line title */
.slide--welcome {
  justify-content: flex-start;
  padding-top: calc(var(--safe-top) + 80px);
  padding-bottom: calc(var(--safe-bottom) + 140px);
}
.welcome-inner {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 24px;
  width: 100%;
}
.welcome-title {
  display: flex; flex-direction: column;
  align-items: center;
  gap: 6px;
  margin: 0;
  line-height: 1.15;
}
.welcome-line { display: block; }

/* Welcome mascot entrance (welcome-grow controller): the mascot fades in mid-skip
   and its first landing is the entrance; see "Skip & Shout" below. Text styling
   (slide-title / brand-floos / brand-yo) is untouched. */
.welcome-grow-zone { position: relative; }
/* The drop-in replaces the generic slide-enter fade for this slide only. */
.slide.active.slide--welcome .slide-mascot { animation: none; }
.welcome-grow-grower {
  width: 100%;
  height: 100%;
}

/* Welcome mascot loop "Skip & Shout" (scratchpad prototype welcome-loops.html, card 6).
   It is also the entrance: welcome-grow adds .is-looping and fades the mascot in, and
   --welcome-phase starts the loop at the top of a skip, so the first thing seen is the
   mascot dropping in and landing. Then: crouch, one skip, squash on landing, ~1.6s
   rest, repeat every 2.8s. Each landing
   sends a jelly wobble through the FloosYo letters. The rig is an inline copy of
   mascot/welcoming.svg (steps/_welcome_mascot) so shadow, body, cheeks and sparkle
   move separately. Translate values are SVG user units: the art is 819 units wide,
   about 4 units per screen px. Inactive slides pause it (.slide:not(.active)). */
/* No drop-shadow filter here: the rig animates outside its own box, and in WebKit the
   filter's rectangular layer edge shows as a visible box on dark backgrounds. */
.welcome-rig {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.welcome-rig-body { transform-box: fill-box; transform-origin: 50% 100%; }
.welcome-rig-shadow,
.welcome-rig-star,
.welcome-rig-star-halo,
.welcome-rig-cheek,
.welcome-rig-eye { transform-box: fill-box; transform-origin: 50% 50%; }
.welcome-ch { display: inline-block; transform-origin: 50% 100%; }

.slide--welcome { --welcome-phase: -0.56s; }
.slide--welcome.is-looping .welcome-rig-body   { animation: welcome-skip-body 2.8s var(--welcome-phase) infinite; }
.slide--welcome.is-looping .welcome-rig-shadow { animation: welcome-skip-shadow 2.8s var(--welcome-phase) infinite; }
.slide--welcome.is-looping .welcome-rig-cheek  { animation: welcome-skip-cheek 2.8s var(--welcome-phase) infinite; }
.slide--welcome.is-looping .welcome-rig-eye    { animation: welcome-skip-eye 2.8s var(--welcome-phase) infinite; }
/* Glint burst (scratchpad prototype welcome-stars.html, option A): six glint stars
   (--star-delay staggers them, set inline) pop in with a flash on the landing frame,
   settle, twinkle twice, then go out, all in ~1.5s. The halo flashes with each
   twinkle. Hidden between bursts. */
.welcome-rig-star { opacity: 0; }
/* The first landing happens while the native splash is still fading out, so its stars
   go unseen. A one-off burst 0.6s later (~1.6s after load, splash gone) shows them on
   the first visit; it sits last in the list so it wins while running, then the loop
   carries on and bursts on every landing as usual. */
.slide--welcome.is-looping .welcome-rig-star {
  animation:
    welcome-skip-star 2.8s calc(var(--star-delay) + var(--welcome-phase)) infinite both,
    welcome-star-intro 1.54s calc(var(--star-delay) + 0.6s) 1;
}
.slide--welcome.is-looping .welcome-rig-star-halo {
  animation:
    welcome-skip-star-halo 2.8s calc(var(--star-delay) + var(--welcome-phase)) infinite both,
    welcome-star-halo-intro 1.54s calc(var(--star-delay) + 0.6s) 1;
}
.slide--welcome.is-looping .welcome-ch         { animation: welcome-skip-jelly 2.8s calc(var(--i) * 0.07s + var(--welcome-phase)) infinite; }

@keyframes welcome-skip-body {
  0%   { transform: none; animation-timing-function: ease-in-out; }
  6%   { transform: scale(1.1, 0.9) rotate(-3deg); animation-timing-function: cubic-bezier(0.2, 0.8, 0.4, 1); }
  12%  { transform: translateY(-60px) scale(0.96, 1.06) rotate(2deg); animation-timing-function: cubic-bezier(0.2, 0.7, 0.4, 1); }
  20%  { transform: translateY(-150px) rotate(4deg); animation-timing-function: cubic-bezier(0.6, 0, 0.8, 0.4); }
  28%  { transform: translateY(-40px) scale(0.95, 1.07) rotate(1deg); animation-timing-function: cubic-bezier(0.6, 0, 0.9, 0.5); }
  32%  { transform: scale(1.12, 0.88) rotate(-3deg); animation-timing-function: ease-out; }
  38%  { transform: scale(1.02, 0.98) rotate(-1deg); animation-timing-function: ease-in-out; }
  44%, 100% { transform: none; }
}
@keyframes welcome-skip-shadow {
  0%, 44%, 100% { transform: none; }
  6%   { transform: scaleX(1.1); animation-timing-function: cubic-bezier(0.2, 0.7, 0.4, 1); }
  20%  { transform: scaleX(0.55); opacity: 0.5; animation-timing-function: cubic-bezier(0.6, 0, 0.9, 0.5); }
  32%  { transform: scaleX(1.15); opacity: 1; }
}
/* Happy eyes squeeze thinner in the air (take-off 12% to just before landing 32%). */
@keyframes welcome-skip-eye {
  0%, 6%   { transform: none; }
  12%, 28% { transform: scaleY(0.55); }
  34%, 100% { transform: none; }
}
@keyframes welcome-skip-cheek {
  0%, 31%, 44%, 100% { transform: none; }
  36% { transform: scale(1.18); }
}
@keyframes welcome-skip-star {
  0%, 31% { transform: scale(0) rotate(-40deg); opacity: 0; }
  35%     { transform: scale(1.3) rotate(8deg); opacity: 1; animation-timing-function: cubic-bezier(0.2, 0.8, 0.3, 1); }
  41%     { transform: scale(0.9) rotate(0deg); opacity: 1; }
  50%     { transform: scale(1) rotate(4deg); opacity: 1; }
  57%     { transform: scale(0.62) rotate(10deg); opacity: 0.7; }
  62%     { transform: scale(1.12) rotate(14deg); opacity: 1; }
  70%     { transform: scale(0.6) rotate(18deg); opacity: 0.6; }
  74%     { transform: scale(0.95) rotate(20deg); opacity: 0.9; }
  86%, 100% { transform: scale(0) rotate(30deg); opacity: 0; }
}
@keyframes welcome-skip-star-halo {
  0%, 31% { transform: scale(0.3); opacity: 0; }
  35%     { transform: scale(1.5); opacity: 1; }
  46%     { transform: scale(1); opacity: 0.45; }
  57%     { opacity: 0.2; }
  62%     { transform: scale(1.25); opacity: 0.85; }
  70%     { opacity: 0.25; }
  74%     { opacity: 0.6; }
  86%, 100% { opacity: 0; }
}
/* Same burst as welcome-skip-star, its 31%-86% span stretched over the whole run. */
@keyframes welcome-star-intro {
  0%   { transform: scale(0) rotate(-40deg); opacity: 0; }
  7%   { transform: scale(1.3) rotate(8deg); opacity: 1; animation-timing-function: cubic-bezier(0.2, 0.8, 0.3, 1); }
  18%  { transform: scale(0.9) rotate(0deg); opacity: 1; }
  35%  { transform: scale(1) rotate(4deg); opacity: 1; }
  47%  { transform: scale(0.62) rotate(10deg); opacity: 0.7; }
  56%  { transform: scale(1.12) rotate(14deg); opacity: 1; }
  71%  { transform: scale(0.6) rotate(18deg); opacity: 0.6; }
  78%  { transform: scale(0.95) rotate(20deg); opacity: 0.9; }
  100% { transform: scale(0) rotate(30deg); opacity: 0; }
}
@keyframes welcome-star-halo-intro {
  0%   { transform: scale(0.3); opacity: 0; }
  7%   { transform: scale(1.5); opacity: 1; }
  27%  { transform: scale(1); opacity: 0.45; }
  47%  { opacity: 0.2; }
  56%  { transform: scale(1.25); opacity: 0.85; }
  71%  { opacity: 0.25; }
  78%  { opacity: 0.6; }
  100% { opacity: 0; }
}
@keyframes welcome-skip-jelly {
  0%, 31%, 50%, 100% { transform: none; }
  34% { transform: scale(1.18, 0.82); }
  38% { transform: translateY(-6px) scale(0.9, 1.12); }
  42% { transform: scale(1.05, 0.95); }
  46% { transform: scale(0.98, 1.02); }
}
@media (prefers-reduced-motion: reduce) {
  .slide--welcome.is-looping .welcome-rig-body,
  .slide--welcome.is-looping .welcome-rig-shadow,
  .slide--welcome.is-looping .welcome-rig-cheek,
  .slide--welcome.is-looping .welcome-rig-eye,
  .slide--welcome.is-looping .welcome-rig-star,
  .slide--welcome.is-looping .welcome-rig-star-halo,
  .slide--welcome.is-looping .welcome-ch { animation: none; }
}

/* Problem mascot loop "Question Storm" (scratchpad prototype lostcoin-loops.html, card 3)
   plus a blink. A steady stream of question marks rises from the head on winding
   arcs (three copies, 1.2s apart on a 3.6s cycle). Underneath: slow breathing (2.4s),
   the eyes blinking (they never move), and a sweat drop running down.
   Rig: steps/_problem_mascot (inline copy of mascot/lost-coin.svg). Translate values
   are SVG user units, about 3.8 per screen px. No JS: inactive slides pause it
   (.slide:not(.active)), the slide-enter fade sits on the wrapper. */
/* No drop-shadow filter here: the rig animates outside its own box, and in WebKit the
   filter's rectangular layer edge shows as a visible box on dark backgrounds. */
.problem-rig {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.problem-rig-body { transform-box: fill-box; transform-origin: 50% 100%; }
.problem-rig-eye,
.problem-rig-sweat,
.problem-rig-q { transform-box: fill-box; transform-origin: 50% 50%; }
.problem-rig-q--2,
.problem-rig-q--3 { opacity: 0; }

.slide--problem .problem-rig-body  { animation: problem-storm-body 2.4s ease-in-out infinite; }
.slide--problem .problem-rig-eye   { animation: problem-storm-eye 3.6s ease-in-out infinite; }
.slide--problem .problem-rig-sweat { animation: problem-storm-sweat 3.6s ease-in infinite; }
.slide--problem .problem-rig-q     { animation: problem-storm-q 3.6s ease-in-out infinite; }
.slide--problem .problem-rig-q--2  { animation-delay: -1.2s; }
.slide--problem .problem-rig-q--3  { animation-delay: -2.4s; }

@keyframes problem-storm-body {
  0%, 100% { transform: scale(1.02, 0.98); }
  50%      { transform: translateY(-8px) scale(0.99, 1.015); }
}
/* Eyes stay where they are drawn; they only blink. The eye ovals are drawn tilted 28deg
   clockwise (measured from the artwork), so the squash runs along that axis; the rotates
   stay in every keyframe so only the scale interpolates (from "none" the eyes would spin
   while closing). */
@keyframes problem-storm-eye {
  0%, 76%, 84%, 100% { transform: rotate(28deg) scaleY(1) rotate(-28deg); }
  80%      { transform: rotate(28deg) scaleY(0.1) rotate(-28deg); }
}
@keyframes problem-storm-sweat {
  0%, 20%   { transform: none; opacity: 1; }
  55%       { transform: translateY(70px); opacity: 0; }
  56%, 75%  { transform: none; opacity: 0; }
  95%, 100% { transform: none; opacity: 1; }
}
@keyframes problem-storm-q {
  0%   { transform: translate(0, 30px) scale(0.3); opacity: 0; }
  12%  { transform: none; opacity: 1; }
  40%  { transform: translate(-110px, -200px) rotate(-12deg) scale(0.95); opacity: 1; }
  70%  { transform: translate(40px, -380px) rotate(9deg) scale(0.85); opacity: 0.85; }
  88%, 100% { transform: translate(-30px, -470px) rotate(-4deg) scale(0.7); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .slide--problem .problem-rig-body,
  .slide--problem .problem-rig-eye,
  .slide--problem .problem-rig-sweat,
  .slide--problem .problem-rig-q { animation: none; }
}

/* Solution mascot loop "Power Charge" (scratchpad prototype fight-loops.html, card 5)
   plus a second sword shine. 4.2s: the sparkles fly in and gather on the sword while
   the mascot crouches, squints and trembles harder; then it releases with a big
   stretch, a wide flash up the blade and the sparkles bursting outward. Once settled,
   a normal gold shine runs up the blade again. Rig: steps/_solution_mascot (inline copy
   of mascot/fight-money.svg). Translate values are SVG user units, about 4 per screen
   px. No JS: inactive slides pause it (.slide:not(.active)). */
/* No drop-shadow filter here: the rig animates outside its own box, and in WebKit the
   filter's rectangular layer edge shows as a visible box on dark backgrounds. */
.solution-rig {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.solution-rig-body { transform-box: fill-box; transform-origin: 50% 100%; }
.solution-rig-shadow,
.solution-rig-eye,
.solution-rig-cheek,
.solution-rig-spark { transform-box: fill-box; transform-origin: 50% 50%; }
.solution-rig-mouth { transform-box: fill-box; transform-origin: 50% 0%; }
.solution-rig-gleam { opacity: 0; }
/* Outward burst direction and stagger, per sparkle. */
.solution-rig-spark--a { --sx: -70px; --sy: -40px; --k: 0; }
.solution-rig-spark--b { --sx: 70px;  --sy: -50px; --k: 1; }
.solution-rig-spark--c { --sx: -80px; --sy: 30px;  --k: 2; }
.solution-rig-spark--d { --sx: 50px;  --sy: 30px;  --k: 3; }
.solution-rig-spark--e { --sx: -40px; --sy: 50px;  --k: 4; }

.slide--solution .solution-rig-body   { animation: solution-charge-body 4.2s infinite; }
.slide--solution .solution-rig-shadow { animation: solution-charge-shadow 4.2s infinite; }
.slide--solution .solution-rig-eye    { animation: solution-charge-eye 4.2s infinite; }
.slide--solution .solution-rig-mouth  { animation: solution-charge-mouth 4.2s infinite; }
.slide--solution .solution-rig-cheek  { animation: solution-charge-cheek 4.2s infinite; }
.slide--solution .solution-rig-spark  { animation: solution-charge-spark 4.2s calc(var(--k) * 0.05s) infinite; }
.slide--solution .solution-rig-gleam  { animation: solution-charge-gleam 4.2s infinite; }

@keyframes solution-charge-body {
  0%   { transform: none; animation-timing-function: ease-in; }
  36%  { transform: scale(1.04, 0.95); }
  38%  { transform: scale(1.045, 0.945) rotate(0.6deg); }
  40%  { transform: scale(1.05, 0.94) rotate(-0.8deg); }
  42%  { transform: scale(1.055, 0.935) rotate(1deg); }
  44%  { transform: scale(1.06, 0.93) rotate(-1.2deg); }
  46%  { transform: scale(1.065, 0.925) rotate(1.2deg); animation-timing-function: cubic-bezier(0.2, 0.9, 0.3, 1.3); }
  52%  { transform: scale(0.93, 1.11) rotate(-3deg); animation-timing-function: ease-in-out; }
  60%  { transform: scale(1.04, 0.97) rotate(1deg); }
  68%  { transform: scale(0.99, 1.01); }
  76%, 100% { transform: none; }
}
@keyframes solution-charge-shadow {
  0%, 76%, 100% { transform: none; }
  46% { transform: scaleX(1.1); }
  52% { transform: scaleX(0.88); }
}
@keyframes solution-charge-eye {
  0%, 20%, 54%, 100% { transform: none; }
  44% { transform: scaleY(0.55); }
}
@keyframes solution-charge-mouth {
  0%, 46%, 72%, 100% { transform: none; }
  20%, 44% { transform: scale(0.85, 0.8); }
  52%, 64% { transform: scale(1.15, 1.35); }
}
@keyframes solution-charge-cheek {
  0%, 46%, 76%, 100% { transform: none; }
  54% { transform: scale(1.25); }
}
/* Sparkles start far out, gather onto the sword while charging, burst on release. */
@keyframes solution-charge-spark {
  0%   { transform: translate(calc(var(--sx) * 2.2), calc(var(--sy) * 2.2)) scale(0.4) rotate(-90deg); opacity: 0; }
  10%  { opacity: 0.8; }
  44%  { transform: translate(calc(var(--sx) * -0.15), calc(var(--sy) * -0.15)) scale(0.7) rotate(0deg); opacity: 1; animation-timing-function: cubic-bezier(0.2, 0.8, 0.3, 1); }
  56%  { transform: translate(calc(var(--sx) * 2), calc(var(--sy) * 2)) scale(1.9) rotate(90deg); opacity: 1; }
  70%  { transform: translate(calc(var(--sx) * 2.6), calc(var(--sy) * 2.6)) scale(0.6) rotate(150deg); opacity: 0; }
  100% { transform: translate(calc(var(--sx) * 2.2), calc(var(--sy) * 2.2)) scale(0.4) rotate(-90deg); opacity: 0; }
}
/* Two passes up the blade: a wide flash on release, then a normal shine once settled. */
@keyframes solution-charge-gleam {
  0%, 46% { transform: translateX(-40px); opacity: 0; }
  47%     { transform: translateX(-30px) scaleX(1.6); opacity: 1; animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
  58%     { transform: translateX(270px) scaleX(1.6); opacity: 1; }
  59%, 79% { transform: translateX(-40px); opacity: 0; }
  80%     { transform: translateX(-30px); opacity: 1; animation-timing-function: cubic-bezier(0.5, 0, 0.3, 1); }
  94%     { transform: translateX(270px); opacity: 1; }
  95%, 100% { transform: translateX(270px); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .slide--solution .solution-rig-body,
  .slide--solution .solution-rig-shadow,
  .slide--solution .solution-rig-eye,
  .slide--solution .solution-rig-mouth,
  .slide--solution .solution-rig-cheek,
  .slide--solution .solution-rig-spark,
  .slide--solution .solution-rig-gleam { animation: none; }
}

/* Name mascot loop "Thinking Quill" (scratchpad prototype name-loops.html, card 2).
   4.2s: taps the quill while thinking with eyebrows raised, then the idea lands: a hop
   with the mouth popping open, a swoosh of the quill, and the scroll appears written.
   Eyes never move, they only blink. Quill turns stay within 4deg so the shaft never
   leaves the fist. Rig: steps/_name_mascot (inline copy of mascot/write-name.svg).
   Translate values are SVG user units, about 4.4 per screen px. No JS: inactive slides
   pause it (.slide:not(.active)). */
.name-rig {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.name-rig-body { transform-box: fill-box; transform-origin: 50% 100%; }
.name-rig-eye,
.name-rig-brow { transform-box: fill-box; transform-origin: 50% 50%; }
.name-rig-mouth { transform-box: fill-box; transform-origin: 50% 0%; }
.name-rig-ink { transform-box: fill-box; transform-origin: 0% 50%; }
/* Pivot at the centre of the fist (~310,555 in art units; view-box origin 38,108). */
.name-rig-quill { transform-box: view-box; transform-origin: 272px 447px; }

.slide--name .name-rig-body  { animation: name-think-body 4.2s infinite; }
.slide--name .name-rig-quill { animation: name-think-quill 4.2s infinite; }
.slide--name .name-rig-eye   { animation: name-think-eye 4.2s ease-in-out infinite; }
.slide--name .name-rig-brow  { animation: name-think-brow 4.2s ease-in-out infinite; }
.slide--name .name-rig-mouth { animation: name-think-mouth 4.2s infinite; }
.slide--name .name-rig-ink   { animation: name-think-ink 4.2s calc(var(--r) * 0.12s) infinite both; }

@keyframes name-think-quill {
  0%, 4%   { transform: none; animation-timing-function: ease-in-out; }
  10%      { transform: rotate(2.6deg); }
  14%      { transform: rotate(1.1deg); }
  18%      { transform: rotate(2.6deg); }
  22%      { transform: rotate(1.1deg); }
  26%      { transform: rotate(2.6deg); }
  40%      { transform: rotate(2.6deg); animation-timing-function: cubic-bezier(0.6, 0, 0.8, 0.4); }
  48%      { transform: rotate(3.4deg); animation-timing-function: cubic-bezier(0.2, 0.8, 0.3, 1); }
  56%      { transform: rotate(-4deg); }
  62%      { transform: rotate(1.1deg); }
  68%      { transform: rotate(-0.6deg); }
  74%, 100% { transform: none; }
}
@keyframes name-think-eye {
  0%, 84%, 88%, 100% { transform: none; }
  86% { transform: scaleY(0.1); }
}
@keyframes name-think-brow {
  0%, 6%, 46%, 100% { transform: none; }
  12%, 40% { transform: translateY(-14px); }
}
@keyframes name-think-body {
  0%, 44% { transform: none; animation-timing-function: ease-in; }
  48%     { transform: scale(1.06, 0.94); animation-timing-function: cubic-bezier(0.2, 0.8, 0.4, 1); }
  54%     { transform: translateY(-30px) scale(0.95, 1.07); animation-timing-function: cubic-bezier(0.6, 0, 0.9, 0.5); }
  60%     { transform: scale(1.04, 0.96); }
  66%, 100% { transform: none; }
}
/* Still until the jump, pops open on the hop, settles back to the grin. */
@keyframes name-think-mouth {
  0%, 44%  { transform: none; animation-timing-function: cubic-bezier(0.2, 0.8, 0.3, 1.2); }
  52%      { transform: scale(1.22, 1.5); }
  58%      { transform: scale(1.12, 1.3); }
  62%      { transform: scale(1.18, 1.38); animation-timing-function: ease-in-out; }
  72%, 100% { transform: none; }
}
@keyframes name-think-ink {
  0%, 54% { opacity: 0; transform: scaleX(0); }
  60%     { opacity: 1; transform: scaleX(1); }
  92%     { opacity: 1; transform: scaleX(1); }
  100%    { opacity: 0; transform: scaleX(1); }
}
@media (prefers-reduced-motion: reduce) {
  .slide--name .name-rig-body,
  .slide--name .name-rig-quill,
  .slide--name .name-rig-eye,
  .slide--name .name-rig-brow,
  .slide--name .name-rig-mouth,
  .slide--name .name-rig-ink { animation: none; }
}

/* Greeting mascot loop "Heart Pop" (scratchpad prototype greet-loops.html, card 2).
   3.2s: a happy hop; on the jump the heart bubble springs out, the sparkles burst
   outward and the mouth opens. The heart beats twice while the cheeks flush, then the
   bubble shrinks away and the sparkles drift back. Eyes never move, they only blink.
   Rig: steps/_greeting_mascot (inline copy of mascot/enh-greetings.svg). Translate
   values are SVG user units, about 6 per screen px. Only the mascot moves: the text
   bubble and its tail are untouched. No JS: inactive slides pause it. */
.greeting-rig {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.greeting-rig-body { transform-box: fill-box; transform-origin: 50% 100%; }
.greeting-rig-shadow,
.greeting-rig-eye,
.greeting-rig-cheek,
.greeting-rig-spark,
.greeting-rig-heart { transform-box: fill-box; transform-origin: 50% 50%; }
.greeting-rig-mouth { transform-box: fill-box; transform-origin: 50% 0%; }
.greeting-rig-bubble { transform-box: fill-box; transform-origin: 85% 100%; }

.slide--greeting .greeting-rig-bubble { animation: greeting-pop-bubble 3.2s infinite; }
.slide--greeting .greeting-rig-heart  { animation: greeting-pop-heart 3.2s infinite; }
.slide--greeting .greeting-rig-body   { animation: greeting-pop-body 3.2s infinite; }
.slide--greeting .greeting-rig-shadow { animation: greeting-pop-shadow 3.2s infinite; }
.slide--greeting .greeting-rig-mouth  { animation: greeting-pop-mouth 3.2s infinite; }
.slide--greeting .greeting-rig-cheek  { animation: greeting-pop-cheek 3.2s infinite; }
.slide--greeting .greeting-rig-spark  { animation: greeting-pop-spark 3.2s calc(var(--j) * 0.05s) infinite; }
.slide--greeting .greeting-rig-eye    { animation: greeting-blink 3.2s infinite; }

@keyframes greeting-pop-bubble {
  0%, 8%    { transform: scale(0) rotate(-20deg); opacity: 0; animation-timing-function: cubic-bezier(0.2, 0.9, 0.3, 1.4); }
  18%       { transform: scale(1.12) rotate(4deg); opacity: 1; }
  24%       { transform: scale(0.96) rotate(-2deg); }
  30%, 74%  { transform: none; opacity: 1; animation-timing-function: ease-in; }
  84%, 100% { transform: scale(0) rotate(10deg); opacity: 0; }
}
@keyframes greeting-pop-heart {
  0%, 30%, 52%, 100% { transform: none; }
  36% { transform: scale(1.3); }
  41% { transform: scale(1.05); }
  46% { transform: scale(1.25); }
}
@keyframes greeting-pop-body {
  0%, 6%  { transform: none; animation-timing-function: ease-in; }
  10%     { transform: scale(1.06, 0.94); animation-timing-function: cubic-bezier(0.2, 0.8, 0.4, 1); }
  17%     { transform: translateY(-75px) scale(0.95, 1.07); animation-timing-function: cubic-bezier(0.6, 0, 0.9, 0.5); }
  24%     { transform: scale(1.05, 0.95); }
  32%, 100% { transform: none; }
}
@keyframes greeting-pop-shadow {
  0%, 10%, 32%, 100% { transform: none; }
  17% { transform: scaleX(0.8); opacity: 0.6; }
  24% { transform: scaleX(1.06); }
}
@keyframes greeting-pop-mouth {
  0%, 8%, 40%, 100% { transform: none; }
  16%, 30% { transform: scale(1.15, 1.3); }
}
@keyframes greeting-pop-cheek {
  0%, 30%, 55%, 100% { transform: none; }
  38%, 46% { transform: scale(1.2); }
}
/* Stars only exist for the burst on the jump: hidden at rest, they pop out, fly, fade. */
@keyframes greeting-pop-spark {
  0%, 12%   { transform: scale(0.7); opacity: 0; }
  14%       { opacity: 1; }
  20%       { transform: translate(calc(var(--dx) * 1.5), calc(var(--dy) * 0.8)) scale(1.6) rotate(60deg); opacity: 1; }
  34%       { transform: translate(calc(var(--dx) * 2), var(--dy)) scale(0.4) rotate(120deg); opacity: 0; }
  35%, 100% { transform: scale(0); opacity: 0; }
}
.greeting-rig-spark { opacity: 0; }
@keyframes greeting-blink {
  0%, 90%, 94%, 100% { transform: none; }
  92% { transform: scaleY(0.1); }
}
@media (prefers-reduced-motion: reduce) {
  .slide--greeting .greeting-rig-bubble,
  .slide--greeting .greeting-rig-heart,
  .slide--greeting .greeting-rig-body,
  .slide--greeting .greeting-rig-shadow,
  .slide--greeting .greeting-rig-mouth,
  .slide--greeting .greeting-rig-cheek,
  .slide--greeting .greeting-rig-spark,
  .slide--greeting .greeting-rig-eye { animation: none; }
}

/* Help mascot loop "Brain Strain" (scratchpad prototype think-loops.html, card 3).
   Thinking so hard it trembles: quick squeezes on a 0.8s beat, eyebrows pulsing down,
   sweat drops flying off left and right, question marks shivering, and the bulb trying
   to flicker on but never quite making it. Eyes never move, they only blink. Rig:
   steps/_help_mascot (inline copy of mascot/deep-thinking.svg). Translate values are SVG
   user units, about 3.5 per screen px. No JS: inactive slides pause it. */
.help-rig {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.help-rig-body { transform-box: fill-box; transform-origin: 50% 100%; }
.help-rig-eye,
.help-rig-brow,
.help-rig-sweat,
.help-rig-q { transform-box: fill-box; transform-origin: 50% 50%; }

.help-rig .help-rig-body         { animation: help-strain-body 0.8s infinite; }
.help-rig .help-rig-brow         { animation: help-strain-brow 0.8s infinite; }
.help-rig .help-rig-sweat--left  { animation: help-strain-sweat-left 1.6s infinite; }
.help-rig .help-rig-sweat--right { animation: help-strain-sweat-right 1.6s 0.8s infinite; }
.help-rig .help-rig-q            { animation: help-strain-q 0.4s infinite; }
.help-rig .help-rig-q--b         { animation-delay: 0.1s; }
.help-rig .help-rig-q--c         { animation-delay: 0.2s; }
.help-rig .help-rig-bulb         { animation: help-strain-bulb 2.4s infinite; }
.help-rig .help-rig-eye          { animation: help-strain-blink 3.2s infinite; }

@keyframes help-strain-body {
  0%, 100% { transform: scale(1.03, 0.97); }
  50%      { transform: scale(0.99, 1.01); }
}
@keyframes help-strain-brow {
  0%, 100% { transform: translateY(6px); }
  50%      { transform: translateY(2px); }
}
@keyframes help-strain-sweat-left {
  0%       { transform: none; opacity: 1; }
  60%      { transform: translate(-60px, -20px) rotate(-30deg); opacity: 0; }
  61%, 80% { transform: none; opacity: 0; }
  100%     { opacity: 1; }
}
@keyframes help-strain-sweat-right {
  0%       { transform: none; opacity: 1; }
  60%      { transform: translate(60px, -20px) rotate(30deg); opacity: 0; }
  61%, 80% { transform: none; opacity: 0; }
  100%     { opacity: 1; }
}
@keyframes help-strain-q {
  0%, 100% { transform: rotate(-6deg); }
  50%      { transform: rotate(6deg); }
}
/* Faded rather than greyed out: a colour filter on dark risks the WebKit layer box. */
@keyframes help-strain-bulb {
  0%, 100% { opacity: 0.5; }
  30% { opacity: 0.5; }
  33% { opacity: 0.85; }
  36% { opacity: 0.5; }
  70% { opacity: 0.5; }
  72% { opacity: 0.8; }
  74% { opacity: 0.5; }
}
@keyframes help-strain-blink {
  0%, 90%, 94%, 100% { transform: none; }
  92% { transform: scaleY(0.1); }
}
@media (prefers-reduced-motion: reduce) {
  .help-rig .help-rig-body,
  .help-rig .help-rig-brow,
  .help-rig .help-rig-sweat--left,
  .help-rig .help-rig-sweat--right,
  .help-rig .help-rig-q,
  .help-rig .help-rig-bulb,
  .help-rig .help-rig-eye { animation: none; }
}

/* Goals mascot loop "Heartbeat" (scratchpad prototype money2-loops.html, card 5).
   3s: a soft lub-dub, the body swelling twice very slightly with the cheeks flushing;
   on every beat a quick light sheen runs across both cash fans and green "$" signs pop
   in and fade. The fans never move. Rig: steps/_goals_mascot (inline copy of mascot/making-money.svg). No JS:
   inactive slides pause it (.slide:not(.active)). */
.goals-rig {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.goals-rig-body { transform-box: fill-box; transform-origin: 50% 100%; }
.goals-rig-shadow,
.goals-rig-cheek,
.goals-rig-spark { transform-box: fill-box; transform-origin: 50% 50%; }
.goals-rig-sheen,
.goals-rig-spark { opacity: 0; }
.goals-rig-mouth { transform-box: fill-box; transform-origin: 50% 0%; }

.goals-rig .goals-rig-body          { animation: goals-beat-body 3s infinite; }
.goals-rig .goals-rig-shadow        { animation: goals-beat-shadow 3s infinite; }
.goals-rig .goals-rig-cheek         { animation: goals-beat-cheek 3s infinite; }
.goals-rig .goals-rig-mouth         { animation: goals-beat-laugh 1s ease-in-out infinite; }
.goals-rig .goals-rig-sheen--left   { animation: goals-beat-sheen 3s ease-in-out infinite; }
.goals-rig .goals-rig-sheen--right  { animation: goals-beat-sheen 3s ease-in-out 0.12s infinite; }
/* The "$" signs are hidden at rest and pop in with the sheen, on every heartbeat. */
.goals-rig .goals-rig-spark--left   { animation: goals-beat-spark 3s calc(var(--k) * 0.05s) infinite both; }
.goals-rig .goals-rig-spark--right  { animation: goals-beat-spark 3s calc(0.12s + var(--k) * 0.05s) infinite both; }

@keyframes goals-beat-body {
  0%, 30%, 100% { transform: none; }
  6%  { transform: scale(1.025, 0.985); }
  12% { transform: scale(0.995, 1.005); }
  18% { transform: scale(1.02, 0.988); }
  24% { transform: none; }
}
@keyframes goals-beat-shadow {
  0%, 30%, 100% { transform: none; }
  6%, 18% { transform: scaleX(1.03); }
}
@keyframes goals-beat-cheek {
  0%, 30%, 100% { transform: none; }
  6%  { transform: scale(1.14); }
  12% { transform: scale(1.02); }
  18% { transform: scale(1.12); }
}
/* Sheen slides across the fan (clip path) along x, in SVG user units. */
@keyframes goals-beat-sheen {
  0%        { transform: translateX(-120px); opacity: 0; }
  3%        { opacity: 1; }
  22%       { transform: translateX(360px); opacity: 1; }
  25%, 100% { transform: translateX(360px); opacity: 0; }
}
/* Continuous, gentle laugh: the mouth softly opens and closes once a second
   (three per heartbeat cycle, so the two stay in step). */
@keyframes goals-beat-laugh {
  0%, 100% { transform: scale(1, 0.96); }
  50%      { transform: scale(1.04, 1.12); }
}
@keyframes goals-beat-spark {
  0%        { transform: translateY(6px) scale(0.3); opacity: 0; }
  7%        { transform: translateY(-4px) scale(1.2); opacity: 1; }
  14%       { transform: translateY(-10px) scale(1); opacity: 1; }
  28%, 100% { transform: translateY(-24px) scale(0.8); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .goals-rig .goals-rig-spark--left,
  .goals-rig .goals-rig-spark--right,
  .goals-rig .goals-rig-mouth,
  .goals-rig .goals-rig-body,
  .goals-rig .goals-rig-shadow,
  .goals-rig .goals-rig-cheek,
  .goals-rig .goals-rig-sheen--left,
  .goals-rig .goals-rig-sheen--right { animation: none; }
}

/* Budget mascot loop "Bubble Pop" (scratchpad prototype budget-loops.html, card 4).
   5s: typing with a tiny bob while coffee steam rises; the chart bubble pops in with a
   spring, the pie slices whirl in one after another, the eyebrows lift, then the bubble
   pops away. Eyes never move, they only blink. Rig: steps/_budget_mascot (inline copy of
   mascot/create-budget.svg). The view box starts at 0,0, so the view-box origins below
   are art coordinates (about 3.9 per screen px). No JS: inactive slides pause it. */
.budget-rig {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.budget-rig-body { transform-box: fill-box; transform-origin: 50% 100%; }
.budget-rig-eye,
.budget-rig-brow { transform-box: fill-box; transform-origin: 50% 50%; }
/* fill-box origins (not view-box px): WebKit misplaces view-box origins on inline SVG,
   which pushed the bubble off the left edge mid-pop. Bubble grows from its tail
   (bbox 0,168-228,416); each slice spins around the pie centre (117,284 in art units). */
.budget-rig-bubble { transform-box: fill-box; transform-origin: 70% 98%; }
.budget-rig-slice  { transform-box: fill-box; }
.budget-rig-slice--green    { transform-origin: 67% 46%; }
.budget-rig-slice--blue     { transform-origin: 2% 108%; }
.budget-rig-slice--blue-low { transform-origin: -5% -2%; }
.budget-rig-steam {
  transform-box: fill-box;
  transform-origin: 50% 100%;
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  opacity: 0;
}

.budget-rig .budget-rig-steam         { animation: budget-steam 2.4s ease-out infinite; }
.budget-rig .budget-rig-steam--1      { animation-delay: 0.8s; }
.budget-rig .budget-rig-steam--2      { animation-delay: 1.6s; }
/* Bubble and slices start 0.7s late so the first pop lands after the slide's
   mascot-enter animation (0.65s). While that runs the wrapper is a composited
   layer, and WebKit clips the bubble (which sits on the SVG's left edge) to it. */
.budget-rig .budget-rig-bubble        { animation: budget-pop-bubble 5s 0.7s infinite both; }
.budget-rig .budget-rig-slice         { animation: budget-pop-slice 5s 0.7s infinite both; }
.budget-rig .budget-rig-slice--blue   { animation-delay: 0.82s; }
.budget-rig .budget-rig-slice--blue-low { animation-delay: 0.94s; }
.budget-rig .budget-rig-body          { animation: budget-pop-body 0.6s ease-in-out infinite; }
.budget-rig .budget-rig-brow          { animation: budget-pop-brow 5s infinite; }
.budget-rig .budget-rig-eye           { animation: budget-blink 5s infinite; }

/* Each wisp draws itself upward while drifting up and fading. */
@keyframes budget-steam {
  0%   { stroke-dashoffset: 100; transform: translateY(0) scaleX(1); opacity: 0; }
  15%  { opacity: 0.6; }
  60%  { stroke-dashoffset: 0; opacity: 0.45; }
  100% { stroke-dashoffset: -100; transform: translateY(-40px) scaleX(1.3); opacity: 0; }
}
@keyframes budget-pop-bubble {
  0%, 6%    { transform: scale(0); opacity: 0; animation-timing-function: cubic-bezier(0.2, 0.9, 0.3, 1.4); }
  16%       { transform: scale(1.06); opacity: 1; }
  22%, 78%  { transform: none; opacity: 1; animation-timing-function: ease-in; }
  86%, 100% { transform: scale(0); opacity: 0; }
}
@keyframes budget-pop-slice {
  0%, 18%   { transform: scale(0) rotate(-90deg); animation-timing-function: cubic-bezier(0.2, 0.9, 0.3, 1.3); }
  28%, 100% { transform: none; }
}
@keyframes budget-pop-body {
  0%, 100% { transform: none; }
  50%      { transform: translateY(-3px) scale(1.005, 0.995); }
}
@keyframes budget-pop-brow {
  0%, 16%, 34%, 100% { transform: none; }
  22%, 28% { transform: translateY(-10px); }
}
@keyframes budget-blink {
  0%, 90%, 94%, 100% { transform: none; }
  92% { transform: scaleY(0.1); }
}
@media (prefers-reduced-motion: reduce) {
  .budget-rig .budget-rig-steam,
  .budget-rig .budget-rig-bubble,
  .budget-rig .budget-rig-slice,
  .budget-rig .budget-rig-body,
  .budget-rig .budget-rig-brow,
  .budget-rig .budget-rig-eye { animation: none; }
}

/* Coin mascot loop "Coin Shine" (scratchpad prototype coin-loops.html, card 1).
   3.6s: a soft light sheen sweeps across the coin face, then the three gold stars flare
   one after another; a slow breath and a blink. Rig: steps/_coin_mascot (inline copy of
   mascot/big-coin.svg). Translate values are SVG user units, about 3.2 per screen px.
   No JS: inactive slides pause it (.slide:not(.active)). */
.coin-rig {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.coin-rig-body { transform-box: fill-box; transform-origin: 50% 100%; }
.coin-rig-eye,
.coin-rig-spark { transform-box: fill-box; transform-origin: 50% 50%; }
.coin-rig-sheen,
.coin-rig-spark { opacity: 0; }

.coin-rig .coin-rig-sheen     { animation: coin-shine-sheen 3.6s ease-in-out infinite; }
.coin-rig .coin-rig-spark     { animation: coin-shine-spark 3.6s infinite both; }
.coin-rig .coin-rig-spark--1  { animation-delay: 0.12s; }
.coin-rig .coin-rig-spark--2  { animation-delay: 0.24s; }
.coin-rig .coin-rig-eye       { animation: coin-shine-blink 3.6s infinite; }
.coin-rig .coin-rig-body      { animation: coin-shine-breath 3.6s ease-in-out infinite; }

@keyframes coin-shine-sheen {
  0%, 20%   { transform: translateX(0); opacity: 0; }
  22%       { opacity: 1; }
  42%       { transform: translateX(520px); opacity: 1; }
  44%, 100% { transform: translateX(520px); opacity: 0; }
}
/* Stars are hidden at rest: each fades in after the sheen, flares, then fades out. */
@keyframes coin-shine-spark {
  0%, 38%   { transform: scale(0.4); opacity: 0; }
  46%       { transform: scale(1.35) rotate(30deg); opacity: 1; }
  54%       { transform: scale(0.95) rotate(45deg); opacity: 1; }
  66%, 100% { transform: scale(0.6) rotate(60deg); opacity: 0; }
}
@keyframes coin-shine-blink {
  0%, 90%, 94%, 100% { transform: none; }
  92% { transform: scaleY(0.1); }
}
@keyframes coin-shine-breath {
  0%, 100% { transform: none; }
  50%      { transform: scale(1.008, 0.994); }
}
@media (prefers-reduced-motion: reduce) {
  .coin-rig .coin-rig-sheen,
  .coin-rig .coin-rig-spark,
  .coin-rig .coin-rig-eye,
  .coin-rig .coin-rig-body { animation: none; }
}

/* Auth mascot loop "Checklist Tick" (scratchpad prototype allset-loops.html, card 1).
   4.2s: the three clipboard checkboxes tick in one by one, the check bubble pops in, its
   gold check snaps in, sparkles burst and it does a happy hop with an open smile. All
   transform origins are fill-box (WebKit places view-box origins wrong on inline SVG).
   Rig: steps/_auth_mascot (inline copy of mascot/all-set.svg). No JS: inactive slides
   pause it (.slide:not(.active)). */
.auth-rig {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.auth-rig-body { transform-box: fill-box; transform-origin: 50% 100%; }
.auth-rig-shadow,
.auth-rig-check,
.auth-rig-tick,
.auth-rig-spark { transform-box: fill-box; transform-origin: 50% 50%; }
.auth-rig-mouth { transform-box: fill-box; transform-origin: 50% 0%; }
.auth-rig-bubble { transform-box: fill-box; transform-origin: 10% 95%; }

.auth-rig .auth-rig-check     { animation: auth-tick-check 4.2s infinite both; }
.auth-rig .auth-rig-check--2  { animation-delay: 0.35s; }
.auth-rig .auth-rig-check--3  { animation-delay: 0.7s; }
.auth-rig .auth-rig-bubble    { animation: auth-tick-bubble 4.2s infinite both; }
.auth-rig .auth-rig-tick      { animation: auth-tick-tick 4.2s infinite both; }
.auth-rig .auth-rig-spark     { animation: auth-tick-spark 4.2s calc(var(--k) * 0.08s) infinite both; }
.auth-rig .auth-rig-body      { animation: auth-tick-hop 4.2s infinite; }
.auth-rig .auth-rig-shadow    { animation: auth-tick-shadow 4.2s infinite; }
.auth-rig .auth-rig-mouth     { animation: auth-tick-mouth 4.2s infinite; }

@keyframes auth-tick-check {
  0%, 6%    { transform: scale(0); opacity: 0; animation-timing-function: cubic-bezier(0.2, 0.9, 0.3, 1.5); }
  14%       { transform: scale(1.3); opacity: 1; }
  20%, 82%  { transform: none; opacity: 1; }
  90%, 100% { transform: scale(0); opacity: 0; }
}
@keyframes auth-tick-bubble {
  0%, 30%   { transform: scale(0); opacity: 0; animation-timing-function: cubic-bezier(0.2, 0.9, 0.3, 1.4); }
  40%       { transform: scale(1.08); opacity: 1; }
  46%, 82%  { transform: none; opacity: 1; }
  90%, 100% { transform: scale(0); opacity: 0; }
}
@keyframes auth-tick-tick {
  0%, 38%   { transform: scale(0) rotate(-20deg); }
  46%       { transform: scale(1.25) rotate(6deg); }
  52%, 100% { transform: none; }
}
/* Sign-in badges (Apple, Google, email, lock) pop in after the hop, hold,
   then shrink away. No spin: the icons must stay readable. */
@keyframes auth-tick-spark {
  0%, 44%   { transform: scale(0); opacity: 0; animation-timing-function: cubic-bezier(0.2, 0.9, 0.3, 1.4); }
  50%       { transform: scale(1.15); opacity: 1; }
  55%, 80%  { transform: none; opacity: 1; }
  88%, 100% { transform: scale(0); opacity: 0; }
}
@keyframes auth-tick-hop {
  0%, 42%   { transform: none; }
  46%       { transform: scale(1.03, 0.96); }
  52%       { transform: translateY(-26px) scale(0.98, 1.03); }
  58%       { transform: scale(1.02, 0.98); }
  64%, 100% { transform: none; }
}
@keyframes auth-tick-shadow {
  0%, 46%, 64%, 100% { transform: none; }
  52% { transform: scaleX(0.88); opacity: 0.7; }
}
@keyframes auth-tick-mouth {
  0%, 44%, 66%, 100% { transform: none; }
  52%, 60% { transform: scale(1.1, 1.2); }
}
@media (prefers-reduced-motion: reduce) {
  .auth-rig .auth-rig-check,
  .auth-rig .auth-rig-bubble,
  .auth-rig .auth-rig-tick,
  .auth-rig .auth-rig-spark,
  .auth-rig .auth-rig-body,
  .auth-rig .auth-rig-shadow,
  .auth-rig .auth-rig-mouth { animation: none; }
}

/* Reminder mascot loop "Gentle Reminder" (scratchpad prototype remind-loops.html, card 5).
   The calm one: a slow 4s breath, the clock hands sweep smoothly round every 8s, the
   envelope bobs softly, cheeks glow, sparkles twinkle one at a time, and every 8s the
   alarm bells give a tiny ding. All origins are fill-box (WebKit-safe). Rig:
   steps/_reminder_mascot (inline copy of mascot/notify-level2.svg). No JS: inactive
   slides pause it (.slide:not(.active)). */
.reminder-rig {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.reminder-rig-body { transform-box: fill-box; transform-origin: 50% 100%; }
.reminder-rig-cheek,
.reminder-rig-spark,
.reminder-rig-envelope { transform-box: fill-box; transform-origin: 50% 50%; }
/* Pivot on the clock centre pin (~72% across, 54% down the hands' box). */
.reminder-rig-hands { transform-box: fill-box; transform-origin: 72% 54%; }
.reminder-rig-bells { transform-box: fill-box; transform-origin: 50% 100%; }

.reminder-rig .reminder-rig-body     { animation: reminder-breath 4s ease-in-out infinite; }
.reminder-rig .reminder-rig-hands    { animation: reminder-sweep 8s linear infinite; }
.reminder-rig .reminder-rig-envelope { animation: reminder-envelope 4s ease-in-out infinite; }
.reminder-rig .reminder-rig-bells    { animation: reminder-ding 8s infinite; }
.reminder-rig .reminder-rig-spark    { animation: reminder-twinkle 4s calc(var(--k) * 0.8s) ease-in-out infinite both; }
.reminder-rig .reminder-rig-cheek    { animation: reminder-cheek 4s ease-in-out infinite; }

@keyframes reminder-breath {
  0%, 100% { transform: scale(1.01, 0.99); }
  50%      { transform: scale(0.995, 1.012); }
}
@keyframes reminder-sweep { to { transform: rotate(360deg); } }
@keyframes reminder-envelope {
  0%, 100% { transform: none; }
  50%      { transform: translateY(-6px) rotate(-2deg); }
}
@keyframes reminder-ding {
  0%, 88%, 100% { transform: none; }
  90%, 94% { transform: rotate(-5deg); }
  92%, 96% { transform: rotate(5deg); }
}
@keyframes reminder-twinkle {
  0%, 100% { transform: scale(0.4); opacity: 0; }
  15%      { transform: scale(1.2) rotate(30deg); opacity: 1; }
  32%      { transform: scale(0.6) rotate(50deg); opacity: 0; }
}
@keyframes reminder-cheek {
  0%, 20%, 70%, 100% { transform: none; }
  40% { transform: scale(1.18); }
}
@media (prefers-reduced-motion: reduce) {
  .reminder-rig .reminder-rig-body,
  .reminder-rig .reminder-rig-hands,
  .reminder-rig .reminder-rig-envelope,
  .reminder-rig .reminder-rig-bells,
  .reminder-rig .reminder-rig-spark,
  .reminder-rig .reminder-rig-cheek { animation: none; }
  .reminder-rig .reminder-rig-spark { opacity: 1; }
}

/* Statement slides (Problem / Solution) — text-forward, no mascot, vertically centered */
.slide--statement {
  justify-content: center;
  padding-top: calc(var(--safe-top) + 80px);
  padding-bottom: calc(var(--safe-bottom) + 140px);
}
.statement-inner {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 24px;
  width: 100%;
}
.statement-title {
  display: flex; flex-direction: column;
  align-items: center;
  gap: 10px;
  margin: 0;
  text-align: center;
  font-size: 30px;
  font-weight: 800;
  line-height: 1.2;
  letter-spacing: -0.01em;
  color: var(--color-base-content, #111);
}
.statement-line { display: block; }
.statement-line .hl { color: var(--brand-blue); }
/* Only _solution uses this line, and its copy ("Subscriptions. Daily habits.
   Impulse buys.") measures 19.28em at weight 700 — 386px at the old 20px, well
   past the 345px content box, so it always broke in two. The vw term keeps it
   on one line from 320pt up, and 19px is the ceiling the big phones allow. */
.statement-line--muted {
  font-size: clamp(13px, 4.4vw, 19px);
  font-weight: 700;
  margin-top: 8px;
  color: color-mix(in oklab, var(--color-base-content, #111) 60%, transparent);
}
@media (max-width: 360px) {
  .statement-title { font-size: 28px; }
}
/* Slides 2 and 3 (_problem, _solution) take the welcome slide's .slide-title
   size, so the opening three screens read as one typographic set. Scoped to the
   two slides so _climax and _trial keep their own statement sizes. The two-class
   selector also outranks the 360px rule above, so 26px holds at every width. */
.slide--problem .statement-title,
.slide--solution .statement-title {
  font-size: 26px;
}
/* Slide 2 emphasis: the answer's sentence lines drop to the muted recipe so
   "You are not alone." carries the screen on its own. The shared clamp above was
   fitted to copy that no longer ships and lands at 16.5px on a 375pt phone, so
   the size is pinned here instead. The sibling rule stops the 8px top margin
   stacking on every sentence line; only the first one opens the gap. */
.slide--solution .statement-line--muted {
  font-size: 19px;
}
.slide--solution .statement-line--muted + .statement-line--muted {
  margin-top: 0;
}
.statement-eyebrow {
  align-self: stretch;
  text-align: start;
  margin: 0 0 12px;
}
.statement-desc {
  margin: 18px 0 0;
  font-size: 15px;
  line-height: 1.55;
  text-align: start;
  color: color-mix(in oklab, var(--color-base-content, #111) 70%, transparent);
  max-width: 360px;
}

/* Bombshell (slide 5) — three-tier hierarchy: eyebrow (name) · amount (hero) · caption */
.bombshell-story {
  display: flex; flex-direction: column;
  align-items: center; text-align: center;
  gap: 0; margin: 0;
  max-width: 420px;
}
.bombshell-story > p { margin: 0; }
.bombshell-hero {
  position: relative;
  display: flex; flex-direction: column;
  align-items: center;
  gap: 10px;
  margin: 18px 6px 12px;
  padding: 26px 30px 22px;
  text-align: center;
  color: var(--text-primary);
  max-width: 360px;
  text-wrap: balance;
  background: #ffffff;
  border-radius: 18px;
}
/* Dark mode — paper card becomes a soft dark surface; perforation dots use
   the dark page bg so the torn-edge illusion still reads. */
[data-theme="floosyo-dark"] .bombshell-hero {
  background: #1c2030;
  color: var(--text-primary);
}
[data-theme="floosyo-dark"] .bombshell-hero::before,
[data-theme="floosyo-dark"] .bombshell-hero::after {
  background-image: radial-gradient(circle at 7px 7px, var(--color-base-100, #0b0f1a) 5.5px, transparent 6px);
}
[data-theme="floosyo-dark"] .bombshell-hero .bombshell-caption::before {
  border-top-color: rgba(255,255,255,0.18);
}
[data-theme="floosyo-dark"] .bombshell-hero .bombshell-caption em {
  color: rgba(234,237,243,0.78);
}
[data-theme="floosyo-dark"] .bombshell-subnote {
  color: rgba(234,237,243,0.55);
}
[data-theme="floosyo-dark"] .bombshell-eyebrow {
  color: rgba(234,237,243,0.62);
}
[data-theme="floosyo-dark"] .bombshell-eyebrow bdi {
  color: var(--brand-blue);
}
[data-theme="floosyo-dark"] .bombshell-hero .hl-down {
  background: rgba(248, 113, 113, 0.18);
  color: #f87171;
}
/* Perforated top + bottom — circles match the page background so the card
   reads as a torn paper receipt. */
.bombshell-hero::before,
.bombshell-hero::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  height: 14px;
  background-image: radial-gradient(circle at 7px 7px, var(--color-base-100, #f0f4ff) 5.5px, transparent 6px);
  background-size: 14px 14px;
  background-repeat: repeat-x;
  pointer-events: none;
}
.bombshell-hero::before { top: -7px; }
.bombshell-hero::after  { bottom: -7px; transform: scaleY(-1); }
/* Dashed separator above the caption — like a receipt subtotal line. */
.bombshell-hero .bombshell-caption {
  position: relative;
  padding-top: 14px;
  margin-top: 4px;
  align-self: stretch;
}
.bombshell-hero .bombshell-caption::before {
  content: "";
  position: absolute;
  top: 0; left: 8%; right: 8%;
  border-top: 1px dashed color-mix(in oklab, var(--color-base-content, #111) 18%, transparent);
}
.bombshell-hero .bombshell-caption em {
  font-style: italic;
  font-weight: 600;
  color: color-mix(in oklab, var(--color-base-content, #111) 70%, transparent);
}
.bombshell-subnote {
  display: block;
  margin-top: 6px;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.1px;
  line-height: 1.4;
  color: color-mix(in oklab, var(--color-base-content, #111) 45%, transparent);
}
.bombshell-eyebrow {
  display: block;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.2px;
  color: color-mix(in oklab, var(--color-base-content, #111) 52%, transparent);
}
.bombshell-eyebrow bdi {
  font-weight: 700;
  color: var(--brand-blue);
}
.bombshell-amount {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin: 2px 0 0;
}
.bombshell-hero .hl {
  color: var(--brand-blue);
  font-weight: 900;
  font-size: 56px;
  line-height: 1;
  letter-spacing: -2px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.bombshell-hero .hl-unit {
  font-size: 0.5em;
  font-weight: 800;
  letter-spacing: -0.2px;
  color: color-mix(in oklab, var(--brand-blue) 80%, transparent);
  margin-inline-start: 4px;
  vertical-align: 0.18em;
}
.bombshell-hero .hl-down {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px; height: 32px;
  border-radius: 999px;
  background: color-mix(in oklab, #e11d48 14%, transparent);
  color: #e11d48;
  font-size: 0;
  position: relative;
  flex: none;
  animation: bombshell-minus-pulse 2.6s ease-in-out infinite;
}
.bombshell-hero .hl-down::before {
  content: "−";
  font-size: 26px;
  font-weight: 900;
  line-height: 1;
  letter-spacing: 0;
  transform: translateY(-1px);
}
@keyframes bombshell-minus-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(225, 29, 72, 0); }
  50%      { box-shadow: 0 0 0 8px rgba(225, 29, 72, 0.08); }
}
.bombshell-caption {
  display: block;
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -0.2px;
  line-height: 1.3;
  color: color-mix(in oklab, var(--color-base-content, #111) 62%, transparent);
}
:root[dir="rtl"] .bombshell-caption { line-height: 1.5; }
.bombshell-body {
  margin: 40px 0 0;
  font-size: 14.5px;
  font-weight: 400;
  line-height: 1.6;
  text-align: start;
  align-self: stretch;
  color: color-mix(in oklab, var(--color-base-content, #111) 68%, transparent);
  max-width: 360px;
  padding: 16px 18px;
  border-radius: 14px;
  background: color-mix(in oklab, #e11d48 4%, transparent);
  border-inline-start: 3px solid color-mix(in oklab, #e11d48 45%, transparent);
}
.bombshell-body .bombshell-stat {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin: 0;
}
.bombshell-stat-num {
  font-size: 28px;
  font-weight: 900;
  line-height: 1;
  letter-spacing: -0.8px;
  color: #e11d48;
  font-variant-numeric: tabular-nums;
  flex: none;
}
.bombshell-stat-num .bombshell-stat-denom {
  font-size: 18px;
  font-weight: 700;
  color: color-mix(in oklab, #e11d48 70%, transparent);
  margin-inline-start: 1px;
}
.bombshell-stat-label {
  font-size: 13px;
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: 0.1px;
  color: color-mix(in oklab, var(--color-base-content, #111) 78%, transparent);
  text-align: start;
}
.bombshell-body .bombshell-stat + p,
.bombshell-body p {
  margin: 0;
}
.bombshell-body strong {
  font-weight: 700;
  color: color-mix(in oklab, var(--color-base-content, #111) 92%, transparent);
}

/* Slide 5 stat citation — single informative paragraph under the receipt card.
   No box. The "2 of 5" inside <strong> picks up brand-blue as a quiet anchor. */
.bombshell-citation {
  margin: 32px auto 0;
  max-width: 320px;
  font-size: 14px;
  line-height: 1.55;
  font-weight: 500;
  text-align: center;
  color: color-mix(in oklab, var(--color-base-content, #111) 62%, transparent);
}
.bombshell-citation strong {
  font-weight: 800;
  color: var(--brand-blue);
}
:root[dir="rtl"] .bombshell-body { line-height: 1.85; }
@media (max-width: 360px) {
  .bombshell-eyebrow { font-size: 12px; }
  .bombshell-hero .hl { font-size: 46px; letter-spacing: -1.4px; }
  .bombshell-hero .hl-down { width: 28px; height: 28px; }
  .bombshell-hero .hl-down::before { font-size: 22px; }
  .bombshell-caption { font-size: 15px; }
  .bombshell-amount { gap: 8px; }
}

.climax-title { gap: 14px; }
.climax-line--break { margin-top: 10px; }

/* Slide 8 — Mirror: story rhythm, tight gap, brand-colored closer */
.mirror-eyebrow {
  color: var(--text-secondary);
  font-size: 13px; font-weight: 500;
  text-transform: lowercase;
  letter-spacing: 0.3px;
  margin: 0;
}
.mirror-eyebrow .hl { color: var(--brand-blue); font-weight: 700; }
.mirror-hero {
  font-size: 28px; line-height: 1.2; font-weight: 800;
  color: var(--text-primary);
  text-align: center;
  margin: 10px 0 4px;
  letter-spacing: -0.5px;
  max-width: 360px;
  text-wrap: balance;
}
.mirror-hero bdi { font-weight: 800; color: var(--brand-blue); }
.mirror-subline {
  margin: 6px auto 2px;
  max-width: 320px;
  font-size: 14px;
  line-height: 1.55;
  font-weight: 500;
  text-align: center;
  text-wrap: balance;
  color: color-mix(in oklab, var(--color-base-content, #111) 62%, transparent);
}
.mirror-hero-prefix {
  display: inline;
  font-size: inherit;
  font-weight: inherit;
  color: inherit;
  text-transform: none;
  letter-spacing: inherit;
}
/* Promise card — connected to the hero by a vertical spine + a magnifier node
   that straddles the card's top edge, so hero and goal read as one column
   instead of a detached stamp floating below. */
.mirror-spine {
  width: 3px;
  height: 24px;
  margin: 4px auto 0;
  border-radius: 2px;
  background: linear-gradient(to bottom, color-mix(in oklab, var(--brand-blue) 30%, transparent), var(--brand-blue));
}
.mirror-node {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center; justify-content: center;
  width: 60px; height: 60px;
  margin: 0 auto -30px;
  border-radius: 999px;
  background: #fff;
  border: 1px solid color-mix(in oklab, var(--brand-blue) 22%, transparent);
  box-shadow: 0 10px 26px -8px color-mix(in oklab, var(--brand-blue) 50%, transparent);
}
[data-theme="floosyo-dark"] .mirror-node {
  background: linear-gradient(180deg, #1b2336, #141b29);
  border-color: color-mix(in oklab, var(--brand-blue) 40%, transparent);
  box-shadow: 0 10px 26px -8px rgba(61,138,254,.5), inset 0 1px 0 rgba(255,255,255,.06);
}
.mirror-node-glyph { font-size: 26px; line-height: 1; }
.mirror-promise {
  position: relative;
  align-self: stretch;
  max-width: 360px;
  margin: 0 auto;
  padding: 42px 22px 24px;
  border-radius: 22px;
  border: 1px solid color-mix(in oklab, var(--brand-blue) 16%, transparent);
  background: color-mix(in oklab, var(--brand-blue) 5%, #fff);
  box-shadow: 0 20px 44px -24px color-mix(in oklab, var(--color-base-content, #111) 30%, transparent);
  text-align: center;
}
[data-theme="floosyo-dark"] .mirror-promise {
  border-color: color-mix(in oklab, var(--brand-blue) 22%, transparent);
  background: linear-gradient(180deg, color-mix(in oklab, var(--brand-blue) 9%, var(--input-bg)), color-mix(in oklab, var(--brand-blue) 3%, transparent));
  box-shadow: 0 24px 50px -24px rgba(0,0,0,.7);
}
.mirror-promise-label {
  margin: 0;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: color-mix(in oklab, var(--color-base-content, #111) 50%, transparent);
}
.mirror-promise-title {
  margin: 8px 0 0;
  font-size: 24px;
  line-height: 1.2;
  font-weight: 800;
  letter-spacing: -0.4px;
  color: var(--brand-blue);
  text-wrap: balance;
}
.mirror-promise-role {
  margin: 12px 0 0;
  font-size: 13px;
  line-height: 1.5;
  font-style: italic;
  color: color-mix(in oklab, var(--color-base-content, #111) 60%, transparent);
}
:root[dir="rtl"] .mirror-promise-role { line-height: 1.7; font-style: normal; }

/* Slide 17 — Commitment eyebrow deserves presence; inline response fades in */
.commit-eyebrow {
  font-size: 14px; font-weight: 600;
  color: var(--brand-blue);
  letter-spacing: 0.4px;
  text-transform: uppercase;
}
/* Slide 18 · Plan loading · CalAI-style linear bar + checklist, then merged ready state */

/* Option C — stack the loading + ready stages in one grid cell so the body
   footprint = max(both heights) and never changes during the crossfade. With a
   constant body height the centered shell stays put, so the mascot above never
   shifts. Stages crossfade in place via opacity (no display:none reflow). */
.slide--plan-loading .slide-body {
  display: grid;
  grid-template-areas: "stage";
}
.slide--plan-loading .pl-loading,
.slide--plan-loading .pl-ready {
  grid-area: stage;
  align-self: start;
}
.pl-stage-hidden {
  visibility: hidden;
  pointer-events: none;
}

.pl-loading {
  display: flex; flex-direction: column; align-items: center;
  gap: 14px; width: 100%; text-align: center;
  transition: opacity .28s ease, transform .28s ease;
}
.pl-loading.is-leaving { opacity: 0; transform: translateY(-6px); }
.pl-loading.pl-stage-hidden { opacity: 0; }

.pl-pct {
  font-size: 56px; font-weight: 800; color: var(--text-primary);
  letter-spacing: -0.04em; line-height: 1; margin: 0;
  font-variant-numeric: tabular-nums;
}
.pl-status {
  font-size: 15px; font-weight: 600; color: var(--text-secondary);
  margin: 0; min-height: 1.2em;
  transition: opacity .25s ease;
}

.pl-track {
  position: relative;
  width: 100%; height: 6px;
  border-radius: 999px;
  background: color-mix(in oklab, var(--text-primary, #111) 10%, transparent);
  overflow: hidden;
  margin: 4px 0 8px;
}
.pl-track-fill {
  position: absolute; top: 0; bottom: 0; inset-inline-start: 0;
  width: 0%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--brand-blue, #3D8AFE), color-mix(in oklab, var(--brand-blue, #3D8AFE) 70%, #8b5cf6));
  transition: width .25s linear;
}

.pl-checklist {
  list-style: none; margin: 4px 0 0; padding: 0;
  width: 100%; display: flex; flex-direction: column; gap: 12px;
  text-align: start;
}
.pl-item {
  display: flex; align-items: center; gap: 12px;
  font-size: 15px; font-weight: 600;
  color: color-mix(in oklab, var(--text-primary, #111) 45%, transparent);
  transition: color .25s ease, transform .25s ease;
}
.pl-item-bullet {
  flex: 0 0 22px; width: 22px; height: 22px;
  border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  background: color-mix(in oklab, var(--text-primary, #111) 10%, transparent);
  color: transparent;
  transition: background .25s ease, color .25s ease, transform .25s ease;
}
.pl-item-check { width: 14px; height: 14px; }
.pl-item.active {
  color: var(--text-primary);
}
.pl-item.active .pl-item-bullet {
  background: color-mix(in oklab, var(--brand-blue, #3D8AFE) 18%, transparent);
}
.pl-item.done {
  color: var(--text-primary);
}
.pl-item.done .pl-item-bullet {
  background: var(--brand-blue, #3D8AFE);
  color: #fff;
  animation: pl-pop .35s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

.pl-ready {
  width: 100%; max-width: 360px; margin: 0 auto;
  text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 22px 22px 20px;
  border-radius: 22px;
  background: color-mix(in oklab, var(--brand-blue) 5%, var(--input-bg));
  border: 1px solid color-mix(in oklab, var(--brand-blue) 16%, transparent);
  box-shadow: 0 18px 48px -28px color-mix(in oklab, var(--brand-blue) 50%, transparent);
  transition: opacity .4s ease, transform .4s ease;
}
.pl-ready.pl-stage-hidden { opacity: 0; transform: translateY(8px); }
.pl-ready-check {
  width: 36px; height: 36px; border-radius: 999px;
  background: color-mix(in oklab, #16a34a 18%, transparent);
  color: #16a34a;
  display: inline-flex; align-items: center; justify-content: center;
  margin-bottom: 2px;
  animation: pl-pop .45s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
.pl-ready-check svg { width: 20px; height: 20px; }
.pl-ready-title {
  font-size: 24px; font-weight: 800; color: var(--text-primary);
  margin: 4px 0 0; letter-spacing: -0.3px; line-height: 1.2;
}
.pl-ready-name { color: var(--brand-blue); font-weight: 800; }
.pl-ready-sub {
  font-size: 14.5px; line-height: 1.5; font-weight: 600;
  color: var(--text-secondary);
  margin: 8px 0 0; max-width: 300px;
}
.pl-ready-rule {
  width: 100%; height: 1px; margin: 16px 0 0;
  background: color-mix(in oklab, var(--brand-blue) 20%, transparent);
}
.pl-ready-ask {
  /* One line. "Ready to take control of your finances?" measures 18.55em at
     weight 800, so it needs 298px at the old 16.5px — past both the 290px cap
     that used to sit here and the card's 298px inner width at 390pt. The cap
     goes, and the size tracks the viewport: the card is 360px at most, minus
     44px of padding, so the ceiling is 17px. */
  font-size: clamp(13px, 4vw, 17px); line-height: 1.45; font-weight: 800;
  letter-spacing: -0.2px; color: var(--text-primary);
  margin: 14px 0 0; max-width: 100%;
}
.pl-ready-accent { color: var(--brand-blue); }
[data-theme="floosyo-dark"] .pl-ready {
  background: color-mix(in oklab, var(--brand-blue) 10%, var(--input-bg));
  border-color: color-mix(in oklab, var(--brand-blue) 28%, transparent);
}

@keyframes onb-fade-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes pl-pop { from { transform: scale(0.6); opacity: 0; } to { transform: scale(1); opacity: 1; } }

/* Name slide — eyebrow + bold single-question title */
.slide-eyebrow {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: color-mix(in oklab, var(--color-base-content, #111) 55%, transparent);
  text-align: start;
  align-self: stretch;
  margin: 0 0 6px;
}
.slide--name .name-title {
  text-align: start;
  align-self: stretch;
  font-size: 33px;
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--text-primary);
  margin-bottom: 28px;
}
.slide--name .slide-eyebrow {
  font-size: 15px;
  font-weight: 600;
  margin-bottom: 10px;
}
.slide--name .onboard-form { max-width: none; }
.slide--name .form-input {
  min-height: 56px;
  height: auto;
  padding: 14px 18px 18px;
  line-height: 1.5;
  font-size: 18px;
  font-weight: 600;
  border-radius: 16px;
}

/* Sub-count slide: left/right-aligned (start) content + full-width chips */
.slide--sub-count .slide-inner { text-align: start; }
.slide--sub-count .sub-count-title,
.slide--trial-reminder .sub-count-title {
  text-align: start;
  align-self: stretch;
  font-size: 24px;
  line-height: 1.3;
  margin-bottom: 8px;
}
/* Reminder slide spacing copied from the q_peace slide (7), which has the same
   anatomy: anchored shell, plain .slide-mascot, then .slide-body. The only gap
   q_peace has that this slide does not is its .slide-eyebrow line, which stands
   13px at line-height 1 plus a 6px bottom margin. Reproducing that 25px as top
   margin on the headline puts the copy at the same distance below the mascot
   without inventing an eyebrow this screen has no use for. */
.slide--trial-reminder .sub-count-title {
  margin-top: 25px;
}

/* The reminder headline centres; the questionnaire slides stay left-aligned,
   where the copy leads into a list of choices below it. */
.slide--trial-reminder .sub-count-title {
  text-align: center;
}
/* The trial slide's .slide-body bleeds 24px past the shell on each side so the
   floaters can overhang without being clipped. The headline lives inside that
   box, so it needs the bleed padded back or it runs to the screen edges instead
   of lining up with every other slide's copy.

   Margins restore the rhythm the old headline had: 27px above, -3px below. They
   still SUM to 24px, so the block's height is unchanged and the framed
   screenshot below — centred by the stage's auto margins — does not move. */
.slide--trial .statement-title {
  padding-inline: var(--trial-bleed);
  margin: 27px 0 -3px;
}
/* q_peace title is one word longer — drop so it lands on two lines, not three. */
.slide--sub-count[data-slide="8"] .sub-count-title {
  font-size: 22px;
}
/* Slide 7 asks the longest question in the flow. The break after the comma is
   hard-coded in the copy so the second clause always opens the line; that makes
   "When your money is under control," (16.7em in Nunito Sans at weight 800) the
   line that sets the ceiling: 20.65px at 393pt, 19.58px at 375pt. The vw term
   tracks those limits with a slack of about 2%, and the 21px cap is what the
   larger phones can take. */
.slide--sub-count[data-slide="7"] .sub-count-title {
  font-size: clamp(16px, 5.15vw, 21px);
}
.slide--sub-count[data-slide="8"] .sub-count-chips {
  margin-top: 10px;
}
/* Slides 4, 5 and 9 had their helper line sitting too far under the question.
   Tighten the title's bottom margin so the sub-line reads as part of the
   question block, not as a separate paragraph. Scoped per slide so q_peace
   (7, 8), commitment and trial_reminder keep their current rhythm. */
.slide--sub-count[data-slide="5"] .sub-count-title,
.slide--sub-count[data-slide="6"] .sub-count-title {
  margin-bottom: 2px;
}
.slide--goal .slide-title {
  margin-bottom: 2px;
}
.slide--sub-count .sub-count-desc {
  text-align: start;
  align-self: stretch;
  margin-bottom: 24px;
  max-width: none;
}
.sub-count-chips {
  width: 100%;
  display: flex; flex-direction: column;
  gap: 8px;
}
.sub-count-chip {
  display: flex; align-items: center; gap: 12px;
  width: 100%;
  min-height: 59px;
  padding: 13.5px 14px;
  border-radius: 14px;
  border: 1px solid transparent;
  background: var(--input-bg);
  box-shadow: none;
  font-family: inherit; font-size: 16px; font-weight: 600;
  color: var(--color-base-content, #111);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
  transition: transform 0.15s ease, border-color 0.2s ease, background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
}
.sub-count-dots {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px;
  border-radius: 999px;
  background: var(--color-base-100, #f0f4ff);
  color: var(--color-base-content, #111);
  flex-shrink: 0;
  box-shadow: none;
}
.sub-count-dots svg { width: 18px; height: 18px; display: block; }
.sub-count-chip.on .sub-count-dots { background: #fff; color: var(--brand-blue); box-shadow: none; }
[data-theme="floosyo-dark"] .sub-count-dots {
  background: rgba(255,255,255,0.92);
  color: #111;
  box-shadow: 0 1px 2px rgba(0,0,0,0.2);
}
.sub-count-chip:active { transform: scale(0.98); }
.sub-count-chip.on {
  border-color: var(--brand-blue);
  background: var(--brand-blue);
  color: #fff;
  box-shadow: 0 10px 24px -12px color-mix(in oklab, var(--brand-blue) 60%, transparent);
}
.sub-count-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px;
  border-radius: 10px;
  background: color-mix(in oklab, var(--brand-blue) 12%, transparent);
  color: var(--brand-blue);
  flex-shrink: 0;
  transition: background 0.2s ease, color 0.2s ease;
}
.sub-count-icon svg { width: 18px; height: 18px; display: block; }
.sub-count-chip.on .sub-count-icon {
  background: rgba(255, 255, 255, 0.22);
  color: #fff;
}
.sub-count-emoji {
  display: inline-flex;
  align-items: center; justify-content: center;
  width: 32px; height: 32px;
  border-radius: 999px;
  background: transparent;
  font-size: 24px; line-height: 1;
  flex-shrink: 0;
  box-shadow: none;
}
.sub-count-chip.on .sub-count-emoji {
  background: transparent;
  box-shadow: none;
}
[data-theme="floosyo-dark"] .sub-count-emoji {
  background: transparent;
  box-shadow: none;
}
/* Goal amount (slide 10) fits a card, a dial and four stamps above the CTA,
   which is one element more than any other slide carries. Everything on it is
   pulled in ~15% so the terms row clears the fold on a short phone. Scoped to
   the slide so the shared .slide-* sizes are untouched elsewhere. */
.slide--goal-amount .slide-title { font-size: 22px; margin-bottom: 6px; }
.slide--goal-amount .goal-lede { font-size: 13px; margin-bottom: 12px; }
.slide--goal-amount .goal-card { padding: 13px 15px; border-radius: 19px; }
.slide--goal-amount .goal-card-icon img { width: 29px; height: 29px; }
.slide--goal-amount .goal-card-name { font-size: 15.5px; }
.slide--goal-amount .goal-card-when { font-size: 11px; }
.slide--goal-amount .goal-card-amount { font-size: 32px; margin-top: 11px; }
.slide--goal-amount .goal-card-cur { font-size: 21px; }
.slide--goal-amount .goal-card-slider { margin-top: 11px; }
.slide--goal-amount .goal-card-slider .amount-track { height: 22px; }
.slide--goal-amount .goal-card-slider .amount-rail { height: 9px; }
.slide--goal-amount .goal-card-slider .amount-thumb { width: 23px; height: 23px; border-width: 3px; }
.slide--goal-amount .goal-card-slider .amount-glow { height: 13px; }
.slide--goal-amount .goal-card-foot { margin-top: 8px; font-size: 10.5px; }
.slide--goal-amount .goal-field-label { margin: 17px 0 8px; }
.slide--goal-amount .goal-field-label--spaced { margin-top: 17px; }
.slide--goal-amount .goal-stamp { padding: 10px 2px; border-radius: 13px; }
.slide--goal-amount .goal-stamp-big { font-size: 15.5px; }
.slide--goal-amount .goal-stamp-sub { font-size: 8px; }

/* ── Goal chart (forecast slide) ────────────────────────────────────────
   Three paths to the goal, sharing one curve. The shape is the compound
   chart's own, measured as height over x rather than over the bezier's t:
   height(x) = x^3.38 — flat for the first third, then it sweeps. Separation
   between the lines comes from where each one lands, never from bending them
   differently. Drawn by onboarding#renderGoalChart. */
.goal-chart-card {
  width: 100%;
  background: var(--color-base-200, #fff);
  border-radius: 26px;
  /* .slide-shell centres its text; this card reads as a document — eyebrow over
     date over chart — so it sets its own start alignment, same as the v5 mock. */
  text-align: start;
  padding: 18px 18px 14px;
  box-shadow: 0 20px 40px -28px rgba(15, 23, 42, 0.55), 0 1px 2px rgba(15, 23, 42, 0.04);
  margin-bottom: 18px;
}
.goal-chart-eyebrow {
  font-size: 9.5px; font-weight: 900; letter-spacing: .17em; text-transform: uppercase;
  color: color-mix(in oklab, var(--color-base-content, #111) 38%, transparent);
  margin: 0;
}
:root[dir="rtl"] .goal-chart-eyebrow { letter-spacing: normal; }
.goal-chart-head { display: flex; align-items: baseline; gap: 9px; margin: 6px 0 2px; }
.goal-chart-date {
  font-size: 42px; font-weight: 800; letter-spacing: -.035em; line-height: 1;
  color: #10B981; font-variant-numeric: tabular-nums;
}
.goal-chart-was {
  font-size: 14px; font-weight: 800; text-decoration: line-through;
  color: color-mix(in oklab, var(--color-base-content, #111) 30%, transparent);
  font-variant-numeric: tabular-nums;
}
.goal-chart-plot { position: relative; margin-top: 8px; }
.goal-chart-svg { display: block; width: 100%; overflow: visible; }
.goal-chart-axis { display: flex; justify-content: space-between; margin-top: 3px; padding: 0 2px; }
.goal-chart-axis span {
  font-size: 9.5px; font-weight: 800; letter-spacing: .05em;
  color: color-mix(in oklab, var(--color-base-content, #111) 32%, transparent);
}
:root[dir="rtl"] .goal-chart-axis span { letter-spacing: normal; }
.goal-chart-legend { display: flex; gap: 13px; margin-top: 11px; flex-wrap: wrap; }
.gcl {
  display: flex; align-items: center; gap: 6px;
  font-size: 11px; font-weight: 800;
  color: color-mix(in oklab, var(--color-base-content, #111) 62%, transparent);
}
.gcl i { width: 15px; height: 3.5px; border-radius: 2px; display: block; flex-shrink: 0; }

/* ── Goal water (save slide) ─────────────────────────────────────────────
   Copied verbatim from the goal strip's styles in budgets/index.html.erb, so
   the tank the user fills here is pixel-identical to the one waiting for them
   on the dashboard. Those styles are page-scoped to that view, which is why
   this is a copy rather than a reference.

   NOTE: mirrors budgets/index.html.erb. A change there wants copying here. */
.save-goal-strip {
  position: relative;
  width: 100%;
  border-radius: 18px;
  padding: 10px 16px;
  display: flex;
  align-items: center;
  gap: 8px;
  overflow: visible;
  background: linear-gradient(to left, var(--color-base-200, #FFF), var(--color-base-100, #F0F7FF));
  border: 1px solid rgba(61, 138, 254, 0.08);
  box-shadow: 0 1px 4px rgba(15, 23, 42, 0.05);
  opacity: 0;
  transform: translateY(8px);
  transition: opacity .45s ease, transform .45s cubic-bezier(.22,1,.36,1);
}
.save-goal-strip.is-on { opacity: 1; transform: translateY(0); }
.save-goal-strip .goal-strip-name {
  font-size: 13px; font-weight: 700;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.save-goal-strip .goal-strip-sub {
  font-size: 11px; line-height: 1.5;
  color: color-mix(in oklab, var(--color-base-content, #111) 65%, transparent);
}

.goal-fill {
  position: absolute;
  inset: 0;
  border-radius: 18px;
  overflow: hidden;
  pointer-events: none;
  /* One 64px period of the crest the strip has always used, as a mask tile
     repeated down the trailing edge of the water. */
  --goal-wave: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2012%2064'%20preserveAspectRatio='none'%3E%3Cpath%20fill='%23fff'%20d='M6,0%20C10.2,8%2010.2,24%206,32%20C1.8,40%201.8,56%206,64%20L0,64%20L0,0%20Z'/%3E%3C/svg%3E");
}
/* Shallow water under the uncovered part, so both sides of the wave are the
   same material instead of water against dead space. */
.goal-fill-sea {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, transparent 0%, rgba(16, 185, 129, 0.045) 45%, rgba(16, 185, 129, 0.08) 100%);
}
.goal-fill-liquid {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  /* +6px: the wave swings ±4.2px inside its 12px strip, so the mean surface
     sits 6px in from the element's own edge. */
  width: calc(var(--goal-pct) + 6px);
  min-width: 16px;
  overflow: hidden;
  background: linear-gradient(180deg, rgba(16, 185, 129, 0.10) 0%, rgba(16, 185, 129, 0.18) 55%, rgba(16, 185, 129, 0.26) 100%);
  /* Layer 1 is the flat body, layer 2 the wave tile repeating down the last
     12px. Compositing them together is what gives the water a wavy edge. */
  -webkit-mask-image: linear-gradient(#000, #000), var(--goal-wave);
  mask-image: linear-gradient(#000, #000), var(--goal-wave);
  -webkit-mask-size: calc(100% - 12px) 100%, 12px 64px;
  mask-size: calc(100% - 12px) 100%, 12px 64px;
  -webkit-mask-repeat: no-repeat, repeat-y;
  mask-repeat: no-repeat, repeat-y;
  -webkit-mask-composite: source-over;
  mask-composite: add;
  animation: goalWave 2.8s linear infinite;
}
/* Caustics: light pools drifting through the water, in place of a printed
   stripe pattern. Masked along with everything else inside the water. */
.goal-fill-liquid::before {
  content: "";
  position: absolute;
  inset: -30%;
  background:
    radial-gradient(42px 22px at 18% 52%, rgba(255, 255, 255, 0.55), transparent 70%),
    radial-gradient(56px 26px at 58% 76%, rgba(255, 255, 255, 0.55), transparent 72%),
    radial-gradient(34px 18px at 88% 34%, rgba(255, 255, 255, 0.55), transparent 70%);
  opacity: 0.22;
  filter: blur(7px);
  animation: goalCaustics 10s ease-in-out infinite alternate;
}
/* Meniscus: a bright band at the water's trailing edge. The wave mask cuts
   it into the surface line. */
.goal-fill-liquid::after {
  content: "";
  position: absolute;
  inset-block: 0;
  right: 0;
  width: 14px;
  background: linear-gradient(to right, transparent, rgba(16, 185, 129, 0.55) 88%, rgba(16, 185, 129, 0.55));
  opacity: 0.75;
}
/* Bubbles rise only inside the water, so they can never appear in the part
   that is not saved yet. Sizes, speeds, drifts and delays all differ —
   regular bubbles read as a pattern, not as water. */
.goal-bubble {
  position: absolute;
  bottom: -7px;
  border-radius: 50%;
  background:
    radial-gradient(circle at 32% 28%, rgba(255, 255, 255, 0.55), transparent 62%),
    radial-gradient(circle at 70% 78%, rgba(255, 255, 255, 0.55), transparent 74%);
  box-shadow: inset 0 0 0 0.7px rgba(16, 185, 129, 0.5);
  opacity: 0;
  animation: goalBubble var(--bubble-dur) cubic-bezier(0.4, 0, 0.7, 1) var(--bubble-delay) infinite;
}
.goal-bubble-1 { --bubble-dur: 5.4s; --bubble-delay: 0s;   --bubble-drift: 5px;  inset-inline-start: 16%; width: 5px;   height: 5px; }
.goal-bubble-2 { --bubble-dur: 7.1s; --bubble-delay: 1.3s; --bubble-drift: -4px; inset-inline-start: 34%; width: 3.5px; height: 3.5px; }
.goal-bubble-3 { --bubble-dur: 4.8s; --bubble-delay: 2.6s; --bubble-drift: 6px;  inset-inline-start: 55%; width: 6px;   height: 6px; }
.goal-bubble-4 { --bubble-dur: 6.3s; --bubble-delay: 0.7s; --bubble-drift: -5px; inset-inline-start: 72%; width: 4px;   height: 4px; }
.goal-bubble-5 { --bubble-dur: 8.0s; --bubble-delay: 3.5s; --bubble-drift: 3px;  inset-inline-start: 88%; width: 2.5px; height: 2.5px; }
/* Mirror the whole water block so the wave and its meniscus lean with the
   fill direction in RTL. */
[dir="rtl"] .goal-fill-liquid { transform: scaleX(-1); }

/* Percentage tracks the app's theme, not the OS. Tailwind's dark: variant
   compiles to prefers-color-scheme, which is the wrong signal here: the user
   picks light/dark in settings and the two disagree constantly. */
.goal-strip-pct { color: #059669; }
[data-theme="floosyo-dark"] .goal-strip-pct { color: #34D399; }

[data-theme="floosyo-dark"] .goal-fill-sea {
  background: linear-gradient(180deg, transparent 0%, rgba(52, 211, 153, 0.05) 45%, rgba(52, 211, 153, 0.09) 100%);
}
[data-theme="floosyo-dark"] .goal-fill-liquid {
  background: linear-gradient(180deg, rgba(52, 211, 153, 0.10) 0%, rgba(52, 211, 153, 0.19) 55%, rgba(52, 211, 153, 0.28) 100%);
}
[data-theme="floosyo-dark"] .goal-fill-liquid::before {
  background:
    radial-gradient(42px 22px at 18% 52%, rgba(255, 255, 255, 0.16), transparent 70%),
    radial-gradient(56px 26px at 58% 76%, rgba(255, 255, 255, 0.16), transparent 72%),
    radial-gradient(34px 18px at 88% 34%, rgba(255, 255, 255, 0.16), transparent 70%);
}
[data-theme="floosyo-dark"] .goal-fill-liquid::after {
  background: linear-gradient(to right, transparent, rgba(52, 211, 153, 0.6) 88%, rgba(52, 211, 153, 0.6));
}
[data-theme="floosyo-dark"] .goal-bubble {
  background:
    radial-gradient(circle at 32% 28%, rgba(255, 255, 255, 0.16), transparent 62%),
    radial-gradient(circle at 70% 78%, rgba(255, 255, 255, 0.16), transparent 74%);
  box-shadow: inset 0 0 0 0.7px rgba(52, 211, 153, 0.55);
}

@keyframes goalWave {
  from { -webkit-mask-position: 0 0, 100% 0;     mask-position: 0 0, 100% 0; }
  to   { -webkit-mask-position: 0 0, 100% -64px; mask-position: 0 0, 100% -64px; }
}
@keyframes goalCaustics {
  from { transform: translate3d(-7px, 2px, 0) scale(1); }
  to   { transform: translate3d(9px, -3px, 0) scale(1.07); }
}
@keyframes goalBubble {
  0%   { opacity: 0;    transform: translate3d(0, 0, 0) scale(0.55); }
  14%  { opacity: 0.9; }
  72%  { opacity: 0.75; transform: translate3d(calc(var(--bubble-drift) * 0.7), -46px, 0) scale(0.95); }
  100% { opacity: 0;    transform: translate3d(var(--bubble-drift), -72px, 0) scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .goal-fill-liquid,
  .goal-fill-liquid::before,
  .goal-bubble { animation: none; }
  .goal-bubble { display: none; }
}

/* The goal pair follows the habit slide's anatomy (slide 11) rather than a
   bespoke one: mascot and body centre as a unit with the habit slide's own
   gap, so on a tall screen (tablet) the pair sits in the middle of the page
   instead of clustering under the progress bar and reading small.

   The anchored shell is kept for its scroll host — the picker's six tiles do
   overflow a short phone — but its top-pin is handed back to the self-
   balancing auto margin, and the body's peek-fade is dropped. On a screen
   where nothing scrolls that mask was greying the last tile row for no
   reason, and the habit slide has no fade either. */
.slide--goal .slide-shell.slide-shell--anchored > .slide-mascot,
.slide--goal-amount .slide-shell.slide-shell--anchored > .slide-mascot {
  margin-top: auto;
  margin-bottom: clamp(28px, 6vh, 56px);
}
.slide--goal .slide-shell.slide-shell--anchored > .slide-body,
.slide--goal-amount .slide-shell.slide-shell--anchored > .slide-body {
  -webkit-mask-image: none;
          mask-image: none;
  padding-bottom: 0;
}

/* ── Goal (slides 9 & 10) ──────────────────────────────────────────────
   The picker is a 2x3 grid rather than the stacked chip list: six goals in a
   column would push the CTA off a small phone, and a goal is picked by its
   picture as much as its name. Tiles carry the app's own 3D goal icons, so
   what is tapped here is what the goal wears in the app afterwards. */
.goal-grid {
  width: 100%;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 9px;
}
.goal-tile {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 7px;
  padding: 15px 10px;
  border-radius: 16px;
  border: 1px solid transparent;
  background: var(--input-bg);
  font-family: inherit;
  color: var(--color-base-content, #111);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
  transition: transform 0.15s ease, border-color 0.2s ease, background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
}
.goal-tile:active { transform: scale(0.97); }
.goal-tile.on {
  border-color: var(--brand-blue);
  background: var(--brand-blue);
  color: #fff;
  box-shadow: 0 10px 24px -12px color-mix(in oklab, var(--brand-blue) 60%, transparent);
}
.goal-tile-icon { display: block; line-height: 0; }
.goal-tile-label {
  font-size: 13.5px; font-weight: 700; line-height: 1.25; text-align: center;
}

/* ── Goal amount (slide 10) ──────────────────────────────────────────────
   The card is the goal as it will look in the app, and the controls below
   write into it. Spacing is set here rather than inherited from the budget
   slide: that screen is only a dial and can afford a 52px hero, this one has
   a card and two controls to fit above the CTA. */
.goal-lede { margin-bottom: 16px; }

.goal-card {
  width: 100%;
  border-radius: 22px;
  background: var(--color-base-200, #fff);
  border: 1px solid color-mix(in oklab, var(--color-base-content, #111) 10%, transparent);
  padding: 16px 18px;
}
.goal-card-top { display: flex; align-items: center; gap: 11px; }
.goal-card-icon { display: inline-flex; line-height: 0; flex-shrink: 0; }
.goal-card-icon img { width: 34px; height: 34px; }
.goal-card-name {
  flex: 1; min-width: 0;
  text-align: start;
  font-size: 17px; font-weight: 800; letter-spacing: -0.01em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.goal-card-when {
  flex-shrink: 0;
  font-size: 12px; font-weight: 800;
  color: color-mix(in oklab, var(--color-base-content, #111) 45%, transparent);
  font-variant-numeric: tabular-nums;
}
.goal-card-amount {
  display: flex; align-items: baseline; gap: 4px;
  margin-top: 14px;
  font-size: 38px; font-weight: 800; letter-spacing: -0.035em; line-height: 1;
  font-variant-numeric: tabular-nums;
}
.goal-card-cur {
  font-size: 25px;
  color: color-mix(in oklab, var(--color-base-content, #111) 40%, transparent);
}
/* The card's rail is the target dial. Same .amount-* parts the budget slide
   uses, trimmed to sit inside a card rather than span the screen. */
.goal-card-slider { width: 100%; margin-top: 14px; }
.goal-card-slider .amount-track { height: 26px; }
.goal-card-slider .amount-rail { height: 10px; }
.goal-card-slider .amount-thumb { width: 26px; height: 26px; border-width: 3px; }
.goal-card-slider .amount-glow { height: 15px; }
.goal-card-foot {
  display: flex; justify-content: space-between; margin-top: 10px;
  font-size: 11.5px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase;
  color: color-mix(in oklab, var(--color-base-content, #111) 42%, transparent);
}
:root[dir="rtl"] .goal-card-foot { letter-spacing: normal; }

.goal-field-label {
  width: 100%;
  font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: .18em;
  color: color-mix(in oklab, var(--color-base-content, #111) 36%, transparent);
  text-align: start;
  margin: 24px 0 9px;
}
:root[dir="rtl"] .goal-field-label { letter-spacing: normal; }
.goal-stamps {
  width: 100%;
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px;
}
.goal-stamp {
  border-radius: 14px; padding: 12px 2px;
  border: 1px solid transparent;
  background: var(--input-bg);
  font-family: inherit;
  color: var(--color-base-content, #111);
  text-align: center;
  cursor: pointer; -webkit-tap-highlight-color: transparent;
  transition: transform 0.15s ease, background 0.2s ease, border-color 0.2s ease;
}
.goal-stamp:active { transform: scale(0.96); }
.goal-stamp.on { background: var(--brand-blue); border-color: var(--brand-blue); color: #fff; }
.goal-stamp-big { display: block; font-size: 17px; font-weight: 900; line-height: 1; }
.goal-stamp-sub {
  display: block; margin-top: 3px;
  font-size: 8.5px; font-weight: 900; text-transform: uppercase; letter-spacing: .1em;
  color: color-mix(in oklab, var(--color-base-content, #111) 40%, transparent);
}
:root[dir="rtl"] .goal-stamp-sub { letter-spacing: normal; }
.goal-stamp.on .goal-stamp-sub { color: rgba(255,255,255,.75); }

.sub-count-label {
  flex: 1; min-width: 0;
  text-align: start;
  letter-spacing: -0.005em;
}
[data-theme="floosyo-dark"] .sub-count-icon {
  background: color-mix(in oklab, var(--brand-blue) 18%, transparent);
}
[data-theme="floosyo-dark"] .sub-count-chip.on .sub-count-icon {
  background: rgba(255, 255, 255, 0.18);
}
.slides-viewport { position: relative; width: 100%; height: 100%; z-index: 10; }
.slide {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center;
  justify-content: flex-start;
  padding: calc(var(--safe-top) + 24px) 24px calc(var(--safe-bottom) + 120px);
  opacity: 0; transform: translate3d(40px, 0, 0);
  transition: opacity 0.18s cubic-bezier(0.22, 1, 0.36, 1),
              transform 0.18s cubic-bezier(0.22, 1, 0.36, 1);
  will-change: opacity, transform;
  pointer-events: none;
  overflow: hidden;
}
.slide.active { opacity: 1; transform: translate3d(0, 0, 0); pointer-events: all; }
.slide.exit-forward { opacity: 0; transform: translate3d(-40px, 0, 0); }
.slide.exit-back { opacity: 0; transform: translate3d(40px, 0, 0); }

/* Apple-style mascot entrance — slow fade + subtle scale on slide enter.
   Sits on the wrapper so the inner .mascot-img keeps its breathe animation. */
.slide.active .slide-mascot {
  animation: mascot-enter 0.65s cubic-bezier(0.22, 1, 0.36, 1) both;
}
@keyframes mascot-enter {
  from { opacity: 0; transform: scale(0.94) translateY(6px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .slide.active .slide-mascot { animation: none; }
}

/* Apple-style staggered fade-in for option lists. Apply .onb-stagger to a
   wrapper; its direct children fade up in sequence once the slide is active. */
.slide.active .onb-stagger > * {
  animation: onb-fade-in 0.5s cubic-bezier(0.32, 0.72, 0, 1) both;
}
.slide.active .onb-stagger > *:nth-child(1) { animation-delay: 0.08s; }
.slide.active .onb-stagger > *:nth-child(2) { animation-delay: 0.16s; }
.slide.active .onb-stagger > *:nth-child(3) { animation-delay: 0.24s; }
.slide.active .onb-stagger > *:nth-child(4) { animation-delay: 0.32s; }
.slide.active .onb-stagger > *:nth-child(5) { animation-delay: 0.40s; }
.slide.active .onb-stagger > *:nth-child(6) { animation-delay: 0.48s; }
@media (prefers-reduced-motion: reduce) {
  .slide.active .onb-stagger > * { animation: none; }
}

/* C · Pause infinite animations inside non-active slides — no GPU work for
   off-screen mascot rings, halos, breathe, pulses across the 25 mounted slides. */
.slide:not(.active) *,
.slide:not(.active) {
  animation-play-state: paused !important;
}

.slide-inner {
  display: flex; flex-direction: column; align-items: center;
  text-align: center; max-width: 380px; width: 100%; padding-top: 8px;
}

.brand-floos { color: var(--brand-dark); font-weight: 800; }
.brand-yo { color: var(--brand-blue); font-weight: 800; }

/* Mascot — 10% larger on slides 0-3 (209x209), original on final slide */
.mascot-container {
  width: 209px; height: 209px; margin-bottom: 28px;
  position: relative; display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.mascot-img {
  width: 160px; height: 160px; object-fit: contain;
  position: relative; z-index: 2;
  filter: drop-shadow(0 12px 28px rgba(61,138,254,0.1));
  animation: mascot-breathe 4.5s cubic-bezier(0.32, 0.72, 0, 1) infinite;
}
@keyframes mascot-breathe {
  0%, 100% { transform: scale(1) translateY(0); }
  50% { transform: scale(1.02) translateY(-3px); }
}

.slide-title {
  font-size: 26px; font-weight: 800; line-height: 1.15;
  margin-bottom: 10px; letter-spacing: -0.02em;
  color: var(--text-primary);
}
.slide-title .hl { color: var(--brand-blue); }

.slide-desc {
  font-size: 14px; line-height: 1.55; color: var(--text-secondary);
  max-width: 300px; margin-bottom: 20px;
}

.onboard-form {
  width: 100%; max-width: 340px;
  display: flex; flex-direction: column; gap: 14px;
  text-align: start;
}
.form-group { display: flex; flex-direction: column; gap: 6px; }
.form-label { font-size: 13px; font-weight: 600; color: var(--text-primary); letter-spacing: 0.01em; }
.form-input, .form-select {
  min-height: 48px;
  border: 1.5px solid var(--input-border);
  border-radius: 14px; padding: 12px 16px 14px;
  line-height: 1.5;
  font-family: "Almarai", "Nunito Sans", ui-sans-serif, system-ui, sans-serif;
  font-size: 15px; color: var(--text-primary);
  background: var(--input-bg);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  transition: border-color 0.2s, box-shadow 0.2s;
  outline: none; width: 100%; -webkit-appearance: none;
  text-align: start;
}
.form-input:focus, .form-select:focus {
  border-color: var(--brand-blue);
  box-shadow: 0 0 0 3px rgba(61,138,254,0.1);
}
.form-input::placeholder { color: var(--input-placeholder); }

.form-row { display: flex; gap: 10px; }
.form-row .form-group { flex: 1; }

.amount-input-wrap { position: relative; }
.amount-input-wrap .currency-badge {
  position: absolute; top: 50%; transform: translateY(-50%);
  inset-inline-start: 14px; z-index: 2;
  font-size: 13px; font-weight: 700; color: var(--brand-blue);
  background: var(--btn-back-bg); padding: 3px 8px; border-radius: 6px;
  pointer-events: none;
}
.amount-input-wrap .form-input {
  padding-inline-start: 60px;
  font-size: 18px; font-weight: 700;
}

.bottom-nav {
  position: fixed; bottom: 0; left: 0; right: 0;
  padding: 16px 24px calc(var(--safe-bottom) + 14px);
  z-index: 100;
  background: linear-gradient(to top,
    var(--color-base-100, #f0f4ff) 55%,
    color-mix(in srgb, var(--color-base-100, #f0f4ff) 80%, transparent) 85%,
    transparent);
  backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
}
.nav-row { display: flex; gap: 12px; max-width: 380px; margin: 0 auto; }

.onboarding .btn {
  flex: 1; height: 54px; border: none; border-radius: 999px;
  font-family: inherit; font-size: 15px; font-weight: 600;
  cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px;
  /* Explicit property list, NOT `all`. The button is flex:1 with width:100%, so
     `all` animated its resolved width too: on first paint — and again when the
     web font swaps and the row re-measures — the CTA visibly grew from narrow
     to full width. Only the properties the :hover/:active/.is-locked rules
     actually change belong here. */
  transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1),
              box-shadow 0.3s cubic-bezier(0.32, 0.72, 0, 1),
              background-color 0.3s cubic-bezier(0.32, 0.72, 0, 1),
              color 0.3s cubic-bezier(0.32, 0.72, 0, 1),
              opacity 0.3s cubic-bezier(0.32, 0.72, 0, 1);
  -webkit-tap-highlight-color: transparent;
}
.onboarding .btn:active { transform: scale(0.96); }
/* The rule above is display:flex, which outranks the UA [hidden] rule, so the
   steps that draw their own footer (auth, paywall) could not hide the shared
   CTA without this. */
.onboarding .btn[hidden] { display: none; }
.btn-back {
  background: var(--btn-back-bg); color: var(--brand-blue);
  flex: 0 0 54px;
}
.btn-next {
  background: var(--brand-blue); color: white;
  box-shadow: 0 4px 20px rgba(61,138,254,0.3),
              inset 0 1px 0 rgba(255,255,255,0.15);
}
@media (hover: hover) {
  .btn-next:hover {
    box-shadow: 0 8px 28px rgba(61,138,254,0.4),
                inset 0 1px 0 rgba(255,255,255,0.18);
    transform: translateY(-1px);
  }
}
.btn-next:active {
  box-shadow: 0 2px 10px rgba(61,138,254,0.2),
              inset 0 1px 0 rgba(255,255,255,0.15);
  transform: translateY(0) scale(0.98);
}
.onboarding .btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(61,138,254,0.35),
              0 4px 20px rgba(61,138,254,0.3);
}
.btn-dashboard {
  background: linear-gradient(135deg, var(--brand-blue), var(--brand-blue-light));
  color: white; box-shadow: 0 4px 24px rgba(61,138,254,0.3);
}
.btn-icon { font-size: 18px; display: inline-flex; align-items: center; justify-content: center; line-height: 1; }
.btn-icon svg { display: block; }

.ready-features { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; margin-top: 20px; }
.ready-feature {
  padding: 8px 16px; border-radius: 100px;
  background: var(--feature-bg);
  border: 1px solid var(--feature-border);
  font-size: 13px; font-weight: 500; color: var(--brand-blue);
  letter-spacing: 0.01em;
}

@keyframes shake {
  0%, 100% { transform: translateX(0); }
  20%, 60% { transform: translateX(-6px); }
  40%, 80% { transform: translateX(6px); }
}
.shake { animation: shake 0.4s ease; }

/* Scrollable slide (budget step) */
.slide--scrollable.active {
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* IE/Edge */
}
.slide--scrollable.active::-webkit-scrollbar { display: none; } /* Chrome/Safari */
.slide--scrollable { padding-bottom: calc(var(--safe-bottom) + 220px) !important; }
.slide[data-slide="20"] .sub-count-chips {
  margin-top: 10px;
}
/* Slide 15 (Budget): trim excess bottom padding — manual form is gone. */
.slide[data-slide="17"].slide--scrollable { padding-bottom: calc(var(--safe-bottom) + 140px) !important; }
/* Climax slide inherits mirror layout (hero / sub / closer). */
.slide--climax .statement-desc { text-align: center; }
.slide--bridge .statement-desc { text-align: center; }

/* Two lines, not three. The first line carries the name, so at the shared 30px
   it wrapped and orphaned "didn't" onto a line of its own. Sized so the longest
   English line fits whole; balance keeps a long name splitting evenly rather
   than leaving a single word stranded. */
.slide--climax .statement-title {
  font-size: 25px;
  letter-spacing: -0.025em;
  text-wrap: balance;
}
.slide--climax .mirror-closer {
  text-align: start;
  align-self: stretch;
  max-width: 360px;
  margin-block-start: 22px;
  font-size: 15px;
  font-weight: 400;
  line-height: 1.55;
  color: color-mix(in oklab, var(--color-base-content, #111) 70%, transparent);
}
:root[dir="rtl"] .slide--climax .mirror-closer { line-height: 1.8; }
.slide--climax .mirror-closer .hl {
  color: color-mix(in oklab, var(--color-base-content, #111) 70%, transparent);
  font-weight: 400;
}

/* Tier cards */
.tiers { display: flex; flex-direction: column; gap: 8px; width: 100%; max-width: 380px; }

.tier {
  border: 1px solid var(--input-border);
  border-radius: 16px; padding: 6px 16px; cursor: pointer;
  /* Explicit properties rather than `all`: transitioning `all` also animates
     layout values the card changes on reprice, which is what made the switch
     feel loose. Transform carries a light overshoot, colour does not. */
  transition: border-color 0.2s ease,
              background 0.2s ease,
              box-shadow 0.24s cubic-bezier(0.22, 1, 0.36, 1),
              transform 0.34s cubic-bezier(0.34, 1.42, 0.5, 1);
  position: relative; overflow: hidden;
  background: var(--input-bg);
}
.tier::before {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(135deg, rgba(61,138,254,0.04) 0%, transparent 60%);
  opacity: 0; transition: opacity 0.35s; pointer-events: none;
}
.tier:hover { border-color: var(--brand-blue-light); }
.tier:hover::before { opacity: 1; }
.tier.selected {
  border-color: var(--brand-blue);
  background: rgba(61,138,254,0.04);
  box-shadow: 0 6px 20px -10px rgba(61,138,254,0.3);
}
.tier.selected::before { opacity: 1; }

[data-theme="floosyo-dark"] .tier.selected { background: rgba(61,138,254,0.08); }

.tier-top { display: flex; justify-content: space-between; align-items: center; }
.tier-left { display: flex; align-items: center; gap: 12px; }

.tier-icon {
  width: 44px; height: 44px;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.tier-icon img {
  width: 44px; height: 44px; object-fit: contain;
}
.tier-icon--lg { width: 52px; height: 52px; }
.tier-icon--lg img { width: 52px; height: 52px; }
/* Budget tiers — bigger mascot boxes (scoped, doesn't affect reminder cadence cards). */
.tier[data-tier-key] .tier-icon { width: 68px; height: 68px; }
.tier[data-tier-key] .tier-icon img { width: 68px; height: 68px; }

.tier-label { text-align: start; min-width: 0; }
.tier-label h3 { font-size: 16px; font-weight: 700; color: var(--text-primary); }
.tier-label p { font-size: 12px; color: var(--text-secondary); margin-top: 1px; }

/* Budgeting framework cards (slide 14) — clean white cards on the app palette.
   Reuses the .tier shell; adds a split bar + named-slot legend. */
.tiers.frameworks { gap: 12px; }
.tier.framework { padding: 10px 16px; }
.tier.framework .tier-top { align-items: flex-start; }
/* Name is the title; the ratio lives in the legend %s below (shown once). */
.tier.framework .tier-label h3 { font-size: 17px; letter-spacing: -0.01em; }
.tier.framework .tier-label p {
  font-size: 11.5px; margin-top: 2px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 250px;
}
.tier.framework .fw-tick {
  margin-inline-start: auto; width: 24px; height: 24px; border-radius: 50%;
  border: 2px solid var(--input-border); display: grid; place-items: center;
  color: transparent; font-weight: 900; font-size: 12px; flex-shrink: 0;
}
.tier.framework.selected .fw-tick { background: var(--brand-blue); border-color: var(--brand-blue); color: #fff; }

.fw-split { display: flex; gap: 4px; height: 7px; margin-top: 13px; }
.fw-split .fw-seg { border-radius: 4px; min-width: 6px; }
.fw-names { display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 11px; }
.fw-name { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; color: var(--text-primary); white-space: nowrap; }
.fw-name::before { content: ""; width: 8px; height: 8px; border-radius: 2px; display: inline-block; }
.fw-name small { color: var(--brand-blue); font-weight: 700; margin-inline-start: 2px; }
.fw-seg-0 { background: #3b82f6; } .fw-seg-1 { background: #f59e0b; } .fw-seg-2 { background: #22c55e; }
.fw-name-0::before { background: #3b82f6; } .fw-name-1::before { background: #f59e0b; } .fw-name-2::before { background: #22c55e; }

/* Enhanced active state — more lift, a soft brand glow and a subtle ring so the
   chosen method reads clearly above the rest. */
.tier.framework.selected {
  border-color: var(--brand-blue);
  background: linear-gradient(180deg, rgba(61,138,254,0.10), rgba(61,138,254,0.03));
  box-shadow: 0 14px 30px -14px rgba(61,138,254,0.55), 0 0 0 3px rgba(61,138,254,0.14);
  transform: translateY(-3px);
}
/* Pressed state reads before the selection lands, so the tap has an answer even
   on the card that is already chosen. */
.tier.framework:active { transform: scale(0.985); transition-duration: 0.09s; }
.tier.framework.selected .tier-label h3 { color: var(--brand-blue); }
.tier.framework.selected .fw-name { color: var(--text-primary); font-weight: 600; }
.tier.framework.selected .fw-name small { color: var(--brand-blue); opacity: 0.85; }

.tier-amount {
  font-size: 24px; font-weight: 800; color: var(--brand-blue);
  display: flex; align-items: baseline; gap: 4px; white-space: nowrap;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}
.tier-amount small { font-size: 24px; color: var(--text-secondary); font-weight: 500; }

.popular-badge {
  position: absolute; top: 0;
  inset-inline-end: 0;
  background: linear-gradient(135deg, #22c55e, #16a34a);
  color: #fff; font-size: 10px; font-weight: 700;
  padding: 4px 14px;
  border-start-start-radius: 0;
  border-start-end-radius: 0;
  border-end-start-radius: 10px;
  border-end-end-radius: 0;
}

/* Breakdown expand/collapse */
.breakdown {
  max-height: 0; overflow: hidden; opacity: 0;
  transition: max-height 0.5s cubic-bezier(0.4, 0, 0.2, 1),
              margin-top 0.5s cubic-bezier(0.4, 0, 0.2, 1),
              opacity 0.4s ease;
  margin-top: 0;
}
.tier.selected .breakdown { max-height: 600px; margin-top: 14px; opacity: 1; }

.breakdown-divider {
  height: 1px; margin-bottom: 10px;
  background: linear-gradient(to left, transparent, var(--input-border), transparent);
}
.breakdown-title {
  font-size: 12px; color: var(--text-secondary);
  text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; font-weight: 600;
}

.cat-row { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; }
.cat-left { display: flex; align-items: center; gap: 10px; }
.cat-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.cat-name { font-size: 14px; color: var(--text-secondary); }
.cat-right { display: flex; align-items: center; gap: 10px; }
.cat-amount { font-size: 14px; font-weight: 700; min-width: 45px; text-align: center; color: var(--text-primary); }

.adjust-btn {
  width: 28px; height: 28px; border-radius: 50%;
  border: 1px solid var(--input-border); background: transparent;
  color: var(--text-secondary); font-size: 16px; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: all 0.2s; -webkit-tap-highlight-color: transparent;
}
.adjust-btn:hover { border-color: var(--brand-blue); color: var(--brand-blue); }
.adjust-btn:active { transform: scale(0.92); }

.breakdown-total {
  display: flex; justify-content: space-between; align-items: center;
  margin-top: 12px; padding-top: 12px;
  border-top: 1px solid var(--input-border);
}
.bt-label { font-size: 13px; color: var(--text-secondary); }
.bt-val { font-size: 18px; font-weight: 800; color: var(--brand-blue); display: flex; align-items: baseline; gap: 4px; }
.bt-val small { font-size: 12px; font-weight: 600; }

.tweak-hint { text-align: center; font-size: 11px; color: var(--text-secondary); margin-top: 12px; opacity: 0.6; }

/* Manual entry toggle & form */
.manual-toggle {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  margin-top: 20px; width: 100%; max-width: 380px;
  font-size: 13px; color: var(--text-secondary); cursor: pointer;
  background: none; border: none; font-family: inherit;
  transition: color 0.2s; -webkit-tap-highlight-color: transparent;
}
.manual-toggle:hover { color: var(--brand-blue-light); }
.manual-arrow { transition: transform 0.3s; font-size: 10px; }
.manual-toggle.open .manual-arrow { transform: rotate(180deg); }

.manual-form {
  max-height: 0; overflow: hidden; opacity: 0;
  transition: max-height 0.4s ease, opacity 0.3s ease, margin-top 0.4s ease;
  margin-top: 0; width: 100%; max-width: 380px;
}
.manual-form.open { max-height: 200px; opacity: 1; margin-top: 16px; }

/* Accessible focus rings for inputs (keyboard nav polish) */
.form-input:focus-visible, .form-select:focus-visible {
  border-color: var(--brand-blue);
  box-shadow: 0 0 0 3px rgba(61,138,254,0.18);
}

/* Respect reduced-motion preference — disable all continuous/decorative motion */
@media (prefers-reduced-motion: reduce) {
  .mascot-img,
  .reminder-enable-btn,
  .reminder-enable-btn::after,
  .reminder-enable-icon,
  .wl-ticker,
  .wl-orb {
    animation: none !important;
  }
  .slide,
  .slide-title,
  .slide-desc,
  .ready-feature,
  .tier {
    transition: none !important;
  }
}

/* ── Reminder style slide ── */
.reminder-enable-wrap {
  display: flex; flex-direction: column; align-items: center;
  gap: 8px; margin-top: 20px; width: 100%; max-width: 360px;
  animation: fadeInUp 0.4s ease both;
}
.reminder-enable-btn {
  position: relative; overflow: hidden;
  width: 100%;
  display: inline-flex; align-items: center; justify-content: center;
  gap: 12px;
  padding: 20px 30px;
  border-radius: 9999px;
  /* Verbatim stops from .landing-grad-text (application.css) */
  background: linear-gradient(135deg, #3B7DD8 0%, #5DA0F7 50%, #22C55E 100%);
  color: white;
  font-size: 16px; font-weight: 800;
  letter-spacing: 0.01em;
  border: none; cursor: pointer;
  box-shadow: 0 0 0 0 rgba(93, 160, 247, 0.50);
  transition: transform 0.2s cubic-bezier(0.32, 0.72, 0, 1);
  animation: enable-pulse 3.4s cubic-bezier(0.32, 0.72, 0, 1) infinite;
}
.reminder-enable-btn::after {
  content: ""; position: absolute; inset: -2px;
  border-radius: inherit;
  background: linear-gradient(120deg, transparent 30%, rgba(255,255,255,0.32) 50%, transparent 70%);
  transform: translateX(-100%);
  animation: enable-sheen 3.4s cubic-bezier(0.32, 0.72, 0, 1) infinite;
  pointer-events: none;
}
.reminder-enable-btn:active { transform: scale(0.97); animation-play-state: paused; }
.reminder-enable-btn:active .reminder-enable-icon { animation-play-state: paused; }
.reminder-enable-btn[disabled] { opacity: 0.7; cursor: wait; animation: none; }
.reminder-enable-btn[disabled]::after { animation: none; }
.reminder-enable-btn[disabled] .reminder-enable-icon { animation: none; }
@keyframes enable-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(93, 160, 247, 0.50); }
  70%      { box-shadow: 0 0 0 8px rgba(93, 160, 247, 0); }
}
@keyframes enable-sheen {
  0%        { transform: translateX(-100%); }
  65%, 100% { transform: translateX(100%); }
}
.reminder-enable-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  transform-origin: 50% 10%;
  /* Same duration/easing as enable-pulse so the bell shake lands on the ring crest */
  animation: enable-bell-ring 3.4s cubic-bezier(0.32, 0.72, 0, 1) infinite;
}
.reminder-enable-icon svg { display: block; }
@keyframes enable-bell-ring {
  0%, 60%, 100% { transform: rotate(0); }
  65% { transform: rotate(14deg); }
  72% { transform: rotate(-12deg); }
  80% { transform: rotate(8deg); }
  88% { transform: rotate(-4deg); }
  94% { transform: rotate(2deg); }
}
.reminder-enable-hint {
  font-size: 12px; color: var(--text-secondary); text-align: center;
  max-width: 320px; line-height: 1.5;
}

.reminder-denied {
  margin-top: 20px; text-align: center;
  padding: 20px;
  border-radius: 16px;
  background: rgba(245, 158, 11, 0.06);
  border: 1px solid rgba(245, 158, 11, 0.18);
  max-width: 360px; width: 100%;
  animation: fadeInUp 0.4s cubic-bezier(0.32, 0.72, 0, 1) both;
}
.reminder-denied-icon { font-size: 28px; margin-bottom: 6px; }
.reminder-denied-title { font-size: 14px; font-weight: 700; color: var(--text-primary); }
.reminder-denied-desc { font-size: 12px; color: var(--text-secondary); margin-top: 4px; line-height: 1.55; }

/* ── Premium "enabled" success state ────────────────────────────────── */
.reminder-enabled {
  --rem-emerald: #10B981;
  --rem-emerald-deep: #059669;
  margin-top: 24px;
  width: 100%;
  max-width: 360px;
  padding: 26px 22px 22px;
  border-radius: 22px;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  background:
    radial-gradient(120% 100% at 50% 0%, rgba(16, 185, 129, 0.10) 0%, rgba(16, 185, 129, 0) 65%),
    linear-gradient(180deg, rgba(255,255,255,0.86) 0%, rgba(255,255,255,0.66) 100%);
  border: 1px solid rgba(16, 185, 129, 0.18);
  box-shadow:
    0 24px 48px -28px rgba(16, 185, 129, 0.55),
    0 8px 22px -14px rgba(15, 23, 42, 0.12),
    inset 0 1px 0 rgba(255, 255, 255, 0.7);
  backdrop-filter: blur(12px) saturate(140%);
  -webkit-backdrop-filter: blur(12px) saturate(140%);
  animation: reminderEnabledIn 0.55s cubic-bezier(0.16, 1, 0.3, 1) both;
}
[data-theme="floosyo-dark"] .reminder-enabled {
  background:
    radial-gradient(120% 100% at 50% 0%, rgba(16, 185, 129, 0.14) 0%, rgba(16, 185, 129, 0) 65%),
    linear-gradient(180deg, rgba(20, 27, 38, 0.78) 0%, rgba(15, 21, 30, 0.7) 100%);
  border-color: rgba(16, 185, 129, 0.24);
  box-shadow:
    0 24px 48px -28px rgba(16, 185, 129, 0.4),
    0 8px 22px -14px rgba(0, 0, 0, 0.55),
    inset 0 1px 0 rgba(255, 255, 255, 0.05);
}

.reminder-enabled-badge {
  position: relative;
  width: 72px;
  height: 72px;
  margin-bottom: 14px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.reminder-enabled-glow {
  position: absolute; inset: -14px;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(16, 185, 129, 0.45), rgba(16, 185, 129, 0));
  filter: blur(6px);
  animation: reminderEnabledGlow 2.4s ease-in-out infinite;
}
.reminder-enabled-ring {
  position: absolute; inset: 0;
  border-radius: 50%;
  border: 1.5px solid rgba(16, 185, 129, 0.35);
  animation: reminderEnabledRing 2.4s cubic-bezier(0.32, 0.72, 0, 1) infinite;
}
.reminder-enabled-check {
  position: relative;
  width: 56px; height: 56px;
  filter: drop-shadow(0 6px 14px rgba(16, 185, 129, 0.45));
}
.reminder-enabled-check-circle {
  fill: url(#__noop__);
  fill: var(--rem-emerald);
  stroke: rgba(255, 255, 255, 0.85);
  stroke-width: 0;
  /* Gradient via inline style fallback: solid emerald with subtle inner shine via stroke. */
}
.reminder-enabled-check-circle {
  fill: var(--rem-emerald);
  filter: brightness(1.05);
}
.reminder-enabled-check-path {
  stroke: #FFFFFF;
  stroke-width: 4.5;
  stroke-dasharray: 40;
  stroke-dashoffset: 40;
  animation: reminderEnabledDraw 0.55s 0.18s cubic-bezier(0.65, 0, 0.35, 1) forwards;
}

.reminder-enabled-title {
  font-size: 18px;
  font-weight: 800;
  letter-spacing: -0.01em;
  color: var(--text-primary);
  background: linear-gradient(135deg, var(--rem-emerald-deep) 0%, var(--brand-blue) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  margin-bottom: 0;
}

@keyframes reminderEnabledIn {
  0%   { opacity: 0; transform: translateY(10px) scale(0.96); }
  100% { opacity: 1; transform: translateY(0)    scale(1); }
}
@keyframes reminderEnabledDraw {
  0%   { stroke-dashoffset: 40; }
  100% { stroke-dashoffset: 0; }
}
@keyframes reminderEnabledGlow {
  0%, 100% { opacity: 0.45; transform: scale(1); }
  50%      { opacity: 0.9;  transform: scale(1.08); }
}
@keyframes reminderEnabledRing {
  0%   { transform: scale(1);   opacity: 0.7; }
  70%  { transform: scale(1.45); opacity: 0; }
  100% { transform: scale(1.45); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .reminder-enabled,
  .reminder-enabled-glow,
  .reminder-enabled-ring,
  .reminder-enabled-check-path { animation: none !important; }
  .reminder-enabled-check-path { stroke-dashoffset: 0; }
}

/* Reminder slide — tight bottom padding (no over-scroll into empty space) */
.slide[data-slide="19"].slide--scrollable {
  padding-bottom: calc(var(--safe-bottom) + 120px) !important;
}
/* Forecast slide — V9 owns its own layout (fixed header + scroll region). */
.slide[data-slide="22"].slide--scrollable {
  padding: calc(var(--safe-top) + 80px) 0 calc(var(--safe-bottom) + 110px) !important;
}
.slide[data-slide="22"].slide--scrollable.active { overflow: hidden !important; }
.slide[data-slide="22"] .slide-inner { flex: 1 1 auto; min-height: 0; width: 100%; }


@keyframes fadeInUp {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Slide 0 · welcome hook tease — curiosity line under the main desc */
.welcome-tease {
  font-size: 12px !important;
  font-style: italic;
  color: #16a34a;
  margin-top: -6px !important;
  max-width: 300px;
}
[data-theme="floosyo-dark"] .welcome-tease { color: #4ade80; }

/* Slide 10 · Meet Save · micro-win · morphing card + counter + confetti */
.save-stage { position: relative; }
.save-desc { margin-bottom: 18px; }
/* Save slide is tall (card + scoreboard + streak chip + hint) — lift the
   mascot so the streak chip isn't clipped on shorter devices. */
.slide[data-slide="15"] { padding-top: calc(var(--safe-top) + 40px); }

/* Slide 5 (sub_bombshell) — mascot pinned to top via slide-shell--top, then
   receipt card and citation flow below. Match the bottom margin to the top
   margin (40px) so the mascot sits with symmetric breathing room. */
.slide--bombshell .slide-mascot {
  margin-bottom: 40px;
}
.slide--bombshell .bombshell-hero {
  margin-top: 0;
  margin-bottom: 0;
}
.slide--bombshell .bombshell-citation {
  margin-top: 28px;
}

.save-card {
  width: 100%;
  text-align: start;
  transition: transform .3s cubic-bezier(.32,.72,0,1), border-color .3s ease, box-shadow .3s ease;
}
.save-stage.is-saved .save-card {
  border-color: rgba(34,197,94,.45);
  box-shadow: 0 0 0 1px rgba(34,197,94,.25) inset;
}

/* Peek row · ~2px sliver visible at bottom edge, unclickable. Mimics a
   second expense row peeking under the demo to hint at the list. */
.save-peek {
  max-height: 2px;
  overflow: hidden;
  pointer-events: none;
  opacity: .9;
}

.save-actions { margin-top: 10px; width: 100%; margin-inline: auto; justify-content: center; }

.save-badge-new {
  align-self: flex-start;
  display: inline-flex; align-items: center;
  padding: 4px 10px; border-radius: 999px;
  background: rgba(61,138,254,.12);
  color: var(--brand-blue);
  font-size: 10px; font-weight: 900;
  letter-spacing: .06em; text-transform: uppercase;
}

.save-row { display: flex; align-items: center; gap: 12px; }
.save-row .cat-pill {
  width: 44px; height: 44px; border-radius: 12px;
  background: rgba(245,158,11,.16);
  display: flex; align-items: center; justify-content: center;
  font-size: 22px; flex-shrink: 0;
  transition: transform .35s cubic-bezier(.32,.72,0,1), background .3s ease;
}
.save-stage.is-saved .save-row .cat-pill {
  background: rgba(34,197,94,.18); transform: rotate(-6deg) scale(1.04);
}
.save-row .info-col { flex: 1; min-width: 0; }
.save-row .info-col .n {
  font-size: 15px; font-weight: 900;
  color: var(--text-primary);
  letter-spacing: -0.01em;
}

/* Stacked planned/saved lines — planned strikes + fades, saved slides up */
.save-lines { position: relative; margin-top: 4px; min-height: 18px; }
.save-lines .sub {
  font-size: 12px; color: var(--text-secondary);
  font-weight: 700; display: flex; align-items: baseline; gap: 6px;
  transition: opacity .35s ease, transform .4s cubic-bezier(.32,.72,0,1), color .3s ease;
}
.planned-line .amt {
  color: var(--text-primary); font-weight: 900;
  font-variant-numeric: tabular-nums;
}
.saved-line {
  position: absolute; inset-inline: 0; top: 0;
  opacity: 0; transform: translateY(10px);
  color: #16a34a;
}
.save-lines .saved-line { align-items: center; }
.saved-line .saved-ic {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; border-radius: 50%;
  background: #16a34a; color: #fff; font-size: 10px; font-weight: 900;
}
.saved-line .saved-amt {
  font-weight: 900; font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em; font-size: 13px; color: #16a34a;
}
.saved-line .saved-tail { color: var(--text-secondary); font-weight: 600; }

.save-stage.is-saved .planned-line {
  opacity: 0; transform: translateY(-6px); text-decoration: line-through;
}
.save-stage.is-saved .saved-line {
  opacity: 1; transform: translateY(0);
}

/* Skip + Stop pair — mirrors the real Due Today row's action buttons.
   Skip (green, primary) pulses before tap to invite the first try.
   Stop (amber, outlined) is the same teaching affordance users see
   in-app. Both trigger the save demo so tapping either one works. */
.save-actions {
  align-self: stretch;
  display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
}
.save-btn-skip, .save-btn-stop {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  height: 40px; padding: 0 12px;
  border-radius: 12px;
  font: inherit; font-size: 13px; font-weight: 900;
  letter-spacing: -.01em; line-height: 1;
  cursor: pointer;
  transition: transform .15s cubic-bezier(.32,.72,0,1), box-shadow .25s ease, background .25s ease, color .25s ease;
  -webkit-tap-highlight-color: transparent;
}
.save-btn-skip svg, .save-btn-stop svg { display: block; flex-shrink: 0; }
.save-btn-skip .btn-label, .save-btn-stop .btn-label { display: inline-flex; align-items: center; line-height: 1; }
.save-btn-skip {
  background: #16a34a;
  color: #fff;
  border: 0;
}
.save-btn-skip:active { background: #15803d; }
.save-btn-stop {
  background: transparent;
  color: #3D8AFE;
  border: 1.5px solid rgba(61,138,254,.32);
}
.save-btn-skip:active, .save-btn-stop:active { transform: scale(.96); animation: none; }
.save-btn-stop:active { background: rgba(61,138,254,.08); }
.save-stage.is-saved .save-btn-skip {
  animation: none;
  background: rgba(34,197,94,.15);
  color: #16a34a;
  box-shadow: none;
  cursor: default;
}
.save-stage.is-saved .save-btn-stop {
  background: rgba(61,138,254,.08);
  color: #2A6FE6;
  border-color: rgba(61,138,254,.2);
  cursor: default;
}
[data-theme="floosyo-dark"] .save-stage.is-saved .save-btn-skip {
  background: rgba(34,197,94,.22); color: #4ade80;
}
[data-theme="floosyo-dark"] .save-btn-stop {
  color: #60A5FA; border-color: rgba(96,165,250,.32);
}
[data-theme="floosyo-dark"] .save-stage.is-saved .save-btn-stop {
  background: rgba(96,165,250,.12); color: #60A5FA; border-color: rgba(96,165,250,.22);
}
@keyframes save-invite-pulse {
  0%, 100% { box-shadow: 0 14px 26px -10px rgba(34,197,94,.7), 0 0 0 0 rgba(34,197,94,.55); }
  70%      { box-shadow: 0 14px 26px -10px rgba(34,197,94,.7), 0 0 0 14px rgba(34,197,94,0); }
}

/* Per-button tapped animations · Skip pops, Stop thuds. */
.save-btn-skip.is-tapped { animation: save-skip-pop .55s cubic-bezier(.32,.72,0,1); }
@keyframes save-skip-pop {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.09); }
  70%  { transform: scale(.97); }
  100% { transform: scale(1); }
}
.save-btn-stop.is-tapped {
  background: #3D8AFE;
  color: #fff;
  border-color: transparent;
  animation: save-skip-pop .55s cubic-bezier(.32,.72,0,1);
}

/* Stopped state · blue theme on card, hides planned line, faded skip. */
.save-stage.is-stopped .save-card {
  border-color: rgba(61,138,254,.45);
  box-shadow: 0 14px 32px -14px rgba(61,138,254,.4);
}
.save-stage.is-stopped .planned-line {
  opacity: 0; transform: translateY(-6px); text-decoration: line-through;
}
.save-stage.is-stopped .saved-line {
  opacity: 1; transform: translateY(0);
}
.save-stage.is-stopped .saved-line .saved-amt { color: #2A6FE6; }
.save-stage.is-stopped .saved-line .saved-ic {
  background: #2A6FE6;
}
.save-stage.is-stopped .save-row .cat-pill {
  background: rgba(61,138,254,.1);
}
.save-stage.is-stopped .save-btn-skip {
  animation: none;
  opacity: .45;
  background: rgba(34,197,94,.1);
  color: #16a34a;
  box-shadow: none;
  cursor: default;
}
.save-stage.is-stopped .save-btn-stop {
  cursor: default;
}
[data-theme="floosyo-dark"] .save-stage.is-stopped .save-btn-skip {
  background: rgba(34,197,94,.12); color: #4ade80;
}
[data-theme="floosyo-dark"] .save-stage.is-stopped .saved-line .saved-amt {
  color: #60A5FA;
}
[data-theme="floosyo-dark"] .save-stage.is-stopped .saved-line .saved-ic {
  background: #60A5FA;
}

/* Stopped scoreboard · blue gradient + suffix chip */
.save-stage.is-stopped .save-scoreboard {
  background: linear-gradient(135deg, rgba(61,138,254,.2), rgba(61,138,254,.06));
  border-color: rgba(61,138,254,.4);
  color: #2A6FE6;
  transform: scale(1.04);
  box-shadow: 0 10px 24px -10px rgba(61,138,254,.45);
}
.save-stage.is-stopped .save-scoreboard .ss-amt,
.save-stage.is-stopped .save-scoreboard .ss-cur,
.save-stage.is-stopped .save-scoreboard .ss-suffix { color: #2A6FE6; }
[data-theme="floosyo-dark"] .save-stage.is-stopped .save-scoreboard,
[data-theme="floosyo-dark"] .save-stage.is-stopped .save-scoreboard .ss-amt,
[data-theme="floosyo-dark"] .save-stage.is-stopped .save-scoreboard .ss-cur,
[data-theme="floosyo-dark"] .save-stage.is-stopped .save-scoreboard .ss-suffix {
  color: #60A5FA;
}
.save-scoreboard .ss-suffix {
  font-size: 11px; font-weight: 800; letter-spacing: .04em;
  color: var(--text-secondary); text-transform: uppercase;
  margin-inline-start: -2px;
}

/* Scoreboard chip — counter ticks up on tap */
.save-scoreboard {
  margin-top: 14px;
  display: inline-flex; align-items: center; gap: 8px;
  padding: 10px 16px; border-radius: 999px;
  background: color-mix(in oklab, var(--text-primary) 5%, transparent);
  border: 1px solid color-mix(in oklab, var(--text-primary) 10%, transparent);
  font-size: 13px; font-weight: 700; color: var(--text-secondary);
  transition: background .3s ease, border-color .3s ease, transform .3s cubic-bezier(.32,.72,0,1), box-shadow .3s ease;
}
.save-stage.is-saved .save-scoreboard {
  background: linear-gradient(135deg, rgba(34,197,94,.2), rgba(34,197,94,.08));
  border-color: rgba(34,197,94,.4);
  color: #16a34a;
  transform: scale(1.04);
  box-shadow: 0 10px 24px -10px rgba(34,197,94,.45);
}
.save-scoreboard .ss-ic { font-size: 16px; }
.save-scoreboard .ss-amt {
  color: var(--text-primary); font-weight: 900;
  font-variant-numeric: tabular-nums; font-size: 15px;
  letter-spacing: -0.01em;
}
.save-stage.is-saved .save-scoreboard .ss-amt { color: #16a34a; }
.save-scoreboard .ss-cur {
  font-size: 10px; font-weight: 800; letter-spacing: .05em;
  color: var(--text-secondary); text-transform: uppercase;
}
.save-stage.is-saved .save-scoreboard .ss-cur { color: #16a34a; }

.save-hint {
  margin-top: 10px; font-size: 12px; color: var(--text-secondary);
  text-align: center;
  opacity: 0; transform: translateY(6px) scale(0.98);
  transition: opacity .35s ease .9s, transform .35s cubic-bezier(0.34, 1.56, 0.64, 1) .9s;
}
.save-stage.is-saved .save-hint,
.save-stage.is-stopped .save-hint { opacity: 1; transform: translateY(0) scale(1); }

/* Streak chip — appears ~900ms after the saved card lands. */
.save-streak-chip {
  margin-top: 10px;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 12px; border-radius: 999px;
  background: linear-gradient(135deg, #fff4da 0%, #ffe0a8 100%);
  border: 1px solid rgba(217, 119, 6, 0.35);
  color: #92530c;
  font-size: 12px; font-weight: 700; letter-spacing: .02em;
  opacity: 0; transform: translateY(6px) scale(0.95);
  transition: opacity .35s ease .9s, transform .35s cubic-bezier(0.34, 1.56, 0.64, 1) .9s;
  align-self: center;
}
.save-streak-chip.is-revealed { opacity: 1; transform: translateY(0) scale(1); }
.save-streak-chip .ssc-ic { font-size: 14px; }
[dir="rtl"] .save-streak-chip { letter-spacing: 0; }


/* Slide 13 · Mid-month check-in · coach framing */
.alert-card {
  width: 100%; max-width: 340px;
  padding: 14px 16px; border-radius: 18px;
  background: linear-gradient(180deg, rgba(61,138,254,.08) 0%, rgba(61,138,254,.02) 100%);
  border: 1px solid rgba(61,138,254,.22);
  box-shadow: 0 10px 28px -16px rgba(61,138,254,.3);
  text-align: start;
}
.alert-head {
  display: flex; align-items: center; gap: 10px;
  padding-bottom: 10px;
  border-bottom: 1px solid rgba(61,138,254,.12);
  margin-bottom: 10px;
}
.alert-head .ic {
  width: 28px; height: 28px; border-radius: 10px;
  background: #16a34a; color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-size: 14px; font-weight: 900; flex-shrink: 0;
  box-shadow: 0 4px 10px rgba(34,197,94,.35);
}
.alert-head .title-row { flex: 1; min-width: 0; }
.alert-head .t { font-size: 12px; font-weight: 900; color: var(--text-primary); letter-spacing: -0.01em; }
.alert-head .stamp {
  font-size: 9px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase;
  color: var(--text-secondary); margin-top: 1px;
}
.alert-pace {
  display: flex; align-items: baseline; gap: 8px; margin-bottom: 6px; flex-wrap: wrap;
}
.alert-pace .pct {
  font-size: 26px; font-weight: 900; color: var(--brand-blue);
  letter-spacing: -0.03em; font-variant-numeric: tabular-nums;
}
.alert-pace .sep { color: var(--text-secondary); opacity: .5; font-size: 14px; }
.alert-pace .days {
  font-size: 11px; font-weight: 700; color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}
.alert-msg { font-size: 11px; color: var(--text-primary); font-weight: 600; line-height: 1.5; opacity: .85; }
.alert-chart { position: relative; height: 32px; margin: 10px 0; }
.alert-chart .bars { display: flex; gap: 3px; align-items: flex-end; height: 100%; }
.alert-chart .bars span { flex: 1; border-radius: 2px; background: rgba(61,138,254,.3); }
.alert-chart .bars span.hit { background: #f59e0b; }
.alert-chart .bars span.today {
  background: #16a34a;
  box-shadow: 0 0 0 2px rgba(34,197,94,.22), 0 4px 10px rgba(34,197,94,.3);
}
.alert-actions { display: flex; gap: 7px; margin-top: 6px; }
.alert-actions .pill {
  flex: 1; font-size: 11px; font-weight: 800;
  padding: 9px 6px; border-radius: 12px; text-align: center;
  background: var(--input-bg); color: var(--brand-blue);
  border: 1px solid rgba(61,138,254,.25);
  box-shadow: 0 1px 2px rgba(15,23,42,.04);
  letter-spacing: -0.005em;
}
.alert-actions .pill.primary {
  background: var(--brand-blue); color: #fff;
  border-color: var(--brand-blue);
  box-shadow: 0 6px 14px -6px rgba(61,138,254,.5);
}

/* Slide 14 · Subscription watchdog · dynamic renewal dates + urgent hero */
.sub-list { width: 100%; max-width: 340px; display: flex; flex-direction: column; gap: 8px; }
.sub-rich {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 14px 10px 10px;
  border-radius: 14px;
  background: var(--input-bg);
  border: 1px solid var(--input-border);
  transition: border-color .25s ease, box-shadow .25s ease;
}
.sub-rich.urgent {
  border-color: rgba(244,63,94,.35);
  background: linear-gradient(180deg, rgba(244,63,94,.05) 0%, rgba(244,63,94,.01) 100%);
  box-shadow: 0 8px 20px -14px rgba(244,63,94,.5);
}
.sub-rich .sub-icon {
  width: 38px; height: 38px; border-radius: 11px;
  background: color-mix(in oklab, var(--text-primary) 8%, transparent);
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0; font-size: 20px; line-height: 1;
}
.sub-rich .col { flex: 1; text-align: start; min-width: 0; }
.sub-rich .col .row-title {
  display: flex; align-items: center; gap: 6px; min-width: 0;
}
.sub-rich .col .row-title b {
  font-size: 12px; font-weight: 800; color: var(--text-primary);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sub-rich .col .row-meta {
  display: block;
  font-size: 10px; color: var(--text-secondary);
  margin-top: 2px;
}

/* Countdown pill — inline-block so Arabic text shapes normally (flex was breaking it) */
.sub-rich .countdown {
  display: inline-block;
  font-size: 11px; font-weight: 700;
  color: var(--text-secondary);
  background: color-mix(in oklab, var(--text-primary) 6%, transparent);
  padding: 5px 10px; border-radius: 999px;
  flex-shrink: 0; white-space: nowrap;
  line-height: 1.2;
}
.sub-rich .countdown.ok {
  color: #16a34a;
  background: rgba(34,197,94,.12);
}
.sub-rich.urgent .countdown {
  font-size: 12px; padding: 6px 12px;
  font-weight: 800;
  color: #dc2626;
  background: rgba(244,63,94,.14);
}
.sub-rich.urgent .countdown::before {
  content: "";
  display: inline-block;
  width: 6px; height: 6px; border-radius: 50%;
  background: #dc2626;
  margin-inline-end: 6px;
  vertical-align: middle;
  box-shadow: 0 0 0 0 rgba(220,38,38,.5);
  animation: watchdogDot 1.6s cubic-bezier(.32,.72,0,1) infinite;
}
@keyframes watchdogDot {
  0%, 100% { box-shadow: 0 0 0 0 rgba(220,38,38,.55); }
  70%      { box-shadow: 0 0 0 6px rgba(220,38,38,0); }
}
.watchdog-anchor {
  width: 100%; max-width: 340px;
  margin-top: 8px;
  font-size: 12px; line-height: 1.5;
  color: #16a34a; text-align: start;
}
.watchdog-anchor b { font-weight: 800; }

/* Slide 11 · 12-month forecast */
.compare-box {
  width: 100%; max-width: 340px;
  padding: 14px 16px;
  border-radius: 16px;
  background: rgba(34,197,94,.1);
  border: 1px solid rgba(34,197,94,.3);
  text-align: start;
}
[data-theme="floosyo-dark"] .compare-box {
  background: rgba(34,197,94,.08);
  border-color: rgba(34,197,94,.25);
}
.compare-row {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: 13px; font-weight: 700;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}
.compare-row + .compare-row {
  margin-top: 10px; padding-top: 10px;
  border-top: 1px solid rgba(34,197,94,.2);
}
.compare-row.total { font-weight: 900; font-size: 15px; }
.compare-row .bad { color: #e11d48; font-weight: 900; }
.compare-row .good { color: #16a34a; font-weight: 900; }

/* Paywall slide · owns the whole viewport.
   billing/_apple_paywall brings its own docked purchase sheet and its own
   horizontal rhythm, so the slide drops the shared side/bottom padding and
   keeps only the top safe-area inset — without it the hero sits under the
   status bar and the Dynamic Island.

   The bottom-nav is hidden while this slide is active — its blurred gradient
   strip (z-index 100) would paint over the purchase sheet. The progress
   container stays: it carries the back chevron the user needs to re-read the
   trial explainer.

   The slide keeps zero padding on purpose. The partial's root is `min-h-screen`
   and its purchase sheet is `position: fixed`, contained by this slide (the
   slide's `transform`/`will-change` make it the containing block). Padding the
   slide grows that box past the viewport and the sheet stops sitting on the
   bottom edge — so the room to clear the progress bar is added inside the
   scrolling area instead, where it scrolls away with the content. */
.slide--paywall { padding: 0; }
.slide--paywall [data-bridge--subscription-target="scroller"] {
  padding-top: calc(var(--safe-top) + 44px);
}
.onboarding:has(.slide--paywall.active) .bottom-nav { display: none; }


/* Slide 15 · Scoreboard + Done chips */
.scoreboard {
  width: 100%; max-width: 340px;
  padding: 14px; border-radius: 16px;
  background: linear-gradient(180deg, rgba(34,197,94,.18), rgba(34,197,94,.06));
  border: 1px solid rgba(34,197,94,.3);
  text-align: center;
  margin-bottom: 12px;
}
[data-theme="floosyo-dark"] .scoreboard {
  background: linear-gradient(180deg, rgba(34,197,94,.15), rgba(34,197,94,.04));
  border-color: rgba(34,197,94,.25);
}
.scoreboard .num {
  font-size: clamp(26px, 9vw, 38px);
  font-weight: 900; color: #16a34a;
  letter-spacing: -0.02em; line-height: 1.1;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.scoreboard .lbl {
  font-size: 10px; font-weight: 800; letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-secondary);
  margin-top: 6px;
}
.scoreboard .target {
  font-size: 12px; color: var(--text-secondary);
  margin-top: 6px;
}
.scoreboard .target b { color: #16a34a; font-weight: 900; }

.done-chips { width: 100%; max-width: 340px; display: flex; flex-direction: column; gap: 6px; }
.done-chip {
  width: 100%; padding: 10px 12px; border-radius: 12px;
  background: rgba(61,138,254,.12);
  border: 1px solid rgba(61,138,254,.2);
  font-size: 12px; font-weight: 700;
  line-height: 1.45;
  color: var(--text-primary);
  display: flex; gap: 8px; align-items: flex-start;
  text-align: start;
}
[data-theme="floosyo-dark"] .done-chip {
  background: rgba(61,138,254,.16);
  border-color: rgba(61,138,254,.28);
}
.done-chip .ic {
  color: #22c55e; font-weight: 900; flex-shrink: 0;
  line-height: 1.45;
  margin-top: 1px;
}

/* Slide 7 · Istanbul reveal — big annualized number + anchor */
.reveal-card {
  width: 100%; max-width: 340px;
  padding: 20px 16px;
  border-radius: 20px;
  background: linear-gradient(180deg, rgba(61,138,254,.12) 0%, rgba(61,138,254,.03) 100%);
  border: 1px solid rgba(61,138,254,.3);
  box-shadow: 0 12px 30px -14px rgba(61,138,254,.35);
  text-align: center;
}
.reveal-num {
  display: inline-flex; align-items: baseline; gap: 6px;
  font-size: 44px; font-weight: 900;
  color: var(--brand-blue);
  letter-spacing: -0.03em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 28px rgba(61,138,254,.35);
}
.reveal-num .cur {
  font-size: 44px; font-weight: 900;
  color: var(--brand-blue);
  letter-spacing: -0.03em;
}
.reveal-unit {
  /* Set in the copy's own case. Uppercase read as a label on a chart; the line
     is a sentence about their money. Tracking drops with it — the .1em space
     existed to open up caps — and the size comes up a shade to hold weight
     against the number, since lowercase sits visually smaller. */
  font-size: 12.5px; font-weight: 700;
  color: var(--text-secondary);
  letter-spacing: .01em; text-transform: none;
  margin-top: 6px;
}
.reveal-anchor {
  font-size: 13px; font-weight: 600;
  color: var(--brand-blue);
  font-style: italic;
  line-height: 1.55;
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid rgba(61,138,254,.18);
}
/* Istanbul slide — card now leads, so add breathing room before the title
   below it (scoped to slide 10 so the global slide-body gap is untouched). */
[data-slide="13"] .reveal-card { margin-bottom: clamp(12px, 3vh, 24px); }

/* The headline sits between the card and the reassurance line, so both gaps
   have to measure the same. The UA h1 top margin (0.67em, ~17px) is only half
   overridden by .slide-title's margin-bottom, which parked the line closer to
   the card than to the copy below; zeroing the block margins hands both gaps
   to the body's flex gap. */
[data-slide="12"] .slide-body { gap: 22px; }
[data-slide="12"] .slide-title { margin: 0; }
[data-slide="12"] .slide-desc { margin-bottom: 0; }

/* Slide 5 · habit picker — chips + amount + frequency toggle */
.habit-chips {
  display: flex; flex-wrap: wrap; gap: 6px;
  justify-content: center;
  width: 100%; max-width: 340px;
  margin-bottom: 12px;
}
.habit-chip {
  font-size: 12px; font-weight: 700;
  padding: 8px 12px; border-radius: 999px;
  background: var(--input-bg);
  color: var(--text-primary);
  border: 1.5px solid var(--input-border);
  cursor: pointer;
  transition: all .2s cubic-bezier(.32,.72,0,1);
  -webkit-tap-highlight-color: transparent;
}
.habit-chip:active { transform: scale(.96); }
.habit-chip.on {
  background: var(--brand-blue); color: #fff;
  border-color: var(--brand-blue);
  box-shadow: 0 4px 12px rgba(61,138,254,.25);
}

.habit-amount-wrap {
  position: relative; width: 100%; max-width: 340px;
  margin-bottom: 10px;
}
.habit-amount-wrap .cur-badge {
  position: absolute; top: 50%; transform: translateY(-50%);
  inset-inline-start: 10px;
  font-size: 11px; font-weight: 800;
  color: var(--brand-blue);
  background: rgba(61,138,254,.12);
  padding: 3px 8px; border-radius: 6px;
  pointer-events: none;
}
.habit-amount-wrap .freq-badge {
  position: absolute; top: 50%; transform: translateY(-50%);
  inset-inline-end: 10px;
  font-size: 11px; font-weight: 700;
  color: var(--brand-blue);
  background: rgba(61,138,254,.12);
  padding: 3px 10px; border-radius: 6px;
  pointer-events: none;
  text-transform: lowercase;
  letter-spacing: 0.02em;
}
.habit-amount-wrap input {
  width: 100%; height: 48px;
  padding-block: 0;
  padding-inline-start: 60px; /* reserves space where the currency badge sits (inset-inline-start) */
  padding-inline-end: 78px;   /* reserves space for the frequency badge on the trailing side */
  font-size: 18px; font-weight: 800;
  background: var(--input-bg);
  border: 1.5px solid var(--input-border);
  border-radius: 14px;
  color: var(--text-primary);
  outline: none;
  text-align: start;
  font-variant-numeric: tabular-nums;
}
/* Hide native number-input spinners (prevents collision with the badge, esp. in RTL). */
.habit-amount-wrap input::-webkit-outer-spin-button,
.habit-amount-wrap input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.habit-amount-wrap input[type=number] { -moz-appearance: textfield; }
.habit-amount-wrap input:focus {
  border-color: var(--brand-blue);
  box-shadow: 0 0 0 3px rgba(61,138,254,.1);
}

.freq-toggle {
  display: flex; gap: 4px;
  width: 100%; max-width: 340px;
  padding: 4px; border-radius: 999px;
  background: rgba(0,0,0,0.04);
}
[data-theme="floosyo-dark"] .freq-toggle {
  background: rgba(255,255,255,0.06);
}
.freq-toggle .freq-opt {
  flex: 1; text-align: center;
  font-size: 11px; font-weight: 800;
  padding: 8px 4px; border-radius: 999px;
  color: var(--text-secondary);
  cursor: pointer;
  transition: all .25s cubic-bezier(.32,.72,0,1);
  -webkit-tap-highlight-color: transparent;
}
.freq-toggle .freq-opt.on {
  background: var(--brand-blue); color: #fff;
  box-shadow: 0 2px 8px rgba(61,138,254,.3);
}

/* Slide 4 · voice demo — numbered 3-step walkthrough */
.vstep-list {
  width: 100%; max-width: 340px;
  display: flex; flex-direction: column; gap: 8px;
}
.vstep {
  display: flex; align-items: stretch; gap: 10px;
  padding: 10px 12px; border-radius: 12px;
  background: rgba(61,138,254,.08);
  border: 1px solid rgba(61,138,254,.2);
  text-align: start;
}
.vstep-num {
  width: 26px; height: 26px; border-radius: 50%;
  background: var(--brand-blue); color: #fff;
  font-size: 12px; font-weight: 900;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0; align-self: flex-start;
}
.vstep-num.done { background: #22c55e; }
.vstep-body { flex: 1; min-width: 0; }
.vstep-title {
  font-size: 13px; font-weight: 800; color: var(--text-primary);
  margin-bottom: 3px;
}
.vstep-title .done-check { color: #22c55e; font-weight: 900; }
.vstep-sub {
  font-size: 11px; color: var(--text-secondary);
  line-height: 1.45;
}
.vstep-sub b { color: var(--brand-blue); font-weight: 900; }
.vstep-mini { display: flex; align-items: center; gap: 8px; }
.vwave { display: inline-flex; gap: 2px; align-items: center; height: 14px; }
.vwave span {
  display: inline-block; width: 3px; border-radius: 1px;
  background: var(--brand-blue); opacity: .7;
}
.vwave span:nth-child(1) { height: 40%; }
.vwave span:nth-child(2) { height: 80%; }
.vwave span:nth-child(3) { height: 55%; }
.vwave span:nth-child(4) { height: 95%; }
.vwave span:nth-child(5) { height: 45%; }
.vwave span:nth-child(6) { height: 70%; }
.vwave span:nth-child(7) { height: 35%; }
.vwave span:nth-child(8) { height: 60%; }

/* ── Slide 13 · Three ways in · the add-expense sheet on the widgets slide's stage ──
   Same anatomy as slide 22 (.slide--widgets): the wallpaper across the top
   with the headline on it, one thing on stage, a flat edge with two big
   corners. .ow-wall and .ow-title are that slide's own classes, reused whole.

   On stage is the dashboard's add-expense sheet. Its card, deck, panels, slot,
   field, tiles, dock, thumb, skeleton and confirm card all come from
   application.css (the sheet's own rules). What lives here is this slide's
   sizing, the sheet's dark-mode dock rule (the dashboard keeps it in an inline
   <style> the onboarding page never loads), the two states the demo adds (a
   pressed tile, a live field) and the "done" mascot. */
.slide--ways {
  --ow-col: 348px;
  padding: 0 0 calc(var(--safe-bottom) + 120px);
}
/* The wall sizes to the sheet rather than to a fixed height: the sheet is one
   size on every phone, and a title that wraps to a third line in a long locale
   pushes the sheet down instead of clipping the dock. */
.slide--ways .ow-wall {
  height: auto;
  padding: calc(var(--safe-top) + 56px) 16px 14px;
}
/* The blue half on its own line, the way the old voice title broke — and in
   the wallpaper's own highlight tint, since brand blue on a blue wall is lost.
   No nowrap: a long locale wraps to a third line and the wall grows with it. */
.slide--ways .ow-title .brand-yo {
  display: block;
  color: #BFE0FF;
}
/* Scoped down from the shared 26px so this slide's second sentence holds one
   line. Measured against the bundled Nunito Sans at weight 800 with the title's
   -0.02em tracking: "Here are 3 easy ways to track it." renders 379px at 26px
   and 350px at 24px, both past the 348px --ow-col; 22px lands at 321px, which
   keeps 27px of slack for a longer locale. The vw term carries the same fit
   down to the narrow phones, where the column itself is under 348px.
   Scoped to .slide--ways so .slide--widgets, which this class comes from,
   keeps its own size. */
.slide--ways .ow-title {
  font-size: clamp(20px, 5.6vw, 22px);
}

/* The sheet's card, on the wallpaper. Its bottom pad closes under the slot's
   character count, so it is trimmed to what that line needs. */
.slide--ways .ways-card {
  width: 100%;
  max-width: var(--ow-col);
  margin-top: 16px;
  padding-bottom: 10px;
}
/* .expense-stack's gap between the two panes. */
.slide--ways .expense-dock-wrap { margin-top: 10px; }
/* The thumb by formula, not by measurement (see #selectWayTab): a layout
   property, so it always repaints, and inset-inline-start mirrors it in RTL.
   expense-deck still writes its translate inline, hence the !important. */
.slide--ways .expense-thumb {
  left: auto;
  inset-inline-start: var(--ways-thumb-x, 8px);
  transform: none !important;
  transition: inset-inline-start 0.4s cubic-bezier(0.32, 1.26, 0.5, 1);
}
@media (prefers-reduced-motion: reduce) {
  .slide--ways .expense-thumb { transition: none; }
}

/* Dark-mode dock — copied from the inline <style> in budgets/index.html.erb. */
[data-theme="floosyo-dark"] .slide--ways .glass-nav-bar {
  background: rgba(17, 26, 44, 0.82) !important;
  border-color: rgba(96, 165, 250, 0.1) !important;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35), 0 1px 3px rgba(0, 0, 0, 0.2), inset 0 0.5px 0 rgba(255, 255, 255, 0.04) !important;
}

/* Below the wallpaper, the app again: the sheet's analyzing skeleton and its
   confirm card land here, on the same column as the card above so their edges
   line up. Start-aligned, not centred: centred content that outgrows a short
   phone overflows upward, under the wall. No display rule on the items:
   Tailwind's .hidden must keep winning. */
.slide--ways .ways-out { justify-content: flex-start; padding-block: 30px 0; }
.slide--ways .ways-out-item { width: 100%; max-width: var(--ow-col); }
/* The caption under the landed card: slide 22's .ow-meta-line, spaced off
   the card. */
.slide--ways .ways-card-line { margin-top: 10px; text-align: center; }

/* The field, live while the demo types. 6.5rem is the sheet's <textarea> at
   three rows, so the stand-in is the same box. */
.slide--ways .ways-field {
  min-height: 6.5rem;
  transition: border-color 0.2s ease;
}
.slide--ways .ways-field.is-live { border-color: var(--color-primary); }
/* The demo's finger on a tile, and on the mascot. */
.slide--ways .expense-photo.is-pressed {
  transform: scale(0.94);
  background: color-mix(in oklch, var(--color-primary) 22%, transparent);
}
.slide--ways .is-tapped { transform: scale(0.95); }

/* The mascot box: the sheet's 160×140 (.expense-masc, and the mic button's
   own size), here on the wrapper so every image in it — the way in, the
   listening state, the done state — fills the same box and the box alone
   changes size on a short phone. */
.slide--ways .ways-masc { position: relative; width: 160px; height: 140px; }
.slide--ways .ways-masc-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
}
/* Done: cool-cute takes the mascot's place while the card is showing. */
.slide--ways .ways-masc-way,
.slide--ways .ways-masc-done { transition: opacity 0.3s ease, transform 0.15s ease; }
.slide--ways .ways-masc-done { opacity: 0; pointer-events: none; }
.slide--ways .is-done .ways-masc-way  { opacity: 0; }
.slide--ways .is-done .ways-masc-done { opacity: 1; }

/* Height budget. The sheet's full metrics need a Pro Max; everything else
   trades mascot and padding for the room the landed card needs under the
   wall: 874pt (iPhone 17), 852, 844, then 812 (mini, X). Measured against a
   wall of safe-top + padding + two title lines + card + dock, and a landing
   block of card + caption. SE-class phones still clip, as slide 22 does. */
@media (max-height: 899px) {
  .slide--ways .ow-wall { padding-top: calc(var(--safe-top) + 40px); padding-bottom: 12px; }
  .slide--ways .ways-card { margin-top: 12px; padding-bottom: 8px; }
  .slide--ways .expense-panel { min-height: 0; }
  .slide--ways .ways-masc { width: 120px; height: 105px; }
}
@media (max-height: 829px) {
  .slide--ways .ow-wall { padding-top: calc(var(--safe-top) + 30px); }
  .slide--ways .ways-masc { width: 100px; height: 88px; }
}

/* Speak's rings and breathing mascot. Copied from the inline <style> in
   app/views/budgets/index.html.erb, which the onboarding page does not load. */
.voice-waves {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 260px;
  height: 260px;
  overflow: visible;
}
.voice-waves path {
  fill: none;
  stroke: currentColor;
  stroke-linecap: round;
  stroke-dasharray: var(--min) 999;
  stroke-dashoffset: var(--off);
  opacity: 0.14;
  animation: voiceWavePulse 1.4s cubic-bezier(0.45, 0, 0.55, 1) infinite both;
}
@keyframes voiceWavePulse {
  0%, 100% { stroke-dasharray: var(--min) 999; stroke-dashoffset: var(--off); opacity: 0.14; }
  50%      { stroke-dasharray: var(--len) 999; stroke-dashoffset: 0;          opacity: 0.75; }
}
@keyframes mascotListen {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.04); }
}
.mascot-listen:not(.hidden) { animation: mascotListen 1.5s ease-in-out infinite; }

/* The field's caret while the demo types. */
.voice-caret {
  display: inline-block; width: 2px; height: 16px;
  vertical-align: -2px;
  background: var(--brand-blue);
  animation: voiceCaret 1s steps(2) infinite;
}
@keyframes voiceCaret { 50% { opacity: 0; } }

/* The card's entrance — the sheet's own confirmCard, only its arrival lives
   here. */
.voice-card-in {
  width: 100%;
  animation: voiceCardIn .38s cubic-bezier(.32,.72,0,1) both;
}
@keyframes voiceCardIn {
  from { opacity: 0; transform: scale(.965); }
  to   { opacity: 1; transform: scale(1); }
}

/* Locked Next button while demo plays */
.btn-next.is-locked,
.btn-dashboard.is-locked {
  opacity: .45; pointer-events: none; cursor: not-allowed;
}

/* Slide 19 · forecast · V9 Impact Bars + compound line */
.slide--forecast .slide-title {
  font-size: 22px;
  font-weight: 800;
  line-height: 1.2;
  margin: 0 0 6px;
  letter-spacing: -0.02em;
}
.slide--forecast .slide-desc {
  font-size: 13px;
  line-height: 1.5;
  margin: 0 auto 10px;
  max-width: 300px;
}

.slide--forecast .lever-card,
.slide--forecast .setup-card,
.slide--forecast .compound-card {
  width: 100%;
  background: var(--input-bg);
  border: 1px solid var(--input-border);
  border-radius: 18px;
  padding: 14px 16px 16px;
  margin-bottom: 12px;
  text-align: start;
  box-shadow: 0 10px 28px -16px rgba(61,138,254,.25);
}

.slide--forecast .lever-eyebrow {
  font-size: 10.5px;
  font-weight: 800;
  letter-spacing: 0.14em;
  color: var(--brand-blue);
  text-transform: uppercase;
  font-family: ui-monospace, Menlo, monospace;
  margin: 0 0 6px;
}
:root[dir="rtl"] .slide--forecast .lever-eyebrow {
  letter-spacing: 0;
  text-transform: none;
  font-size: 12px;
  font-family: inherit;
}
.slide--forecast .lever-list {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

/* "What we're setting up for you" — the amountless half of the plan. Same card
   shell and dashed row divider as the levers above, minus the bar and figures. */
.slide--forecast .setup-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.slide--forecast .setup-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
}
.slide--forecast .setup-row + .setup-row {
  border-top: 1px dashed color-mix(in oklab, currentColor 18%, transparent);
  padding-top: 12px;
}
.slide--forecast .setup-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 999px;
  background: color-mix(in oklab, var(--brand-blue) 10%, transparent);
  font-size: 15px;
  line-height: 1;
  flex-shrink: 0;
}
.slide--forecast .setup-text { min-width: 0; }
.slide--forecast .setup-row-title {
  margin: 0;
  font-size: 12px;
  font-weight: 800;
  color: var(--text-primary);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-family: ui-monospace, Menlo, monospace;
}
:root[dir="rtl"] .slide--forecast .setup-row-title {
  letter-spacing: 0;
  text-transform: none;
  font-size: 13px;
  font-family: inherit;
}
.slide--forecast .setup-row-body {
  margin: 2px 0 0;
  font-size: 11px;
  color: var(--text-secondary);
  line-height: 1.45;
}
.slide--forecast .lever-row { display: flex; flex-direction: column; gap: 4px; }
.slide--forecast .lever-row + .lever-row {
  border-top: 1px dashed color-mix(in oklab, currentColor 18%, transparent);
  padding-top: 14px;
}
.slide--forecast .lever-label {
  margin: 0;
  font-size: 12px;
  font-weight: 800;
  color: var(--text-primary);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-family: ui-monospace, Menlo, monospace;
}
:root[dir="rtl"] .slide--forecast .lever-label {
  letter-spacing: 0;
  text-transform: none;
  font-size: 13px;
  font-family: inherit;
}
.slide--forecast .lever-bar {
  position: relative;
  height: 30px;
  background: color-mix(in oklab, var(--brand-blue) 10%, transparent);
  border-radius: 10px;
  margin-top: 2px;
  display: flex;
  align-items: center;
}
.slide--forecast .lever-bar-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--brand-blue), var(--brand-blue-light));
  border-radius: 10px;
  box-shadow: 0 2px 10px rgba(61,138,254,0.25);
  transition: width 0.9s cubic-bezier(0.32, 0.72, 0, 1);
  flex-shrink: 0;
}
.slide--forecast .lever-amount {
  position: absolute;
  inset-inline-end: 10px;
  top: 50%;
  transform: translateY(-50%);
  white-space: nowrap;
  font-weight: 800;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
  color: var(--text-primary);
  pointer-events: none;
}
.slide--forecast .lever-equation,
.slide--forecast .lever-source {
  margin: 0;
  font-size: 11px;
  color: var(--text-secondary);
  line-height: 1.45;
}
.slide--forecast .lever-equation {
  font-family: ui-monospace, Menlo, monospace;
  font-variant-numeric: tabular-nums;
  margin-top: 2px;
}
:root[dir="rtl"] .slide--forecast .lever-equation {
  font-family: inherit;
}
.slide--forecast .lever-smallest-note {
  margin: 6px 0 0;
  font-size: 11px;
  font-style: italic;
  color: var(--text-secondary);
  opacity: 0.9;
}

.slide--forecast .compound-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 8px;
}
.slide--forecast .compound-title {
  margin: 0;
  font-size: 13px;
  font-weight: 800;
  color: var(--text-primary);
}
.slide--forecast .compound-end {
  margin: 0;
  font-size: 14px;
  font-weight: 900;
  color: var(--brand-blue);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}
.slide--forecast .compound-chart {
  width: 100%;
  height: 140px;
  display: block;
  color: var(--brand-blue);
}
.slide--forecast .compound-caption {
  margin: 10px 0 0;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--text-secondary);
  line-height: 1.45;
  letter-spacing: 0.005em;
}

.slide--forecast .forecast-cta-sentinel {
  height: 1px;
  width: 100%;
}

/* Forecast slide scrolls heavy content (lever card + compound chart) under a
   fixed bottom CTA. The slide already reserves the bottom-nav height + safe
   inset (see the .slide--forecast.slide rule below), so this only adds the
   small optical gap between the last card and that reserved band. It must NOT
   re-add --safe-bottom or the inset gets counted twice. */
.slide--forecast.slide .slide-shell--anchored > .slide-body,
.slide--forecast .slide-shell--anchored > .slide-body {
  padding-bottom: 24px;
  /* Shrink to content instead of filling viewport — eliminates the empty
     dead zone between the last card and the bottom CTA when content is
     shorter than the screen. */
  flex: 0 1 auto;
}

/* Bottom reserve sized to the REAL .bottom-nav box: 16 padding-top + 54 button
   + 14 bottom inset = 84, plus 4px of clearance. The global .slide rule
   reserves 120 + safe, which over-reserves by ~36px and leaves a visible dead
   zone under the compound card when scrolled to the end of this slide. */
.slide--forecast.slide {
  padding-bottom: calc(var(--safe-bottom, 0px) + 88px);
}
.slide--forecast .compound-caption-amount {
  font-weight: 800;
  color: var(--brand-blue);
}

/* ── Home Screen widgets (slide 22) ─────────────

   Replaces the theme picker. The wallpaper is the background of the whole top
   of the slide: full bleed behind the progress bar and the headline, closed by
   the dock, handing back to the app at a flat edge with two big corners.

   The card faces are a web port of ~/FloosYoIos/FloosYoWidget. Every colour,
   radius, ring width and offset below is the number the Swift uses, so the
   preview and the widget the user actually installs are the same object. */

.slide--widgets {
  /* WidgetTheme.swift · FY.* */
  --ow-page: #F0F4FF;
  --ow-text: #22222A;
  --ow-muted: #6B7280;
  --ow-tint: #2563EB;
  --ow-pill-bg: rgba(37, 99, 235, .08);
  --ow-goal: #10B981;
  --ow-goal-text: #059669;
  --ow-goal-pill: rgba(16, 185, 129, .10);
  --ow-over: #EF4444;
  --ow-track: rgba(34, 34, 42, .10);
  --ow-doodle: rgba(37, 99, 235, .035);
  --ow-bubble: rgba(255, 255, 255, .5);
  --ow-bubble-line: rgba(16, 185, 129, .5);
  --ow-edge: 0 8px 22px rgba(26, 26, 46, .16), 0 0 0 .5px rgba(0, 0, 0, .06);

  /* Card art is laid out at the device's own point sizes and scaled to fit. */
  --ow-scale: .90;
  --ow-col: 348px;

  padding: 0 0 calc(var(--safe-bottom) + 120px);
}
[data-theme="floosyo-dark"] .slide--widgets {
  --ow-page: #0F1117;
  --ow-text: #EAEDF3;
  --ow-muted: #94A3B8;
  --ow-tint: #60A5FA;
  --ow-pill-bg: rgba(96, 165, 250, .12);
  --ow-goal: #34D399;
  --ow-goal-text: #34D399;
  --ow-goal-pill: rgba(52, 211, 153, .12);
  --ow-track: rgba(234, 237, 243, .10);
  --ow-doodle: rgba(96, 165, 250, .05);
  --ow-bubble: rgba(255, 255, 255, .16);
  --ow-bubble-line: rgba(52, 211, 153, .55);
  --ow-edge: 0 10px 26px rgba(0, 0, 0, .5), 0 0 0 .5px rgba(255, 255, 255, .07);
}
@media (max-width: 372px) { .slide--widgets { --ow-scale: .82; } }
@media (max-width: 340px) { .slide--widgets { --ow-scale: .74; } }

/* The progress bar sits on the wallpaper on this slide, so it loses its own
   background and goes white. Same show-on-one-slide mechanism the welcome
   corners use. */
.onboarding:has(.slide.active.slide--widgets) .progress-container,
.onboarding:has(.slide.active.slide--ways) .progress-container { background: transparent; }
.onboarding:has(.slide.active.slide--widgets) .progress-bar,
.onboarding:has(.slide.active.slide--ways) .progress-bar { background: rgba(255, 255, 255, .28); }
.onboarding:has(.slide.active.slide--widgets) .progress-bar__fill,
.onboarding:has(.slide.active.slide--ways) .progress-bar__fill { background: #fff; }
.onboarding:has(.slide.active.slide--widgets) .ob-back-btn,
.onboarding:has(.slide.active.slide--ways) .ob-back-btn { color: rgba(255, 255, 255, .85); }

/* ── the wallpaper ── */
.ow-wall {
  position: relative;
  width: 100%;
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  overflow: hidden;
  /* 450 below the notch is the reference height. The min only bites on a short
     phone (SE class), where the full wall would leave the pager and the caption
     less room than they need and `.slide`'s overflow would clip the arrows. */
  height: min(calc(var(--safe-top) + 450px), calc(100vh - 320px));
  padding: calc(var(--safe-top) + 68px) 16px 22px;
  border-end-start-radius: 44px;
  border-end-end-radius: 44px;
  background:
    radial-gradient(78% 52% at 20% 4%, rgba(150, 216, 238, .92), transparent 60%),
    radial-gradient(86% 66% at 92% 88%, rgba(124, 208, 180, .88), transparent 58%),
    linear-gradient(158deg, #2f6ee0 0%, #4aa8de 44%, #8fd6c4 100%);
}
[data-theme="floosyo-dark"] .ow-wall {
  background:
    radial-gradient(78% 52% at 20% 4%, rgba(52, 96, 152, .95), transparent 60%),
    radial-gradient(86% 66% at 92% 88%, rgba(30, 92, 86, .95), transparent 58%),
    linear-gradient(158deg, #132038 0%, #1b3552 44%, #1d4440 100%);
}
/* Keeps the progress bar and the headline legible whatever the wallpaper does. */
.ow-wall::before {
  content: "";
  position: absolute;
  inset-inline: 0; inset-block-start: 0;
  height: 230px;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(0, 0, 0, .30), rgba(0, 0, 0, 0));
}
.ow-wall > * { position: relative; z-index: 2; }

.ow-title {
  width: 100%;
  max-width: var(--ow-col);
  color: #fff;
  text-align: center;
  margin: 0;
  font-size: 26px;
  text-shadow: 0 2px 14px rgba(0, 0, 0, .28);
}
.ow-title .hl { color: #BFE0FF; }
.ow-desc {
  color: rgba(255, 255, 255, .86);
  text-align: center;
  margin: 10px auto 0;
  text-shadow: 0 1px 10px rgba(0, 0, 0, .24);
}

/* ── the deck ── */
.ow-stage {
  position: relative;
  width: 100%;
  max-width: var(--ow-col);
  height: calc(170px * var(--ow-scale));
  margin-top: 20px;
}
.ow-slide {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 50%;
  translate: -50% 0;
  opacity: 0;
  transform: translateY(12px) scale(.96);
  transition: opacity .3s ease, transform .42s cubic-bezier(.22, 1, .36, 1);
  pointer-events: none;
}
:root[dir="rtl"] .ow-slide { translate: 50% 0; }
.ow-slide.is-current { opacity: 1; transform: none; pointer-events: auto; }

.ow-case {
  position: relative;
  display: block;
  width: calc(364px * var(--ow-scale));
  height: calc(170px * var(--ow-scale));
}
.ow-inner {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  display: flex;
  /* Two smalls plus this gutter come to exactly one medium: 170 + 24 + 170. */
  gap: 24px;
  transform: scale(var(--ow-scale));
  transform-origin: top left;
}
:root[dir="rtl"] .ow-inner { transform-origin: top right; }

/* ── the dock ── */
.ow-dock {
  width: 100%;
  max-width: var(--ow-col);
  margin-top: auto;
  height: 46px;
  flex: 0 0 auto;
  border-radius: 21px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  background: rgba(255, 255, 255, .30);
  backdrop-filter: blur(10px);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .24);
}
[data-theme="floosyo-dark"] .ow-dock {
  background: rgba(255, 255, 255, .10);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .12);
}
.ow-dock i { width: 32px; height: 32px; border-radius: 10px; background: rgba(255, 255, 255, .42); }
[data-theme="floosyo-dark"] .ow-dock i { background: rgba(255, 255, 255, .17); }

/* ── below the wallpaper, the app again ── */
.ow-below {
  width: 100%;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 0 24px;
}
.ow-pager { display: flex; align-items: center; justify-content: center; gap: 16px; }
.ow-arrow {
  width: 42px; height: 42px;
  flex: 0 0 auto;
  border-radius: 999px;
  display: grid; place-items: center;
  cursor: pointer;
  border: 1px solid var(--feature-border);
  background: var(--feature-bg);
  color: var(--brand-blue);
  transition: transform .15s ease, background .2s ease;
  -webkit-tap-highlight-color: transparent;
}
.ow-arrow svg {
  width: 20px; height: 20px;
  fill: none; stroke: currentColor;
  stroke-width: 2.6; stroke-linecap: round; stroke-linejoin: round;
}
:root[dir="rtl"] .ow-arrow svg { transform: scaleX(-1); }
.ow-arrow:active { transform: scale(.9); }

.ow-dots { display: flex; gap: 7px; align-items: center; }
.ow-dot {
  width: 7px; height: 7px;
  padding: 0;
  border: none;
  border-radius: 999px;
  background: var(--dot-inactive);
  cursor: pointer;
  transition: width .25s ease, background .25s ease;
  -webkit-tap-highlight-color: transparent;
}
.ow-dot.is-on { width: 22px; background: var(--brand-blue); }

.ow-meta { text-align: center; margin-top: 18px; }
.ow-meta-title { margin: 0; font-size: 19px; font-weight: 900; letter-spacing: -.01em; color: var(--text-primary); }
.ow-meta-size {
  display: inline-block;
  margin-inline-start: 8px;
  font-size: 9.5px; font-weight: 900;
  letter-spacing: .05em; text-transform: uppercase;
  color: var(--brand-blue);
  background: var(--feature-bg);
  border: 1px solid var(--feature-border);
  border-radius: 999px;
  padding: 3px 8px;
  vertical-align: middle;
}
:root[dir="rtl"] .ow-meta-size { text-transform: none; letter-spacing: 0; padding-block: 4px; }
.ow-meta-line {
  font-size: 13px; line-height: 1.5;
  color: var(--text-secondary);
  max-width: 300px;
  margin: 6px auto 0;
}

/* ══ the widget faces ══════════════════════════ */

.ow-card {
  position: relative;
  flex: 0 0 auto;
  overflow: hidden;
  border-radius: 26px;
  background: var(--ow-page);
  color: var(--ow-text);
  box-shadow: var(--ow-edge);
}
.ow-card--md { width: 364px; height: 170px; }
.ow-card--sm { width: 170px; height: 170px; }

/* FYWidgetBackground's currency scatter. The small family shows the leading
   crop, exactly as it does on the device. */
.ow-doodles { position: absolute; inset: 0; z-index: 0; pointer-events: none; color: var(--ow-doodle); }
.ow-doodles span {
  position: absolute;
  font-weight: 900;
  line-height: 1;
  transform: translate(-50%, -50%) rotate(var(--ow-rot));
}

/* FYOpenCue. Widgets are read-only, so the cue points into the app. */
.ow-cue {
  position: absolute;
  inset-block-end: 12px; inset-inline-end: 12px;
  z-index: 4;
  border-radius: 999px;
  display: grid; place-items: center;
  background: linear-gradient(135deg, #3D8AFE, #2563EB);
  box-shadow: 0 5px 8px rgba(37, 99, 235, .35);
}
.ow-cue svg {
  width: 40%; height: 40%;
  fill: none; stroke: #fff;
  stroke-width: 3.4; stroke-linecap: round; stroke-linejoin: round;
}
:root[dir="rtl"] .ow-cue svg { transform: scaleX(-1); }

.ow-stack {
  position: absolute;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  text-align: start;
}
.ow-stack--due { inset-block-start: 34px; inset-inline-start: 132px; gap: 6px; }
.ow-stack--goal { inset-block-start: 40px; inset-inline-start: 150px; inset-inline-end: 44px; gap: 5px; }
.ow-stack--budgets {
  inset-block-start: 14px; inset-inline-start: 0; inset-inline-end: 12px; inset-block-end: 12px;
  justify-content: space-between;
}
.ow-stack--sm {
  inset-block-start: 16px; inset-inline-start: 14px; inset-inline-end: 14px;
  flex-direction: row; align-items: center; gap: 8px;
}
.ow-sm-text { min-width: 0; display: flex; flex-direction: column; gap: 2px; text-align: start; }

.ow-row { display: flex; align-items: center; gap: 5px; white-space: nowrap; }
.ow-row--head { align-items: baseline; gap: 8px; }
.ow-stack--budgets .ow-row--head { padding-inline-start: 70px; }

.ow-pill {
  display: inline-flex; align-items: center;
  font-size: 11px; font-weight: 900;
  text-transform: uppercase; letter-spacing: .4px;
  padding: 4px 10px;
  border-radius: 999px;
  color: var(--ow-tint);
  background: var(--ow-pill-bg);
}
.ow-pill--goal { color: var(--ow-goal-text); background: var(--ow-goal-pill); }
:root[dir="rtl"] .ow-pill { text-transform: none; letter-spacing: 0; padding-block: 5px; }

.ow-name { font-size: 11.5px; font-weight: 800; color: var(--ow-muted); }
.ow-pct { font-size: 12px; font-weight: 900; color: var(--ow-goal-text); font-variant-numeric: tabular-nums; }
.ow-day { font-size: 11px; font-weight: 700; color: var(--ow-muted); }
.ow-ctx { font-size: 11px; font-weight: 700; color: var(--ow-muted); white-space: nowrap; }
.ow-ctx--lg { font-size: 14px; }

.ow-hero {
  font-size: 34px; font-weight: 900;
  letter-spacing: -.02em; line-height: 1.06;
  color: var(--ow-text);
  font-variant-numeric: tabular-nums;
}
.ow-hero--goal { font-size: 32px; }
.ow-hero--sm { font-size: 22px; }
.ow-money-frac { font-size: .6em; }

/* FYProgressRing. The sweep replays whenever a card becomes the current one. */
.ow-ring { position: relative; flex: 0 0 auto; display: block; }
.ow-ring svg { position: absolute; inset: 0; transform: rotate(-90deg); }
.ow-ring-arc { stroke-dashoffset: var(--ow-circ); }
.ow-ring-center {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  line-height: 1;
}
.ow-ring-center img { display: block; }
.ow-ring-pct { font-weight: 900; font-variant-numeric: tabular-nums; color: var(--ow-tint); }
.ow-ring-pct--over { color: var(--ow-over); }
.ow-ring-pct--goal { color: var(--ow-goal-text); }

/* FYGoalWater / FYWaterSurfaceShape. The sea covers the whole card, the body
   fills from the leading edge to the goal percentage behind a vertical sine of
   amplitude 4.2 over a 64pt period, and the meniscus is drawn twice: a wide
   blurred pass for the glow and a hairline on top. On the device the crest is a
   fixed silhouette and only the level moves, so that is all the clock moves.
   Both paths are re-traced every frame by the deck's fill clock, and the bubble
   field is sized to the filled part so bubbles never float over what is not
   saved yet. */
.ow-water { position: absolute; inset: 0; z-index: 1; pointer-events: none; }
:root[dir="rtl"] .ow-water { transform: scaleX(-1); }
.ow-water svg { position: absolute; inset-block-start: 0; inset-inline-start: 0; display: block; }
.ow-wave-line { fill: none; stroke: var(--ow-goal); stroke-opacity: .47; stroke-width: 1.4; }
.ow-wave-line--soft { stroke-opacity: .10; stroke-width: 7; }
.ow-card--goal .ow-bubbles { inset-block: 0; inset-inline-start: 0; inset-inline-end: auto; width: 0; opacity: 0; }

/* FYBubbleField. Held positions: a widget renders stills, so bubbles that
   actually rose would cost a run of timeline entries for scenery. */
.ow-bubbles { position: absolute; inset: 0; overflow: hidden; }
.ow-bubbles i {
  position: absolute;
  border-radius: 999px;
  background: var(--ow-bubble);
  border: .7px solid var(--ow-bubble-line);
  transform: translate(-50%, 50%);
}
/* FYGoalBubbleBackground: the small family takes the goal's colour and its
   bubbles and leaves the surface to the wide card. */
.ow-wash {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--ow-goal) 0%, transparent) 0%,
    color-mix(in srgb, var(--ow-goal) 4.5%, transparent) 55%,
    color-mix(in srgb, var(--ow-goal) 14%, transparent) 100%);
}

/* GoalMoneyScatter, cropped from running-with-money.svg. */
.ow-notes { position: absolute; inset: 0; z-index: 2; pointer-events: none; }
.ow-notes img {
  position: absolute;
  display: block;
  transform: rotate(var(--ow-rot)) scaleX(var(--ow-flip, 1));
}

.ow-mascot {
  position: absolute;
  z-index: 3;
  pointer-events: none;
  transform-origin: bottom left;
}
:root[dir="rtl"] .ow-mascot { transform-origin: bottom right; }
/* The mascot grows in from a point, staggered behind its neighbours, the way a
   widget pops onto the Home Screen when its timeline entry lands. */
.ow-mascot.is-growing { animation: ow-grow .82s cubic-bezier(.22, 1.28, .36, 1) both; }
@keyframes ow-grow {
  from { transform: scale(.28); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}
.ow-mascot img { display: block; height: 100%; width: auto; }
.ow-mascot--due img, .ow-mascot--budget img { filter: drop-shadow(0 6px 7px rgba(37, 99, 235, .16)); }
/* Vertically centred rather than bottom aligned, so it grows out of the leading
   edge instead of up off the floor. `translate` is its own property, so the
   grow keyframes still compose with the centring. */
.ow-mascot--budgets { inset-block-start: 50%; translate: 0 -50%; height: 96px; transform-origin: left center; }
:root[dir="rtl"] .ow-mascot--budgets { transform-origin: right center; }
.ow-mascot--budgets img { filter: drop-shadow(0 6px 7px rgba(37, 99, 235, .16)); }
.ow-mascot--goal img { filter: drop-shadow(0 6px 7px rgba(16, 185, 129, .18)); }

/* TopBudgets cells: fixed width and anchored to the start, so one or two
   budgets never float to the middle. */
.ow-cells { display: flex; align-items: flex-start; gap: 6px; padding-inline-start: 68px; }
.ow-cell { width: 90px; display: flex; flex-direction: column; align-items: center; gap: 10px; }
.ow-cell-name {
  font-size: 11px; font-weight: 800;
  color: var(--ow-text);
  padding-top: 5px;
  max-width: 100%;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ow-cell-money { font-size: 10px; font-weight: 700; color: var(--ow-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.ow-cell-money--over { color: var(--ow-over); }

/* The fill clock still runs under reduced motion, it just lands on frame one:
   the card arrives filled instead of filling. */
@media (prefers-reduced-motion: reduce) {
  .ow-slide { transition: none; }
  .ow-mascot.is-growing { animation: none; }
  .ow-ring-arc { stroke-dashoffset: var(--ow-dash); }
}

/* Arabic descender fix — small uppercase/tracked labels clip letters like س, ش, ص in Almarai.
   Drop the (Arabic-meaningless) uppercase + wide tracking and give the line-box room. */
:root[dir="rtl"] .reveal-unit,
:root[dir="rtl"] .slide-eyebrow,
:root[dir="rtl"] .statement-eyebrow,
:root[dir="rtl"] .alert-head .stamp,
:root[dir="rtl"] .breakdown-title,
:root[dir="rtl"] .scoreboard .lbl,
:root[dir="rtl"] .popular-badge {
  text-transform: none;
  letter-spacing: 0;
  line-height: 1.7;
  padding-block: 2px;
}

/* Arabic title tuning for q_peace-style sub-count slides:
   drop Latin-tight tracking, give the line room, and let the browser
   balance wrapping so no single word is orphaned on its own line. */
:root[dir="rtl"] .slide--sub-count .sub-count-title {
  letter-spacing: 0;
  line-height: 1.5;
  word-spacing: 0.03em;
  text-wrap: balance;
  max-width: 22ch;
}
:root[dir="rtl"] .slide--sub-count .sub-count-desc {
  line-height: 1.75;
  max-width: 34ch;
}


/* ── Slide 24 · Rating · 5-star tap UI ── */
.rating-stars {
  display: flex;
  gap: 8px;
  margin: 8px 0 14px;
  justify-content: center;
}
.rating-star {
  width: 48px; height: 48px;
  display: inline-flex; align-items: center; justify-content: center;
  border: none; background: transparent; padding: 0; cursor: pointer;
  color: color-mix(in oklab, var(--color-base-content, #111) 18%, transparent);
  transition: color .15s ease, transform .15s cubic-bezier(0.32, 0.72, 0, 1);
  -webkit-tap-highlight-color: transparent;
}
.rating-star svg { width: 38px; height: 38px; display: block; }
.rating-star:active { transform: scale(0.92); }
.rating-star.on { color: #f59e0b; }
.rating-hint {
  font-size: 13px; font-weight: 600;
  color: var(--brand-blue);
  margin: 0;
  text-align: center;
}

/* ── Slide 5 · Subscriptions (merged bombshell + bridge) ──────────────────
   No card and no receipt: the block sits straight on the page like the
   statement slides. The estimate is deliberately quiet in tone but loud in
   scale — the figure is the headline, the reassurance immediately answers it. */
.slide--subs {
  /* Owns the space on BOTH sides of the amount so it stays optically centred
     between the lead-in and the reassurance. Declared on the slide rather than
     on .subs-lede because .subs-reassure is a sibling, not a descendant, and
     would otherwise silently fall back to its own literal. */
  --subs-gap: 20px;
}
/* Explicit margins below own the vertical rhythm, so the shell's default
   14px child gap would double up on every one of them. */
.slide--subs .slide-body { gap: 0; }

.subs-lede {
  margin: 0;
  text-align: center;
}
.subs-lede-line {
  display: block;
  font-size: 17px; font-weight: 750;
  letter-spacing: -0.015em; line-height: 1.2;
  color: color-mix(in oklab, var(--color-base-content, #111) 82%, transparent);
}
/* The name, same .hl convention the statement slides use. Full brand blue at
   this size reads as emphasis rather than competing with the amount below. */
.subs-lede-line .hl { color: var(--brand-blue); font-weight: 850; }
.subs-amount {
  /* Baseline, not flex-start: the unit has to sit on the digits' baseline, and
     hard-coding that offset means doing arithmetic on font metrics the browser
     may not even be using (Nunito Sans's hhea and OS/2 win descenders disagree
     by ~2px at this size). Letting the browser align baselines is exact and
     survives a font swap. */
  display: flex; align-items: baseline; justify-content: center;
  margin-top: var(--subs-gap);
  font-size: 62px; font-weight: 900; line-height: 0.96;
  color: var(--brand-blue);
  font-variant-numeric: tabular-nums;
}
/* A money figure reads left-to-right in both locales, and the odometer emits
   one element per digit, which dir="rtl" would otherwise reverse. Scoped to the
   figure so .subs-unit still flips to the correct side in Arabic. */
.subs-figure { display: flex; align-items: flex-start; direction: ltr; }
.subs-symbol { display: block; height: 1em; line-height: 1; }
.subs-symbol--suffix { margin-inline-start: 0.08em; }
.subs-unit {
  font-size: 0.3em; font-weight: 800; letter-spacing: -0.3px;
  /* No vertical nudge: .subs-amount aligns it on the digits' baseline. */
  margin-inline-start: 4px;
  color: color-mix(in oklab, var(--brand-blue) 66%, transparent);
}

/* Odometer. Each numeric cell is a 1em window onto a strip of twenty digits
   (0-9 twice), and #rollOdometer translates the strip in `em` — one digit per
   unit. Cells are exactly one tabular figure wide, with the display tracking
   re-applied as a negative margin: letter-spacing inside a fixed-width cell
   fights the clip and shears the glyph. */
.subs-odo { display: flex; align-items: flex-start; letter-spacing: normal; }
/* A flex container takes its baseline from its first item, and every digit cell
   below is overflow:hidden — which makes an element report its bottom margin
   edge as its baseline, not its text baseline. Without this strut the odometer
   would hand .subs-figure a baseline ~10px too low, which is exactly what drags
   the unit off in Arabic, where the symbol is a suffix and the odometer is the
   first item. A zero-width space is real text, so it carries a real baseline at
   the same position as the digits, and it is not clipped. */
.subs-odo::before {
  content: "\200B";
  display: block;
  width: 0;
  height: 1em; line-height: 1;
}
.subs-odo-lit { display: block; height: 1em; line-height: 1; }
.subs-odo-cell {
  display: block; overflow: hidden;
  height: 1em; line-height: 1;
  width: 1ch; margin-inline-end: -0.055em;
}
.subs-odo-strip {
  display: block;
  will-change: transform;
  transform: translateY(0);
  transition: transform 1.15s cubic-bezier(0.19, 1.08, 0.36, 1);
}
.subs-odo-strip > span { display: block; height: 1em; line-height: 1; text-align: center; }
@media (prefers-reduced-motion: reduce) {
  .subs-odo-strip { transition: none; }
}

.subs-reassure {
  margin: var(--subs-gap) 0 0;
  font-size: 21px; font-weight: 850;
  line-height: 1.28; letter-spacing: -0.02em;
  text-align: center; text-wrap: balance;
  color: var(--color-base-content, #111);
}
.subs-reassure .hl { color: var(--brand-blue); }
.subs-desc {
  margin: 9px 0 0; max-width: 318px;
  font-size: 14px; line-height: 1.55; text-align: center;
  color: color-mix(in oklab, var(--color-base-content, #111) 58%, transparent);
}

@media (max-width: 360px) {
  .subs-amount { font-size: 52px; }
  .subs-reassure { font-size: 19px; }
}

/* ══════════════════════════════════════════════════════════════════════
   Slide 14 · budget amount
   The handle is deliberately the assign screen's vial grip — a circle with
   a brand ring and a soft drop — so the amount set here and the amounts
   adjusted on /budgets/assign read as the same control. That is also why
   this is drawn rather than an <input type="range">: a native thumb cannot
   be made to match, and the tool pages hand-roll theirs for the same reason.
   ══════════════════════════════════════════════════════════════════════ */
.amount-wrap { width: 100%; max-width: 340px; }

.amount-hero {
  display: flex; align-items: baseline; justify-content: center; gap: 6px;
  font-size: 52px; font-weight: 800; letter-spacing: -0.035em; line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--color-base-content, #111);
}
.amount-cur { font-size: 26px; font-weight: 800; opacity: 0.42; letter-spacing: -0.02em; }
/* Dashed rule so the number reads as something you can tap and change; solid
   brand once it has focus. Without it nobody discovers the field. */
.amount-input {
  width: 5ch; border: 0; padding: 0 0 2px; background: transparent;
  text-align: center; font: inherit; color: inherit; outline: none;
  caret-color: var(--brand-blue); font-variant-numeric: tabular-nums;
  border-bottom: 2px dashed color-mix(in oklab, var(--color-base-content, #111) 22%, transparent);
  transition: border-color 0.18s ease;
}
/* application.css pins every text input to 16px !important inside
   @supports (-webkit-touch-callout: none) so iOS never zooms on focus. That
   guard is false in a desktop browser and true in the app, which is why this
   field looked right on a laptop and rendered at 16px next to its 26px currency
   symbol on device. Out-specify it the same way goals/_form does for its own
   amount field: carry the type selector so this wins on specificity. */
input[type="text"].amount-input { font-size: 52px !important; }

.amount-input:focus { border-bottom-color: var(--brand-blue); border-bottom-style: solid; }

@keyframes amount-limit-pulse {
  0%   { border-bottom-color: #ef4444; color: #ef4444; }
  100% { border-bottom-color: color-mix(in oklab, var(--color-base-content, #111) 22%, transparent); color: inherit; }
}
.amount-input.at-limit { animation: amount-limit-pulse 0.55s ease-out; }

.amount-slider { margin-top: 22px; touch-action: none; user-select: none; -webkit-user-select: none; }
/* A gutter of half a handle each side. Without it the handle is translated
   -50% off a track that starts at the container edge, so at the minimum its
   leading half is clipped clean off. */
.amount-slider > * { margin-inline: 16px; }
.amount-track {
  position: relative; height: 30px; display: flex; align-items: center; cursor: ew-resize;
}
.amount-rail {
  position: absolute; inset-inline: 0; top: 50%; transform: translateY(-50%);
  height: 13px; border-radius: 999px; overflow: hidden;
  background: color-mix(in oklab, var(--color-base-content, #111) 9%, transparent);
  box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.16);
}
[data-theme="floosyo-dark"] .amount-rail {
  background: rgba(255, 255, 255, 0.07);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.5);
}
.amount-fill {
  position: absolute; inset-block: 0; inset-inline-start: 0; width: 0;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--brand-blue), var(--brand-blue-light));
  transition: width 0.06s linear;
}
/* Same lit edge the CTA carries (inset 0 1px 0 rgba(255,255,255,.15)). */
.amount-fill::after {
  content: ""; position: absolute; inset-inline: 0; top: 0; height: 4px; border-radius: 999px;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.38), transparent);
}
.amount-glow {
  position: absolute; top: 50%; transform: translateY(-50%); height: 19px;
  inset-inline-start: 0; width: 0; border-radius: 999px; pointer-events: none;
  box-shadow: 0 0 22px -2px color-mix(in oklab, var(--brand-blue) 55%, transparent);
  transition: width 0.06s linear, opacity 0.25s ease; opacity: 0.85;
}
.amount-thumb {
  position: absolute; inset-inline-start: 0; top: 50%;
  width: 30px; height: 30px; border-radius: 999px;
  transform: translate(-50%, -50%); pointer-events: none;
  background: var(--color-base-200, #fff);
  border: 4px solid var(--brand-blue);
  box-shadow: 0 6px 16px -6px rgba(15, 23, 42, 0.55);
  transition: transform 0.16s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.22s ease;
}
[dir="rtl"] .amount-thumb { transform: translate(50%, -50%); }
.amount-slider.grabbing .amount-thumb {
  transform: translate(-50%, -50%) scale(1.14);
  box-shadow: 0 8px 20px -6px rgba(15, 23, 42, 0.6),
              0 0 0 9px color-mix(in oklab, var(--brand-blue) 14%, transparent);
}
[dir="rtl"] .amount-slider.grabbing .amount-thumb { transform: translate(50%, -50%) scale(1.14); }
.amount-slider.grabbing .amount-glow { opacity: 1; }

/* The value rides the handle while it is dragged — a handle under a fingertip
   is a handle you cannot read. Hidden at rest so it never sits there repeating
   the hero number back at itself. --amount-nudge keeps it on the rail at the
   ends, where centring alone would push it off. */
.amount-bubble {
  position: absolute; bottom: calc(100% + 4px); inset-inline-start: 0;
  transform: translate(calc(-50% + var(--amount-nudge, 0px)), 5px) scale(0.88);
  opacity: 0; pointer-events: none;
  padding: 4px 11px; border-radius: 999px;
  background: var(--brand-blue); color: #fff;
  font-size: 12.5px; font-weight: 800; white-space: nowrap;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 8px 18px -7px rgba(61, 138, 254, 0.85);
  transition: opacity 0.16s ease, transform 0.18s cubic-bezier(0.22, 1, 0.36, 1);
}
.amount-bubble::after {
  content: ""; position: absolute; top: 100%; inset-inline-start: 50%;
  margin-inline-start: calc(-5px - var(--amount-nudge, 0px));
  border: 5px solid transparent; border-top-color: var(--brand-blue);
}
.amount-slider.grabbing .amount-bubble {
  opacity: 1; transform: translate(calc(-50% + var(--amount-nudge, 0px)), 0) scale(1);
}

/* Ticks the value has passed take the brand colour, so the ruler fills up with
   the liquid instead of sitting there inert. */
.amount-ticks { position: relative; height: 11px; margin-top: 8px; }
.amount-tick {
  position: absolute; bottom: 0; inset-inline-start: 0; transform: translateX(-50%);
  width: 1.5px; height: 4px; border-radius: 2px;
  background: color-mix(in oklab, var(--color-base-content, #111) 16%, transparent);
  transition: background 0.18s ease, height 0.18s ease;
}
.amount-tick.major { height: 8px; width: 2px; }
.amount-tick.on { background: color-mix(in oklab, var(--brand-blue) 55%, transparent); }
.amount-tick.major.on { background: var(--brand-blue); }
.amount-scale { position: relative; height: 14px; margin-top: 6px; }
.amount-scale span {
  position: absolute; top: 0; inset-inline-start: 0; transform: translateX(-50%);
  font-size: 11px; line-height: 14px; font-weight: 700; white-space: nowrap;
  font-variant-numeric: tabular-nums;
  color: color-mix(in oklab, var(--color-base-content, #111) 34%, transparent);
}
.amount-scale span:first-child { transform: none; }
.amount-scale span:last-child  { transform: translateX(-100%); }

/* Content fits, so this slide opts out of the anchored shell's peek-fade —
   the gradient would only wash out the bottom of the ruler. Same override
   .slide--trial carries, for the same reason. */
.slide--budget-amount .slide-shell--anchored > .slide-body {
  -webkit-mask-image: none;
          mask-image: none;
  padding-bottom: 0;
}

/* ── Slide 15 · framework legend ───────────────────────────────────────
   Collapsed unless the card is chosen. Four cards each listing their slots
   is a wall of figures on arrival; one card's worth is a reward for having
   picked. Same collapse timing as .breakdown above, applied to the legend
   itself so the markup does not need a wrapper.

   Within the open card the slots are equal columns, one per slot, name over
   amount — an inline wrap made the cells two lines tall and they broke 2-then-1
   at uneven heights, which is what made the card read as unfinished. */
/* .fw-wrap owns the open/close height so the legend itself keeps its own grid.
   Animating grid-template-rows moves the row's REAL height, which matters:
   max-height animated 0 -> 220px on a row that is only ~54px tall does all its
   visible growth in the first quarter of the timeline and all its visible
   shrink in the last quarter. The card being opened therefore claimed its space
   immediately while the card being closed held onto its own until the very end,
   so the list swelled by a full row for most of the switch and shoved every
   card underneath. */
.tier.framework .fw-wrap {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  margin-top: 0; padding-top: 0;
  /* The border stays declared but transparent: a 0-height box still paints its
     border, which would leave a stray hairline on every card. */
  border-top: 1px solid transparent;
  /* Opening and closing MUST share this duration and this curve. With the same
     easing both ways the closing card is H*(1-e(t)) exactly as the opening card
     is H*e(t), so the two sum to H on every frame: the list total never changes
     and nothing below the pair can move. The previous rule deliberately closed
     in 0.2s against a 0.3s open, which is what broke the trade. Every legend is
     one grid row whatever the slot count, so H is identical across the cards
     and the exchange balances exactly. */
  transition: grid-template-rows 0.32s cubic-bezier(0.4, 0, 0.2, 1),
              margin-top 0.32s cubic-bezier(0.4, 0, 0.2, 1),
              padding-top 0.32s cubic-bezier(0.4, 0, 0.2, 1),
              opacity 0.2s linear;
}
.tier.framework.selected .fw-wrap {
  grid-template-rows: 1fr;
  opacity: 1;
  margin-top: 7px; padding-top: 7px;
  /* Separates the education (the split bar) from the answer (the money). */
  border-top-color: color-mix(in oklab, var(--color-base-content, #111) 9%, transparent);
}
/* A grid child only collapses below its content height with both of these. */
.tier.framework .fw-wrap > .fw-names {
  min-height: 0;
  overflow: hidden;
}
.tier.framework .fw-names {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: 10px;
  /* The base .fw-names rule ships margin-top: 11px for the legacy tier card.
     The wrapper owns the gap here, so the inner element must contribute none. */
  margin-top: 0;
}
.tier.framework .fw-name {
  display: block;
  white-space: nowrap;
  min-width: 0;
  /* .slide-shell centres its text; the legend is a table of figures and has to
     read from the start edge like the title above it. */
  text-align: start;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.01em;
  /* The name and its share are the label; the amount below is the value. Muting
     this line is what lets the money lead without shouting. */
  color: var(--text-secondary);
}
.tier.framework .fw-name::before {
  width: 7px; height: 7px;
  margin-inline-end: 5px;
  vertical-align: 1px;
}
.tier.framework .fw-name small {
  font-size: 10.5px; font-weight: 700;
  margin-inline-start: 3px;
  color: var(--brand-blue);
  opacity: 0.72;
}
.tier.framework .fw-amount {
  display: block;
  margin-top: 2px;
  margin-inline-start: 12px;   /* clears the dot, so the numbers line up */
  font-size: 15px; font-weight: 800; letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}

/* No amount to show — the user skipped the amount step. The open card lists
   names and shares only; nothing is left half-drawn. */
.tier.framework .fw-names.is-unpriced {
  display: flex; flex-wrap: wrap; gap: 6px 14px;
}
.tier.framework.selected .fw-wrap:has(.fw-names.is-unpriced) {
  padding-top: 0; border-top-color: transparent;
}
.tier.framework .fw-names.is-unpriced .fw-name {
  display: inline-flex; align-items: center;
  font-size: 13px; color: var(--text-primary);
}
.tier.framework .fw-names.is-unpriced .fw-name::before { vertical-align: baseline; }
.tier.framework .fw-names.is-unpriced .fw-name small { font-size: 11px; opacity: 1; }
.tier.framework .fw-names.is-unpriced .fw-amount { display: none; }

/* German runs the legend a few pixels past the card, which broke the row onto a
   second line on the three-slot frameworks. Tightened only for :lang(de) so the
   other locales keep the 13px reading size. */
.tier.framework .fw-names.is-unpriced:lang(de) { column-gap: 9px; }
.tier.framework .fw-names.is-unpriced:lang(de) .fw-name { font-size: 11.5px; }

/* Split bar — a shade taller with a lit top edge, the same trick the CTA and
   the amount slider use, so the three surfaces read as one family. */
.tier.framework .fw-split { height: 7px; gap: 5px; margin-top: 9px; }
.tier.framework .fw-split .fw-seg { position: relative; overflow: hidden; }
.tier.framework .fw-split .fw-seg::after {
  content: ""; position: absolute; inset-inline: 0; top: 0; height: 3px;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.34), transparent);
}

/* The slots arrive one after another rather than as a block. 45ms apart is
   enough to read as a sequence and short enough that the whole legend is in
   place inside a fifth of a second. */
@keyframes fw-slot-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
.tier.framework.selected .fw-name {
  animation: fw-slot-in 0.26s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.tier.framework.selected .fw-name-1 { animation-delay: 0.045s; }
.tier.framework.selected .fw-name-2 { animation-delay: 0.09s; }

/* The bar is the thing that actually changes shape between frameworks, so it
   gets the one flourish: a quick wipe out from the start edge as it lands. */
@keyframes fw-split-in {
  from { transform: scaleX(0.9); opacity: 0.55; }
  to   { transform: none; opacity: 1; }
}
.tier.framework.selected .fw-split {
  transform-origin: left center;
  animation: fw-split-in 0.3s cubic-bezier(0.22, 1, 0.36, 1) both;
}
:root[dir="rtl"] .tier.framework.selected .fw-split { transform-origin: right center; }

/* The tick reads as a real control rather than a stray ring: it settles into
   place when the card is chosen instead of just changing colour. */
.tier.framework .fw-tick {
  transition: background 0.18s ease,
              border-color 0.18s ease,
              transform 0.32s cubic-bezier(0.34, 1.56, 0.5, 1);
}
.tier.framework.selected .fw-tick {
  transform: scale(1.12);
  box-shadow: 0 4px 12px -4px rgba(61, 138, 254, 0.7);
}

@media (prefers-reduced-motion: reduce) {
  .tier.framework,
  .tier.framework .fw-tick,
  .tier.framework .fw-wrap { transition: none; }
  .tier.framework.selected .fw-name,
  .tier.framework.selected .fw-split { animation: none; }
}

@media (prefers-reduced-motion: reduce) {
  .amount-fill, .amount-glow, .amount-thumb, .amount-bubble, .amount-tick { transition: none; }
  .amount-input.at-limit { animation: none; }
}

/* Skip link for slide 14, directly under the CTA. Same show-on-one-slide
   mechanism as .welcome-signin-row. */
.amount-skip-row { display: none; margin-top: 11px; text-align: center; }
.onboarding:has(.slide.active.slide--budget-amount) .amount-skip-row { display: block; }
.amount-skip-link {
  background: none; border: none; font: inherit; font-size: 13.5px; font-weight: 600;
  padding: 6px 10px; border-radius: 10px; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  color: color-mix(in oklab, var(--color-base-content, #111) 48%, transparent);
}
.amount-skip-link:active { background: color-mix(in oklab, var(--color-base-content, #111) 8%, transparent); }


/* ── Slide 4 · Greeting ──────────────────────────────────────────────────────
   The first payoff screen: the name from the previous slide read back inside a
   speech bubble, with the mascot answering underneath.

   Surface and tail are the dashboard empty-state mascot's
   (budgets/_no_expenses_mascot.html.erb) — base-200 fill, hairline border, and
   a rotated square clipped by a short overflow-hidden box so only its tip
   clears the bubble. The fill has to be OPAQUE: a translucent one lets the
   tail's own square show through the bubble it is attached to, which is what
   turned the tail into a floating diamond. */
/* Surface and border copied from the dashboard greeting bubble
   (budgets/_body.html.erb:219 — bg-base-200 + border-blue-100, and the
   [data-theme="floosyo-dark"] .speech-bubble pair in budgets/index.html.erb). */
.slide--greeting {
  --greet-bg: var(--color-base-200, #e8eefc);
  --greet-line: #dbeafe;
}
[data-theme="floosyo-dark"] .slide--greeting {
  --greet-line: rgba(96, 165, 250, 0.15);
}

.greet-body { gap: 0; }

/* fit-content keeps the bubble wrapped around the sentence instead of holding a
   fixed box the short locales cannot fill. The max-width is wide enough that
   the copy's own <br> is the only break — nothing wraps a second time. */
/* rounded-2xl and the flat, shadowless surface are the dashboard bubble's.
   Padding is scaled to this bubble's larger type — the dashboard's px-6 py-3.5
   is set for 15px copy and would crowd 25px. */
.greet-bubble {
  position: relative;
  width: fit-content;
  max-width: 340px;
  padding: 20px 26px;
  border-radius: 16px;
  background: var(--greet-bg);
  border: 1px solid var(--greet-line);
}
.greet-bubble-text {
  margin: 0;
  font-size: 25px;
  font-weight: 800;
  line-height: 1.22;
  letter-spacing: -0.01em;
  text-align: center;
  color: var(--text-primary);
}
.greet-bubble-text .hl { color: var(--brand-blue); }

/* One span per character, laid out in full from the first frame and revealed in
   order by onboarding#typeGreeting. Hiding with opacity rather than adding the
   characters as they are typed is what keeps the fit-content bubble from
   resizing on every frame. The short transition softens the per-character step
   so the line reads as typed rather than as flashing on. */
.greet-char {
  opacity: 0;
  transition: opacity 0.12s linear;
}
.greet-char.is-typed { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .greet-char { opacity: 1; transition: none; }
}

/* The clipper spans the bubble's full width and the square is placed from the
   inline start, so the tail sits toward the leading edge rather than the middle
   and mirrors on its own in RTL — no percentage offset, no transform.

   11px tall against a 16px square rotated 45deg: pulling the square up by half
   its height leaves exactly its bottom corner inside the box, and the box crops
   everything else, including the two borders that would otherwise close the
   shape into a diamond. */
.greet-bubble-tail {
  position: absolute;
  bottom: -11px;
  inset-inline: 0;
  height: 11px;
  overflow: hidden;
}
.greet-bubble-tail i {
  display: block;
  width: 16px;
  height: 16px;
  margin-top: -8px;
  margin-inline-start: 30px;
  transform: rotate(45deg);
  background: var(--greet-bg);
  border-inline-end: 1px solid var(--greet-line);
  border-block-end: 1px solid var(--greet-line);
}

/* Clear air under the bubble so the tail reads as a tail, with the mascot
   answering from below rather than crowding into it. */
.greet-mascot {
  margin-top: 40px;
  margin-bottom: 0;
}

/* Entrance: bubble lands first, mascot answers it. */
.slide--greeting.active .greet-bubble {
  animation: greet-bubble-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.slide--greeting.active .greet-mascot {
  animation: mascot-enter 0.65s cubic-bezier(0.22, 1, 0.36, 1) 0.18s both;
}
@keyframes greet-bubble-in {
  from { opacity: 0; transform: scale(0.9) translateY(8px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .slide--greeting.active .greet-bubble,
  .slide--greeting.active .greet-mascot { animation: none; }
}

@media (max-width: 360px) {
  .greet-bubble-text { font-size: 22px; }
  .greet-bubble { padding: 16px 22px; }
}

/* ── Trial reminder · compact permission ask ─────────────────────────────────
   The notification permission moved onto this slide from the removed cadence
   screen. There it was the only action and filled the width; here the paywall
   CTA in the bottom nav is the primary action, so the same button is scaled
   down to a pill that sits under the bell without competing with it. Modifiers
   only — the base .reminder-* rules, the targets, and
   onboarding#enableNotifications are all unchanged. */
.reminder-enable-wrap--sm {
  gap: 6px;
  margin-top: 14px;
  max-width: 260px;
}
.reminder-enable-btn--sm {
  width: auto;
  gap: 8px;
  padding: 11px 20px;
  font-size: 14px;
}
.reminder-enable-wrap--sm .reminder-enable-hint {
  font-size: 12px;
}
.reminder-denied--sm {
  margin-top: 14px;
  max-width: 300px;
}

/* Confirmation takes the enable button's exact box — same pill, same padding,
   same 14px label — so it swaps in without the layout moving. The card
   treatment the full-size state uses (gradient, glow, 22px radius, 18px
   gradient-clipped title) is all overridden away. */
.reminder-enabled--sm {
  margin-top: 14px;
  width: auto;
  max-width: none;
  padding: 11px 20px;
  border-radius: 9999px;
  background: rgba(16, 185, 129, 0.10);
  border: 1px solid rgba(16, 185, 129, 0.22);
  box-shadow: none;
}
[data-theme="floosyo-dark"] .reminder-enabled--sm {
  background: rgba(16, 185, 129, 0.14);
  border-color: rgba(16, 185, 129, 0.26);
  box-shadow: none;
}
.reminder-enabled--sm .reminder-enabled-title {
  font-size: 14px;
  font-weight: 600;
  background: none;
  -webkit-text-fill-color: currentColor;
  color: var(--rem-emerald-deep);
}
[data-theme="floosyo-dark"] .reminder-enabled--sm .reminder-enabled-title {
  color: var(--rem-emerald);
}
.reminder-denied--sm .reminder-denied-desc { font-size: 12px; }

/* The anchored shell fades the last 56px of its scroll body to hint that more
   is below. Nothing on this slide scrolls, so the fade only ever dimmed the
   bottom of the permission button. Opt out. */
.slide--trial-reminder .slide-shell--anchored > .slide-body {
  -webkit-mask-image: none;
          mask-image: none;
}


