:root {
  --font-family: 'Aeonik-Regular', sans-serif;
  --color-1: #F2EDDA;
  --color-2: #E4E7E0;
  --color-3: #DCFF54;
  --color-4: #1D2A3A;
  --color-5: #EDF8FD;
  --color-6: #1A1F2A;
  --primary-purple-500: #9061F9;

  --bg-primary: 26, 31, 42;

  --link-color: #2688ff;

  --gray-1: #F2F2F2;
  --gray-2: #E6E6E6;

  --border-radius-1: 5px;
  --border-radius-2: 10px;
  --border-radius-3: 15px;

  --bold-font-weight: 600;


  /* OLD Colors: */
  /* --farmbank-green-100: #DEF7EC;
  --farmbank-green-200: #BCF0DA;
  --farmbank-teal: #409384;
  --farmbank-dark-teal: #156A60;
  --farmbank-light-teal: #6FADA6;
  --farmbank-yellow: #E7AE44;
  --form-label-color: #4B5563; */

  /* Updated 15 Dec 24*/
  --farmbank-green-100: #DEF7EC;
  --farmbank-green-200: #BCF0DA;
  --farmbank-dark-teal: #156A60;

  --farmbank-teal: #298D7B;
  --farmbank-light-teal: #3DA297;
  --farmbank-yellow: #DF9B1B;
  --form-label-color: #4B5563;

  --form-input-border-width-small-screens: 1px !important;
  --form-input-padding-small-screens: 0.5rem !important;
  --form-label-font-size: 1rem !important;

  /* Updated 31 May 24 */
  --farmbank-gold: #DB8931;
  --farmbank-dark-gold: #C37C2F;
  --farmbank-light-gold: rgba(219, 137, 49, 0.2);
  --farmbank-light-yellow: #EFE6B8;
  --farmbank-black: #1D1F26;
  --farmbank-dark-gray: #374151;
  --farmbank-light-gray: #D1D5DB;
  --farmbank-medium-gray: #6B7280;
  --farmbank-blue-500: #3F83F8;
  --form-label-font-weight: 300;
  --shiny-gold: linear-gradient(to bottom, rgba(255, 244, 232, 1), rgba(195, 124, 47, 1), rgba(29, 31, 38, 0));
  --shiny-gold-and-gray: linear-gradient(0deg, rgba(219, 219, 219, 0.20) 0%, rgba(219, 219, 219, 0.20) 100%), linear-gradient(109deg, #99A5D0 -7.3%, #EFE6B8 41.88%, #A0B8CA 91.06%);

  /* Motion contract (FRM-404, extended app-wide). Every animation in the
     product reads these values — no literal durations anywhere downstream. */
  --motion-fast: 120ms;   /* press, focus, tick, label crossfade */
  --motion-step: 240ms;   /* step entry WITHIN a screen */
  --motion-reward: 450ms; /* payoff beats: progress bar, code sweep, the minimum
                             time a check stays visible before its result */
  /* A whole screen replacing another has further to travel than a panel inside
     one, and is the only transition that also has to cover a navigation the
     member already knows they asked for — so it runs slightly longer than
     --motion-step rather than sharing it. */
  --motion-page: 340ms;   /* one screen replacing another */
  --motion-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  /* A screen travels the full width of the viewport, which is far enough that
     the generic ease reads as a slide that runs out of energy near the end.
     This one leaves fast and settles long, which is what makes a push feel
     carried rather than animated. */
  --motion-page-ease: cubic-bezier(0.32, 0.72, 0, 1);

  /* How far the liveness status ring stands off its icon. */
  --kyc-ring-gap: 6px;
}

/* font-display is the whole reason these three declarations are worth reading.

   Without it the property defaults to `auto`, which every engine implements as
   BLOCK: text in one of these families is painted as nothing at all until the
   font file resolves. Chrome's block period is ~100ms; WebKit's is three
   seconds. So on an iPhone, every figure and label on a screen is invisible for
   as long as the font request takes, and the member watches a page arrive with
   its furniture in place and its content missing. That is the "200ms where the
   rates and the tenors are not there yet" on the time deposit term step, and it
   is why the same screens feel slower on a phone than on the desk: the block is
   paid on a mobile network, not on localhost.

   `swap` gives the fallback face immediately and re-renders when the real one
   lands. Text is never invisible, whatever the network is doing.

   THE URLS ARE /fonts, NOT /assets, AND THAT IS THE OTHER HALF OF THE FIX.
   These pointed at /assets/Aeonik-*.otf, which was a 404 in development and in
   production alike: the files sat at the ROOT of app/assets, which is not an
   asset load path (Rails adds app/assets/* , the subdirectories) and was not
   linked from app/assets/config/manifest.js either. document.fonts reported
   Aeonik-Pro in state "error" on every page in the product — the whole app had
   been rendering in the sans-serif fallback, and paying WebKit's three-second
   block on every page load for a file that was never going to arrive.

   public/fonts rather than the asset pipeline, because this stylesheet is a
   STATIC file served straight out of public/ and never passes through
   Sprockets: it cannot call asset_path, so it cannot name a digested filename,
   so a precompiled font is unreachable from here by construction. /js and
   /stylesheets are already served this way; the fonts now sit beside them. */
@font-face {
  font-family: 'Aeonik-Pro';
  font-style: normal;
  font-weight: normal;
  font-display: swap;
  src: url("/fonts/Aeonik-Regular.otf");
}

@font-face {
  font-family: 'Aeonik-Pro-Light';
  font-weight: light;
  font-display: swap;
  src: url("/fonts/Aeonik-Light.otf");
}

@font-face {
  font-family: 'Aeonik-Pro-Bold';
  font-weight: bold;
  font-display: swap;
  src: url("/fonts/Aeonik-Bold.otf");
}

html {
  font-family: 'Aeonik-Pro', sans-serif;
}

.font-light {
  font-family: 'Aeonik-Pro-Light', sans-serif !important;
}

.font-bold {
  font-family: 'Aeonik-Pro-Bold', sans-serif !important;
}

.bg-gray-50 {
  background: #F9FAFB !important;
}

.text-gray-600 {
  color: #4B5563 !important;
}
.border-farmbank-md-gray {
  border-color: #9CA3AF;
}

.custom-button {
  background-color: var(--bg-callout);
  color: var(--bg-callout-text);
  border: 2px solid transparent;
}

.custom-button-minor {
  background-color: #fff;
  color: var(--text-dark);
  border: 2px solid transparent;
}

.compact-container {
  max-width: 1100px;
}

/* TOASTS */
.toasts-container {
  max-width: 300px;
  position: fixed;
  top: 5px;
  right: 10px;
}

.toasts-container .toast {
  transform: translateY(-120%);
  grid-template-rows: 1fr;
  display: grid;
  opacity: 0;
}

.toast .inner {
  background-color: #fff;
  width: 280px;
}

.toasts-container .toast-animate-in {
  transition: 0.5s;
  transform: translateY(0%);
  opacity: 1;
}

.toasts-container .toast-animate-out {
  transform: translateX(120%);
  transition: 0.5s;
  grid-template-rows: 0fr;
}

.from-below {
  transform: translateY(20%);
}

:focus-visible {
  outline: none
}

.focus-border:focus {
  border-color: #191919;
}

.border-3 {
  border-width: 3px;
}


shimmer,
.shimmer {
  background: #f6f7f8;
  background-image: linear-gradient(to right, #f6f7f8 0%, #edeef1 20%, #f6f7f8 40%, #f6f7f8 100%);
  background-repeat: no-repeat;
  background-size: 200% 100%;
  position: relative;

  -webkit-animation-duration: 1s;
  -webkit-animation-fill-mode: forwards;
  -webkit-animation-iteration-count: infinite;
  -webkit-animation-name: placeholderShimmer;
  -webkit-animation-timing-function: linear;
}


@-webkit-keyframes placeholderShimmer {
  0% {
    background-position: -468px 0;
  }

  100% {
    background-position: 468px 0;
  }
}


@media (min-width: 768px) {
  .md\:right-0 {
    right: 0px;
  }

  .md\:rounded-l-2xl {
    border-top-left-radius: 1rem;
    border-bottom-left-radius: 1rem;
    border-bottom-right-radius: 0px;
    border-top-right-radius: 0px;
  }
}

.tooltip {
  background-color: #333;
  color: white;
  padding: 5px 10px;
  border-radius: 4px;
  font-size: 13px;
}


.format-text h1 {
  font-size: 2.5rem;
  font-weight: var(--bold-font-weight);
  line-height: 1.2;
  margin-bottom: 1rem;
}

.format-text h2 {
  font-size: 2rem;
  font-weight: var(--bold-font-weight);
  line-height: 1.2;
  margin-bottom: 1rem;
}

.format-text h3 {
  font-size: 1.5rem;
  font-weight: var(--bold-font-weight);
  line-height: 1.2;
  margin-bottom: 1rem;
}

.format-text h4 {
  font-size: 1.25rem;
  font-weight: var(--bold-font-weight);
  line-height: 1.2;
  margin-bottom: 1rem;
}

.format-text a {
  color: var(--link-color);
  text-decoration: underline;
}

.format-text p,
.format-text li {
  font-size: 1.1rem;
}

.format-text code {
  font-size: 0.9rem;
}

.format-text p,
.format-text code {
  margin-bottom: 1rem;
}

.format-text strong {
  font-weight: var(--bold-font-weight);
}

.format-text ul {
  margin-bottom: 1rem;
  list-style-type: disc;
  margin-left: 20px;
}

.format-text ol {
  margin-bottom: 1rem;
  list-style-type: decimal;
  margin-left: 20px;
}

.format-text code,
.format-text pre code {
  background-color: var(--gray-1);
  border-radius: var(--border-radius-1);
}

.format-text code {
  padding: 2px 4px;
}

.format-text pre code {
  display: block;
  padding: 12px;
}

.bg-gray-50 {
  --tw-bg-opacity: 1;
  background-color: rgba(249, 250, 251, var(--tw-bg-opacity));
}

@media (min-width: 640px) {
  .sm\:left-0 {
    left: 0px;
  }

  .sm\:bg-gray-50 {
    --tw-bg-opacity: 1;
    background-color: rgba(249, 250, 251, var(--tw-bg-opacity));
  }
}

/* Form Styling */
form.farmbank-input-styling label {
  color: var(--form-label-color, #4B5563);
  font-weight: var(--form-label-font-weight);
}

form.farmbank-input-styling input:focus:not(.no-style, .invalid),
form.farmbank-input-styling select:focus:not(.no-style, .invalid) {
  outline: none !important;
  border-color: var(--farmbank-gold) !important;
}

form.farmbank-input-styling input:not(.no-style),
form.farmbank-input-styling select:not(.no-style) {
  border-radius: 0.5rem !important;
  border-width: 1px !important;
}

form.farmbank-input-styling input:not(.no-style).invalid,
form.farmbank-input-styling select:not(.no-style).invalid {
  background: #FDF2F2 !important;
  border-color: #C81E1E !important;
}

form.farmbank-input-styling input:not(.no-style)::placeholder,
form.farmbank-input-styling select:not(.no-style)::placeholder {
  font-weight: 300;
}

input.custom-file-input::file-selector-button {
  display: none;
}

@media (min-width: 640px) {
  .otp-input-field {
    padding-top: 40px !important;
    padding-bottom: 40px !important;
  }
}

/* Custom Borders */

.border-farmbank-teal {
  border-color: var(--farmbank-teal);
}

.border-farmbank-green-200 {
  border-color: var(--farmbank-green-200);
}

.border-farmbank-light-yellow {
  border-color: var(--farmbank-light-yellow);
}

.border-farmbank-yellow {
  border-color: var(--farmbank-yellow);
}

/* Custom Font Colors */
.font-farmbank-teal {
  color: var(--farmbank-teal);
}

.font-farmbank-green-200 {
  color: var(--farmbank-green-200);
}

.font-farmbank-light-yellow {
  color: var(--farmbank-light-yellow);
}

.font-farmbank-blue-500 {
  color: var(--farmbank-blue-500);
}

.font-farmbank-dark-teal {
  color: var(--farmbank-dark-teal);
}

.font-farmbank-dark-gray {
  color: var(---farmbank-dark-gray);
}

.font-farmbank-yellow {
  color: var(--farmbank-yellow);
}

.font-farmbank-light-teal {
  color: var(--farmbank-light-teal);
}

.font-farmbank-black {
  color: var(--farmbank-black);
}

.font-farmbank-gold {
  color: var(--farmbank-gold);
}

.font-farmbank-dark-gold {
  color: var(--farmbank-dark-gold);
}

.font-farmbank-md-gray {
  color: var(--farmbank-medium-gray);
}

/* Custom Backgrounds */
.bg-farmbank-green-100 {
  background: var(--farmbank-green-100);
}

.bg-farmbank-green-200 {
  background: var(--farmbank-green-200);
}

.bg-farmbank-teal {
  background: var(--farmbank-teal);
}

.bg-farmbank-dark-teal {
  background: var(--farmbank-dark-teal);
}

.bg-farmbank-yellow {
  background: var(--farmbank-yellow);
}

.bg-farmbank-gold {
  background: var(--farmbank-gold);
}

.bg-farmbank-light-gold {
  background: var(--farmbank-light-gold);
}

.bg-farmbank-dark-gray {
  background: var(--farmbank-dark-gray);
}

.bg-farmbank-light-gray {
  background: var(--farmbank-light-gray);
}

.bg-primary-purple-500 {
  background: var(--primary-purple-500);
}

.bg-farmbank-light-teal {
  background: var(--farmbank-light-teal);
}

.bg-dark-radial {
  background: radial-gradient(50% 50% at 50% 50%, #463725 0%, #1C1E25 100%)
}

.bg-light-radial {
  background: radial-gradient(50% 50% at 50% 50%, rgba(255, 247, 238, 0.50) 0%, rgba(237, 239, 240, 0.50) 58%);
}

.bg-shiny-gray-gold {
  background: var(--shiny-gold-and-gray);
}

.grid-cols-16 {
  grid-template-columns: repeat(16, minmax(0, 1fr));
}

.input-container.invalid {
  border-color: rgba(239, 68, 68);
}

.translate-x-5 {
  --tw-translate-x: 1.25rem;
}

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

.pie {
  --p: 20;
  --b: 22px;
  --w: 150px;
  --m: #EDEFF0;
  --q: 0.867195;

  width: var(--w);
  aspect-ratio: 1;
  position: relative;
  display: inline-grid;
  margin: 5px;
  place-content: center;
  font-size: 25px;
  font-weight: bold;
  font-family: sans-serif;
}

.pie:before,
.pie:after {
  content: "";
  position: absolute;
  border-radius: 50%;
}

.pie:before {
  inset: 0;
  background: radial-gradient(farthest-side, rgba(255, 255, 255, 0) 98%, var(--m)) top / var(--b) var(--b) no-repeat,
    conic-gradient(#f7cfa3 0, var(--farmbank-dark-gold) calc(var(--p) / 100* 15%), rgb(250 168 0 / 58%) calc(var(--p) / 100* 35%), var(--farmbank-dark-gold) calc(var(--p) / 100 * 65%), #f7cfa3 calc(var(--p) / 100 * 100%), var(--m) 0%);
  -webkit-mask: radial-gradient(farthest-side, #0000 calc(99% - var(--b)), #000 calc(100% - var(--b)));
  mask: radial-gradient(farthest-side, #0000 calc(99% - var(--b)), #000 calc(100% - var(--b)));
}

.pie:after {
  inset: calc(50% - var(--b)/2);
  background: var(--farmbank-gold);
  transform: rotate(calc(var(--p)*3.6deg)) translateY(calc(50% - var(--w)/2));
}

.animate {
  animation: p 1s .5s both;
}

.no-round:before {
  background-size: 0 0, auto;
}

.no-round:after {
  content: none;
}

@keyframes p {
  from {
    --p: 0
  }
}

/* Mobile hamburger menu */
@media (min-width: 640px) {
  .hamburger {
    display: none !important;
  }
}

.hamburger {
  display: inline-block;
}

.hamburger a.main-nav-toggle {
  display: block;
  width: 28px;
  height: 16px;
}

.hamburger a.main-nav-toggle:after,
.hamburger a.main-nav-toggle:before {
  content: '';
  position: absolute;
  top: 0;
  height: 0;
  border-bottom: 2px solid #fff;
  width: 100%;
  left: 0;
  right: 0;
  transition: all ease-out 0.3s;
}

.hamburger a.main-nav-toggle:after {
  top: 100%;
}

.hamburger a.main-nav-toggle i {
  display: block;
  text-indent: 100%;
  overflow: hidden;
  white-space: nowrap;
  height: 2px;
  background-color: #fff;
  width: 100%;
  position: absolute;
  top: 50%;
  transition: all ease-out 0.1s;
}

.hamburger a.main-nav-toggle.active-menu:after {
  transform: rotate(-45deg);
  transform-origin: center;
  top: 50%;
}

.hamburger a.main-nav-toggle.active-menu:before {
  transform: rotate(45deg);
  transform-origin: center;
  top: 50%;
}

.hamburger a.main-nav-toggle.active-menu i {
  opacity: 0;
}

/* Cookie policy */
#farmbank-cookie-policy {
  z-index: 100;
}

@media (min-width: 640px) {
  #default-modal-inner {
    max-height: 85%;
  }
}

.hover\:text-black:hover {
  color: black;
}

.stepper-gradient {
  background: linear-gradient(45deg, #EFE6B8, var(--farmbank-gold));
}

.top-1\/2 {
  top: 50%;
}

.-translate-y-1\/2 {
  transform: translateY(-50%)
}

#funds-success-page-container {
  border: 4px solid transparent;
  background: linear-gradient(to right, black, black),
    linear-gradient(to bottom right, rgba(255, 244, 232, 1), rgba(195, 124, 47, 1), rgba(29, 31, 38, 0));
  background-clip: padding-box, border-box;
  background-origin: padding-box, border-box;
}

.deposit-source-label .check-circle,
.term-length-label .check-circle {
  display: none;
}

.deposit-source-label.active .check-circle,
.term-length-label.active .check-circle {
  display: block;
}

/* Chrome, Safari, Edge, Opera */
input[type=number]::-webkit-outer-spin-button,
input[type=number]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* Firefox */
input[type=number] {
  -moz-appearance: textfield;
}

/* Didit liveness disclosure: make the Privacy Policy link a conventional,
   accessible gold hyperlink matching the Farmbank brand. Scoped to this class
   only — no global link change. */
.disclosure-link {
  color: var(--farmbank-gold);
  /* text-decoration: underline; */
}

.disclosure-link:hover {
  color: var(--farmbank-dark-gold);
  text-decoration: underline;
}

.disclosure-link:focus-visible {
  outline: 2px solid var(--farmbank-gold);
  outline-offset: 2px;
}

/* ── Utility missing from the pinned static Tailwind 3.3 build ──
   The served tailwind-full.css is a static, content-purged file that is not
   rebuilt on deploy, so newly-used utility classes have no generated rule.
   Used by the multi-step add-funds forms so the full-height mobile layout
   tracks the visible viewport. Loaded after tailwind-full.css. */

/* Dynamic viewport height — tracks the visible area on mobile (excludes the
   browser's collapsing toolbar). vh fallback first for browsers without dvh. */
.min-h-\[100dvh\] {
  min-height: 100vh;
  min-height: 100dvh;
}

/* Bottom padding that clears the iOS home indicator / Safari bottom bar.
   dvh measures the full viewport INCLUDING the strip behind the home indicator,
   so a bottom-pinned button needs the safe-area inset added on top of its
   normal 2rem (py-8) padding. Requires viewport-fit=cover on the viewport meta
   (see shared/_head.html.erb); the inset is 0 on devices without a safe area,
   so this equals a plain pb-8 on desktop and older phones. */
.pb-safe-8 {
  padding-bottom: 2rem;
  padding-bottom: calc(2rem + env(safe-area-inset-bottom));
}

/* ---------------------------------------------------------------------------
   OTP code boxes (settings: edit contact details, two-stage verify screen).

   Separate from .otp-input-field on purpose: that class carries a
   `padding: 40px !important` rule the SIGN-IN OTP screen depends on, which
   cannot be overridden by a utility class.
   --------------------------------------------------------------------------- */

/* tonic23.css styles EVERY non-.no-style form input with
     form:not(.no-style) input:not(.no-style)          { height: 50px }
     form:not(.no-style) [type="text"]:not(.no-style)  { width: 100%; padding: .5rem 0 }
   at specificity (0,3,1), which outranks any single Tailwind class (0,1,0).
   This selector is (0,3,2) so it wins, without needing .no-style — which would
   also opt the boxes out of the gold focus ring and the .invalid error styling.
   Below sm the boxes FILL the row (flex + aspect-ratio) so they scale with the
   device instead of being pinned narrow on a large phone. */
/* :not(.otp-input-field) is what lets a caller opt these boxes into the SIGN-IN
   shape instead (box_style: :signin in settings/_otp_input). Both classes are on
   the element then — otp-code-box because it is the JS selector and the motion
   hook, otp-input-field because it carries the shape — and without this guard
   the square sizing here would win on specificity and the two would fight. */
form:not(.no-style) input.otp-code-box:not(.no-style):not(.otp-input-field),
.otp-code-box-ghost {
  flex: 1 1 0;
  min-width: 0;
  width: auto;
  height: auto;
  aspect-ratio: 1 / 1;
  padding: 0;
}

.otp-code-row {
  display: flex;
  flex-direction: row;
  gap: 0.25rem;
}

/* The sign-in row, which is a plain flex row whose boxes share the width it has
   — NOT six fixed-width boxes. Pinning them to 3.5rem each was tried and
   overflows: six of those plus five gaps is 376px against a 375px phone.
   Nothing sizes them here for the same reason nothing sizes them on the sign-in
   screen: the shared form input rules already do it, and the square variant
   above is excluded from these by :not(.otp-input-field). Only the gap, which
   stands in for the mr-2 the sign-in boxes carry. */
.otp-code-row--signin {
  gap: 0.5rem;
}

/* Share the row, the way the sign-in boxes do. They manage it by accident —
   their w-14 loses to the shared form rule's width:100% and six equal items
   shrink to fit — but "by accident" needs `min-width: 0` to be reliable, or a
   flex item's automatic minimum size stops it shrinking and the sixth box ends
   up off the edge. Stated explicitly here rather than inherited from the square
   rule above, which this variant is deliberately excluded from. */
.otp-code-row--signin input.otp-code-box {
  flex: 1 1 0;
  min-width: 0;
  width: auto;
}

/* custom.css sets `form.farmbank-input-styling label { color; font-weight }` at
   (0,2,1) with NO :not(.no-style) escape hatch, so utility classes on the
   heading were discarded and it rendered light grey at weight 300. (0,2,2). */
form.farmbank-input-styling label.otp-code-heading {
  color: #111827;
  font-weight: 600;
}

@media (min-width: 640px) {
  /* Plenty of room above sm, so stop filling and pin a comfortable size —
     left to flex the boxes balloon to ~73px on the desktop card. */
  form:not(.no-style) input.otp-code-box:not(.no-style):not(.otp-input-field),
  .otp-code-box-ghost {
    flex: 0 0 auto;
    width: 3.5rem;
    height: 3.5rem;
    aspect-ratio: auto;
  }

  .otp-code-row {
    gap: 0.5rem;
  }

  /* S11, the unlock code card: fill the row like the sub-sm rule — six equal
     boxes with the 0.5rem gap span the card (ticket S11 proportion). Scoped:
     the settings card keeps the pinned 3.5rem sizing above. */
  .account-unlock-code form:not(.no-style) input.otp-code-box:not(.no-style),
  .account-unlock-code .otp-code-box-ghost {
    flex: 1 1 0;
    min-width: 0;
    width: auto;
    height: auto;
    aspect-ratio: 1 / 1;
  }
}
/* ---------------------------------------------------------------------------
   Onboarding motion (FRM-404)

   Everything here reads the --motion-* contract at the top of this file. Two
   rules hold throughout: animate transform/opacity only, and the reduced-motion
   block at the bottom of this section must stay in step with it.

   The wizard's Next control is an <input type="submit">, which can hold neither
   child elements nor pseudo-elements — so its spinner and shimmer are painted
   as backgrounds, the same technique image-resize.js already uses for its own
   loading state. The progress bar's width and that background sweep are the
   only two non-transform animations, both on lone elements with no siblings.
   --------------------------------------------------------------------------- */

/* Step entry ------------------------------------------------------------- */

/* A CSS animation on the element itself, not a JS-applied class: a class added
   after parse paints one frame at the offset position first, which reads as a
   jump. Re-runs on every htmx swap too, because the node is newly inserted. */
.kyc-step-enter {
  animation: kyc-step-in var(--motion-step) var(--motion-ease) both;
}

@keyframes kyc-step-in {
  from { opacity: 0; transform: translateX(16px); }
  to   { opacity: 1; transform: translateX(0); }
}

/* The other half of the step change: htmx holds the outgoing form for the swap
   delay with this class on it, which is the window the exit animation runs in.
   Without an exit the incoming slide reads as a flicker rather than a move. */
.htmx-swapping#kyc-onboarding-form {
  opacity: 0;
  transform: translateX(-16px);
  transition: opacity var(--motion-step) linear,
              transform var(--motion-step) var(--motion-ease);
  /* Normally zero. The accepted-code sweep is the one thing that has to finish
     before the card leaves — otherwise the exit fade runs straight over the top
     of it and the green is never actually seen. motion.js sets this
     to the sweep's length for that one case, and widens the swap delay to match. */
  transition-delay: var(--kyc-exit-delay, 0ms);
}

/* A rejected save re-renders the SAME step, so it does not animate in AT ALL.
   Even a fade reads as the section blinking out and reloading, which is the
   exact thing this whole change set exists to remove — the errors and the
   digits simply appear where they already were. */
.kyc-swap-in-place .kyc-step-enter {
  animation: none;
}

/* Nothing leaves, so nothing fades out. Emptying the card and refilling it is
   what a reload looks like, which is the whole thing being replaced here. */
.kyc-swap-in-place .htmx-swapping#kyc-onboarding-form {
  opacity: 1;
  transform: none;
  transition: none;
}


/* Progress bar ----------------------------------------------------------- */

/* Rendered at the previous step's width; motion.js flips it to
   data-to after a frame so the travel is actually seen. The gold dot is a child
   pinned to the trailing edge, so it rides along for free. */
.kyc-progress-fill {
  transition: width var(--motion-reward) var(--motion-ease);
}

/* Submit feedback -------------------------------------------------------- */

.kyc-cta-press {
  transform: scale(0.975);
  transition: transform var(--motion-fast) var(--motion-ease);
}

.kyc-cta-busy {
  pointer-events: none;
  cursor: default;
}

.kyc-cta-spin {
  color: transparent !important;
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'24'%20height%3D'24'%20viewBox%3D'0%200%2024%2024'%3E%3Ccircle%20cx%3D'12'%20cy%3D'12'%20r%3D'9'%20fill%3D'none'%20stroke%3D'rgba(255%2C255%2C255%2C0.35)'%20stroke-width%3D'3'%2F%3E%3Cpath%20fill%3D'none'%20stroke%3D'%23ffffff'%20stroke-width%3D'3'%20stroke-linecap%3D'round'%20d%3D'M12%203a9%209%200%200%201%209%209'%3E%3CanimateTransform%20attributeName%3D'transform'%20type%3D'rotate'%20from%3D'0%2012%2012'%20to%3D'360%2012%2012'%20dur%3D'0.7s'%20repeatCount%3D'indefinite'%2F%3E%3C%2Fpath%3E%3C%2Fsvg%3E");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 24px 24px;
  /* these buttons carry Tailwind's `transition-all`, which would otherwise
     animate background-position in from the top-left corner */
  transition: none;
}

/* Layered under the spinner: a slow highlight crossing the button, for waits
   long enough that a still spinner starts to read as a hang. */
.kyc-cta-shimmer {
  color: transparent !important;
  transition: none;
  background-image:
    url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'24'%20height%3D'24'%20viewBox%3D'0%200%2024%2024'%3E%3Ccircle%20cx%3D'12'%20cy%3D'12'%20r%3D'9'%20fill%3D'none'%20stroke%3D'rgba(255%2C255%2C255%2C0.35)'%20stroke-width%3D'3'%2F%3E%3Cpath%20fill%3D'none'%20stroke%3D'%23ffffff'%20stroke-width%3D'3'%20stroke-linecap%3D'round'%20d%3D'M12%203a9%209%200%200%201%209%209'%3E%3CanimateTransform%20attributeName%3D'transform'%20type%3D'rotate'%20from%3D'0%2012%2012'%20to%3D'360%2012%2012'%20dur%3D'0.7s'%20repeatCount%3D'indefinite'%2F%3E%3C%2Fpath%3E%3C%2Fsvg%3E"),
    linear-gradient(100deg, transparent 20%, rgba(255, 255, 255, 0.26) 50%, transparent 80%);
  background-repeat: no-repeat, no-repeat;
  background-position: center, -100% 0;
  background-size: 24px 24px, 200% 100%;
  animation: kyc-cta-sweep 1.15s linear infinite;
}

@keyframes kyc-cta-sweep {
  to { background-position: center, 200% 0; }
}

/* A white spinner disappears on a white button. Secondary controls in this
   flow ("Not now") are outlined on the card, so they get the dark arc. */
.kyc-cta-spin.kyc-cta-on-light,
.kyc-cta-shimmer.kyc-cta-on-light {
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'24'%20height%3D'24'%20viewBox%3D'0%200%2024%2024'%3E%3Ccircle%20cx%3D'12'%20cy%3D'12'%20r%3D'9'%20fill%3D'none'%20stroke%3D'rgba(55%2C65%2C81%2C0.25)'%20stroke-width%3D'3'%2F%3E%3Cpath%20fill%3D'none'%20stroke%3D'%23374151'%20stroke-width%3D'3'%20stroke-linecap%3D'round'%20d%3D'M12%203a9%209%200%200%201%209%209'%3E%3CanimateTransform%20attributeName%3D'transform'%20type%3D'rotate'%20from%3D'0%2012%2012'%20to%3D'360%2012%2012'%20dur%3D'0.7s'%20repeatCount%3D'indefinite'%2F%3E%3C%2Fpath%3E%3C%2Fsvg%3E");
}

.kyc-cta-shimmer.kyc-cta-on-light {
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'24'%20height%3D'24'%20viewBox%3D'0%200%2024%2024'%3E%3Ccircle%20cx%3D'12'%20cy%3D'12'%20r%3D'9'%20fill%3D'none'%20stroke%3D'rgba(55%2C65%2C81%2C0.25)'%20stroke-width%3D'3'%2F%3E%3Cpath%20fill%3D'none'%20stroke%3D'%23374151'%20stroke-width%3D'3'%20stroke-linecap%3D'round'%20d%3D'M12%203a9%209%200%200%201%209%209'%3E%3CanimateTransform%20attributeName%3D'transform'%20type%3D'rotate'%20from%3D'0%2012%2012'%20to%3D'360%2012%2012'%20dur%3D'0.7s'%20repeatCount%3D'indefinite'%2F%3E%3C%2Fpath%3E%3C%2Fsvg%3E"),
    linear-gradient(100deg, transparent 20%, rgba(55, 65, 81, 0.10) 50%, transparent 80%);
}

/* Code entry ------------------------------------------------------------- */

/* The checking state. Staggered so the pulse reads as a wave travelling left to
   right across the code rather than six boxes breathing in unison — it is the
   difference between "something is happening" and "the page is idle". Held on
   screen for a guaranteed minimum by motion.js, because a fast
   server would otherwise replace it before it is seen at all. */
/* One cycle per --motion-reward, which is also the minimum time the checking
   state stays on screen — so the bounce PEAKS inside the window instead of
   after it. At the old 1s cycle the peak landed at 500ms and the boxes were
   gone by 450ms: the animation ran perfectly and was never once seen. */
.otp-input-field.kyc-otp-verifying,
.otp-code-box.kyc-otp-verifying {
  animation: kyc-otp-pulse var(--motion-reward) var(--motion-ease) infinite;
}

.otp-input-field.kyc-otp-verifying:nth-of-type(2),
.otp-code-box.kyc-otp-verifying:nth-of-type(2) { animation-delay: 60ms; }
.otp-input-field.kyc-otp-verifying:nth-of-type(3),
.otp-code-box.kyc-otp-verifying:nth-of-type(3) { animation-delay: 120ms; }
.otp-input-field.kyc-otp-verifying:nth-of-type(4),
.otp-code-box.kyc-otp-verifying:nth-of-type(4) { animation-delay: 180ms; }
.otp-input-field.kyc-otp-verifying:nth-of-type(5),
.otp-code-box.kyc-otp-verifying:nth-of-type(5) { animation-delay: 240ms; }
.otp-input-field.kyc-otp-verifying:nth-of-type(6),
.otp-code-box.kyc-otp-verifying:nth-of-type(6) { animation-delay: 300ms; }

@keyframes kyc-otp-pulse {
  0%, 100% { transform: translateY(0) scale(1); border-color: #E5E7EB; }
  50%      { transform: translateY(-3px) scale(1.06); border-color: var(--farmbank-gold); }
}

/* Accepted: a staggered sweep left to right, so the code reads as taken rather
   than as six boxes changing at once. Runs during the swap delay, while the
   boxes are still the ones the applicant typed into. */
/* Specific enough to outrank the .invalid error styling (0,3,2 with
   !important) that the server re-renders these boxes with. The class is also
   stripped in JS; this is so a corrected code can never come out red even if
   that ever stops happening. */
.otp-input-field.kyc-otp-accepted,
.otp-code-box.kyc-otp-accepted,
form.farmbank-input-styling input.otp-input-field.kyc-otp-accepted:not(.no-style) {
  /* LANDS IN ONE FRAME, and on the sign-in screen that is the whole point.
     These inputs carry `transition: border .5s` from the shared input styling,
     so the border colour set here does not arrive when it is set — it sets off
     towards teal over half a second. The background is not transitioned and
     lands immediately, which is how this was caught: an accepted code on sign
     in was captured into the outgoing view-transition snapshot with the green
     background already on and the border still the red of the attempt before
     it. A snapshot is a single frame; anything it has to show has to be true
     in that frame. */
  transition: none !important;
  border-color: var(--farmbank-teal) !important;
  background-color: #ECFDF5 !important;
  animation: kyc-otp-sweep var(--motion-step) var(--motion-ease) both;
}

.kyc-otp-status-ok {
  color: var(--farmbank-teal) !important;
  font-weight: 500;
}

.otp-input-field.kyc-otp-accepted:nth-of-type(2) { animation-delay: 25ms; }
.otp-input-field.kyc-otp-accepted:nth-of-type(3) { animation-delay: 50ms; }
.otp-input-field.kyc-otp-accepted:nth-of-type(4) { animation-delay: 75ms; }
.otp-input-field.kyc-otp-accepted:nth-of-type(5) { animation-delay: 100ms; }
.otp-input-field.kyc-otp-accepted:nth-of-type(6) { animation-delay: 125ms; }

@keyframes kyc-otp-sweep {
  0%   { transform: translateY(0) scale(1); }
  45%  { transform: translateY(-4px) scale(1.05); }
  100% { transform: translateY(0) scale(1); }
}

/* Rejected: the group shakes where it stands. The digits are put back and the
   caret returns to the first box, so a typo costs one keystroke, not six. */
.otp-input-field.kyc-otp-rejected,
.otp-code-box.kyc-otp-rejected {
  animation: kyc-otp-shake var(--motion-step) var(--motion-ease) 2;
}

/* Four crossings rather than two, and a wider swing at the start that settles.
   The old curve spent both of its iterations at the same +/-5px, which on a
   phone-width row of six boxes is a slow lean each way rather than a shake —
   easy to miss entirely if you blinked, and the report was that there was no
   wiggle at all. Starting wide and decaying is what reads as a refusal: the
   movement is the message, so it has to be legible in the first 100ms. */
@keyframes kyc-otp-shake {
  0%, 100% { transform: translateX(0); }
  15%      { transform: translateX(-9px); }
  35%      { transform: translateX(7px); }
  55%      { transform: translateX(-4px); }
  80%      { transform: translateX(2px); }
}

/* Success celebration ----------------------------------------------------- */

/* Fixed to the viewport and inert to pointers, so for the few seconds it is
   alive it can never cover or swallow the action the member still has to take
   underneath it. overflow:hidden is what lets the pieces start above the top
   edge and leave past the bottom one without ever being seen to appear or
   disappear. */
.fb-confetti-layer {
  position: fixed;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 60;
  /* Gives the flutter below something to rotate in. Without a perspective the
     rotateX/rotateY read as a flat squash rather than paper turning over. */
  perspective: 700px;
}

/* Two elements per piece, and the split is what makes it read as paper rather
   than as a falling brick: the OUTER travels (down the screen, drifting
   sideways) and the INNER tumbles on its own axis at its own rate. One element
   cannot do both, because a single transform would have to interpolate the
   fall and the spin together and the piece ends up orbiting its start point.

   Everything per-piece arrives as a custom property from motion.js, so all of
   them animate off these two rules. */
.fb-confetti {
  position: absolute;
  top: 0;
  left: 0;
  will-change: transform;
  /* `both`, not `forwards`. Every piece is staggered, and a fill mode that
     does not include `backwards` leaves an element with NO animated style
     until its delay elapses — so each waiting piece sat at its static
     top:0/left:0 origin, stacked in the top left corner, in plain sight. At
     800ms into the fall that was 109 of the 166 pieces on screen piled in one
     corner. `backwards` puts them at the `from` keyframe instead, which is
     above the top edge and clipped, where they belong. */
  animation: fbConfettiFall var(--fb-fall, 3000ms) linear var(--fb-delay, 0ms) both;
}

.fb-confetti > i {
  display: block;
  width: var(--fb-w, 9px);
  height: var(--fb-h, 15px);
  background: var(--fb-colour, #DB8931);
  border-radius: var(--fb-radius, 1px);
  will-change: transform;
  animation: fbConfettiFlutter var(--fb-spin, 900ms) linear var(--fb-delay, 0ms) infinite backwards;
}

/* Starts a little above the top edge and ends a little below the bottom one,
   both outside the clip, so a piece is never seen to pop into or out of
   existence — it simply falls through the screen. Linear on purpose: gravity
   at this scale reads as a constant drift, and an eased fall looks like the
   pieces are being placed. */
@keyframes fbConfettiFall {
  from { transform: translate3d(var(--fb-x, 0px), -15vh, 0); }
  to   { transform: translate3d(calc(var(--fb-x, 0px) + var(--fb-drift, 0px)), 115vh, 0); }
}

@keyframes fbConfettiFlutter {
  from {
    transform: rotateZ(var(--fb-tilt, 0deg)) rotateX(0deg) rotateY(0deg);
  }
  to {
    transform: rotateZ(calc(var(--fb-tilt, 0deg) + var(--fb-rz, 360deg)))
               rotateX(var(--fb-rx, 360deg))
               rotateY(var(--fb-ry, 360deg));
  }
}

/* Step panels in the single-document flows --------------------------------- */

/* Add funds and time deposits already hold every step in one document and
   swap them by toggling .hidden — no page load, but no transition either, so
   a step change read as the screen refreshing. A CSS animation, because the
   panel comes from display:none and no transition can follow that.

   Same direction as the wizard's step entry, so a step change feels the same
   wherever it happens. */
/* Driven by a class motion.js puts on, NOT by `:not(.hidden)`.

   Keyed off the hidden state, this rule matched from the moment the document
   was parsed — so the panel a screen OPENS on animated in from opacity 0 on
   every page load, underneath the page transition that was already carrying the
   whole screen in. The member arrived on a screen holding its header and a
   blank where its content goes, for the length of --motion-step. On the time
   deposit term step that blank is the tenors and the rates, which is the one
   part of that screen anybody is there to read.

   The entry belongs to a step CHANGE, which is a thing that happens after the
   document exists. A class is how this rule learns the difference: bindStepPanelEntry
   in motion.js watches the panels for hidden -> visible and adds it, and an
   observer started at DOMContentLoaded cannot see the initial state. So the
   first panel is simply there, and every panel after it arrives. */
[data-step-panel].fb-step-entering {
  animation: fb-step-panel-in var(--motion-step) var(--motion-ease) both;
}

/* Steps travel the way the member is travelling. The panel used to arrive from
   16px to its right whichever direction it was going, so Back played the same
   small nudge as Next and the flow had no sense of place: every step, forward or
   backward, looked like the same thing happening again.

   These two say which way. bindStepPanelEntry works the direction out from
   DOCUMENT ORDER — the panels sit in the markup in flow order, so a panel that
   comes after the one being hidden is a step forward and one that comes before
   is a step back — which needs nothing added to any of the views.

   Further than the old 16px, because this is the movement that replaces the
   whole-page slide these flows used to do on their mid-flow navigation, and it
   has to carry the same meaning on its own. Still a fraction of the width
   rather than the whole of it: the header and the progress bar above are
   staying put, and content that flies the full width past a fixed frame reads
   as a glitch rather than as a page turn. */
[data-step-panel].fb-step-entering.fb-step-forward {
  animation-name: fb-step-panel-forward;
}

[data-step-panel].fb-step-entering.fb-step-back {
  animation-name: fb-step-panel-back;
}

@keyframes fb-step-panel-forward {
  from { opacity: 0; transform: translateX(40px); }
  to   { opacity: 1; transform: translateX(0); }
}

@keyframes fb-step-panel-back {
  from { opacity: 0; transform: translateX(-40px); }
  to   { opacity: 1; transform: translateX(0); }
}

/* The bar travels between step widths instead of being redrawn at the new one.
   Every step panel renders its own form_stepper, so each bar is already sitting
   at its own width when the panel is revealed — there was nothing to animate
   and the progress simply cut. bindStepPanelEntry hands the incoming bar the
   width the outgoing one was showing, which gives this transition something to
   run from. Same duration as the KYC wizard's bar, because it is the same
   gesture. */
.stepper-gradient {
  transition: width var(--motion-reward) var(--motion-ease);
}

@keyframes fb-step-panel-in {
  from { opacity: 0; transform: translateX(16px); }
  to   { opacity: 1; transform: translateX(0); }
}

/* The wizard animates inside an overflow-hidden scroll container; these panels
   are full width with nothing clipping them, so a panel offset to the right
   would widen the page and flash a horizontal scrollbar for the length of the
   animation. `clip` rather than `hidden` on purpose: hidden would make these
   forms scroll containers, which changes how the long steps inside them
   behave. Unsupported browsers fall back to a transient scrollbar, not a
   broken layout. */
#add-funds-form,
#new-time-deposit-form {
  overflow-x: clip;
}

/* Summary sections ------------------------------------------------------- */

/* The panel's height is animated in JS across the Edit/Cancel toggle. This is
   the other half: the newly revealed side fades up as it arrives, so the
   content does not pop into the space the height just opened. A CSS animation,
   not a transition — the panel goes from display:none, which no transition can
   follow. */
[data-summary-panel] > div:not(.hidden) {
  animation: kyc-summary-reveal var(--motion-step) var(--motion-ease) both;
}

@keyframes kyc-summary-reveal {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Upload feedback -------------------------------------------------------- */

/* image-resize.js locks the picker while its photo is being processed; the
   "Choose file" button is the div right after the input. */
input.dropzone[data-kyc-resizing],
input.dropzone[data-kyc-resizing] + div {
  opacity: 0.5;
  cursor: not-allowed;
}

.kyc-upload-card {
  display: none;
  /* the field sits inside `flex flex-col items-start` containers, which would
     otherwise shrink the card to its content width */
  width: 100%;
  align-self: stretch;
  box-sizing: border-box;
  align-items: center;
  gap: 0.625rem;
  margin-top: 0.75rem;
  padding: 0.5rem;
  border: 1px solid var(--farmbank-light-gray);
  border-radius: 0.5rem;
  opacity: 0;
  transform: scale(0.94);
}

.kyc-upload-card.is-shown {
  display: flex;
  opacity: 1;
  transform: scale(1);
  transition: opacity var(--motion-step) var(--motion-ease),
              transform var(--motion-step) var(--motion-ease);
}

.kyc-upload-thumb {
  flex: none;
  width: 42px;
  height: 42px;
  border-radius: 4px;
  object-fit: cover;
  background: var(--gray-2);
}

.kyc-upload-meta {
  display: block;
  flex: 1 1 auto;
  min-width: 0;
}

.kyc-upload-name {
  display: block;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--farmbank-black);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.kyc-upload-size {
  display: block;
  font-size: 0.6875rem;
  color: var(--farmbank-medium-gray);
}

/* Only meaningful while the downscale is running — an empty track sitting
   under a finished upload is noise. Determinate byte progress arrives with the
   htmx conversion; until then this state is the only one the bar has. */
.kyc-upload-bar {
  display: none;
  height: 4px;
  margin-top: 0.375rem;
  border-radius: 3px;
  background: var(--gray-2);
  overflow: hidden;
}

.kyc-upload-bar > i {
  display: block;
  width: 34%;
  height: 100%;
  border-radius: 3px;
  background: var(--farmbank-gold);
  transform: translateX(-100%);
}

.kyc-upload-bar.is-working,
.kyc-upload-bar.is-uploading { display: block; }

/* Determinate: real bytes from htmx:xhr:progress, never a fake. */
.kyc-upload-bar.is-uploading > i {
  transform: none;
  width: 0%;
  transition: width var(--motion-fast) linear;
}

.kyc-upload-bar.is-working > i {
  animation: kyc-upload-indeterminate 1.05s var(--motion-ease) infinite;
}

@keyframes kyc-upload-indeterminate {
  to { transform: translateX(300%); }
}

/* The liveness success check in miniature: a ring around a stroked tick on a
   mint field, not a filled dot with a glyph in it. Success reads the same here
   as it does on the verification screen. */
.kyc-upload-tick {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 4px;
  border-radius: 50%;
  border: 1.5px solid var(--farmbank-teal);
  background: #ECFDF5;
  color: var(--farmbank-teal);
  transform: scale(0);
}

.kyc-upload-tick svg {
  width: 100%;
  height: 100%;
}

/* The one spring in the flow — reserved for the moment something succeeded. */
.kyc-upload-tick.is-shown {
  transform: scale(1);
  transition: transform var(--motion-step) cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* Liveness wait ---------------------------------------------------------- */

/* One visual language across every state of the check, so the screen always
   says which of them it is in. A ring around the status icon: turning while
   the answer is genuinely in flight, settled and coloured once it is not.
   Keyed off the data-state the poll already publishes, so there is nothing to
   keep in sync — including the feature-off screen, which carries the same
   approved state rather than imitating it. */
.kyc-liveness-icon {
  position: relative;
  /* The ring sits OUTSIDE the circle's box, so the element has to reserve that
     space in layout — the step body is an overflow-hidden scroll container and
     the icon can be its first child, which clipped the top of the ring clean
     off. Same value as the inset below, so the two cannot drift. */
  margin: var(--kyc-ring-gap);
}

.kyc-liveness-icon::after {
  content: "";
  position: absolute;
  inset: calc(var(--kyc-ring-gap) * -1);
  border-radius: 50%;
  border: 3px solid transparent;
  pointer-events: none;
}

/* Not started: a quiet ring, nothing moving — nothing has been asked for yet. */
[data-state="not_started"] .kyc-liveness-icon::after {
  border-color: var(--farmbank-light-gold);
}

/* In flight. The only states that spin, because they are the only ones where
   something is actually still happening. */
[data-state="pending"] .kyc-liveness-icon::after,
[data-state="in_review"] .kyc-liveness-icon::after,
[data-state="resubmitted"] .kyc-liveness-icon::after {
  border-color: var(--farmbank-light-gold);
  border-top-color: var(--farmbank-gold);
  animation: kyc-liveness-spin calc(var(--motion-reward) * 2.5) linear infinite;
}

/* A stalled pending row is not in flight — it is waiting for a person to act.
   Spinning at them would promise progress that is not being made. */
[data-state="pending"][data-stalled="true"] .kyc-liveness-icon::after {
  border-color: var(--farmbank-light-gold);
  border-top-color: var(--farmbank-light-gold);
  animation: none;
}

[data-state="approved"] .kyc-liveness-icon::after,
[data-state="finishing"] .kyc-liveness-icon::after { border-color: var(--farmbank-teal); }
[data-state="declined"] .kyc-liveness-icon::after,
[data-state="error"] .kyc-liveness-icon::after { border-color: #F8B4B4; }

/* Resolved states settle in rather than appear, so the moment the answer lands
   is something you can see happen. */
[data-state="approved"] .kyc-liveness-icon,
[data-state="finishing"] .kyc-liveness-icon,
[data-state="declined"] .kyc-liveness-icon,
[data-state="error"] .kyc-liveness-icon {
  animation: kyc-liveness-settle var(--motion-step) cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

@keyframes kyc-liveness-spin {
  to { transform: rotate(360deg); }
}

@keyframes kyc-liveness-settle {
  0%   { transform: scale(0.82); opacity: 0; }
  100% { transform: scale(1); opacity: 1; }
}


/* Which stage the wait is in is entirely a function of the state already
   published on #didit-liveness-root as data-state, so it is selectors rather
   than a JS pass: the poll writes the attribute, this decides what that looks
   like. `data-stalled` is a pending check nobody is finishing — no progress to
   show, so no list. Any state not named here (not_started, declined, error)
   falls through to the hidden default rather than guessing at a position. */
/* Owns its own spacing. It previously carried an mt-6 utility AND `margin: 0`
   here — custom.css loads after Tailwind and the two have equal specificity, so
   this quietly won and the list sat ~6px under the status copy. One source of
   truth for the gap now, rather than a utility that looks like it works. */
.kyc-liveness-stages {
  display: none;
  list-style: none;
  margin: 1.5rem auto 0;
  padding: 0;
  max-width: 20rem;
  text-align: left;
}

[data-state="pending"] .kyc-liveness-stages,
[data-state="in_review"] .kyc-liveness-stages,
[data-state="resubmitted"] .kyc-liveness-stages,
[data-state="finishing"] .kyc-liveness-stages,
[data-state="approved"] .kyc-liveness-stages {
  display: block;
}

/* Every stage is done, so every stage is green. Leaving them grey under a green
   "Identity verified" was the actual problem — a finished checklist that looks
   unfinished — not the checklist itself. */
[data-state="approved"] .kyc-liveness-stage {
  opacity: 1;
  color: var(--farmbank-teal);
}

[data-state="approved"] .kyc-liveness-stage .dot {
  background: var(--farmbank-teal);
}

/* THE LIST HAS TO BE WALKED, NOT JUMPED.
   ----------------------------------------------------------------------------
   Didit usually answers `approved` straight out of `pending` without ever
   passing through In Review, so in practice the only two pictures this list
   ever drew were "stage one, nothing else" and, one poll later, "everything
   green". Three steps of progress shown as none and then all, which is the same
   as showing no progress at all. Neither of the two rules below invents
   progress; each names a fact that is already known at the moment it applies.

   data-received: THE BROWSER WATCHED THE VIDEO BE SUBMITTED. That is a
   different fact from "a row exists and is pending" — it is the exact thing
   stage one describes — and the page has held it all along as sessionCompleted,
   including across a reload, without ever showing it. So the first stage closes
   the moment the recording is handed over, and the second becomes the one
   actually running, which is a true account of the wait: the video is in, the
   decision is not. */
[data-state="pending"][data-received="true"] [data-stage="pending"] {
  opacity: 1;
  color: var(--farmbank-teal);
}

[data-state="pending"][data-received="true"] [data-stage="pending"] .dot {
  background: var(--farmbank-teal);
}

[data-state="pending"][data-received="true"] [data-stage="pending"] [data-when="active"] { display: none; }
[data-state="pending"][data-received="true"] [data-stage="pending"] [data-when="done"] { display: inline; }

[data-state="pending"][data-received="true"] [data-stage="in_review"] {
  opacity: 1;
  color: var(--farmbank-black);
  font-weight: 500;
}

[data-state="pending"][data-received="true"] [data-stage="in_review"] .dot {
  background: var(--farmbank-gold);
}

/* finishing: the decision has LANDED and the step is recording it. The first
   two stages are closed facts by now; the third is the one still running, and
   giving it its own beat is what turns the arrival of the answer into something
   you watch happen rather than something you find already over. It is the only
   moment in this list held open by a clock (settleApproved in the step's
   script), and the work it names has genuinely just occurred. */
[data-state="finishing"] [data-stage="pending"],
[data-state="finishing"] [data-stage="in_review"] {
  opacity: 1;
  color: var(--farmbank-teal);
}

[data-state="finishing"] [data-stage="pending"] .dot,
[data-state="finishing"] [data-stage="in_review"] .dot {
  background: var(--farmbank-teal);
}

[data-state="finishing"] [data-stage="pending"] [data-when="active"],
[data-state="finishing"] [data-stage="in_review"] [data-when="active"] { display: none; }
[data-state="finishing"] [data-stage="pending"] [data-when="done"],
[data-state="finishing"] [data-stage="in_review"] [data-when="done"] { display: inline; }

[data-state="finishing"] [data-stage="approved"] {
  opacity: 1;
  color: var(--farmbank-black);
  font-weight: 500;
}

[data-state="finishing"] [data-stage="approved"] .dot {
  background: var(--farmbank-gold);
}

/* A stage closing is the event worth seeing, so the dot that just turned teal
   pops once as it does. Scoped to the dots the rules above have actually marked
   done: a dot that was already teal when the page loaded has nothing to
   announce, which is why this hangs off the transition states rather than off
   .dot itself. */
[data-state="pending"][data-received="true"] [data-stage="pending"] .dot,
[data-state="finishing"] [data-stage="in_review"] .dot,
[data-state="approved"] [data-stage="approved"] .dot {
  animation: kyc-stage-done var(--motion-step) cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

@keyframes kyc-stage-done {
  0%   { transform: scale(1); }
  45%  { transform: scale(2.1); }
  100% { transform: scale(1); }
}

[data-state="pending"][data-stalled="true"] .kyc-liveness-stages {
  display: none;
}

[data-state="pending"] [data-stage="pending"],
[data-state="in_review"] [data-stage="in_review"],
[data-state="resubmitted"] [data-stage="in_review"] {
  opacity: 1;
  color: var(--farmbank-black);
  font-weight: 500;
}

[data-state="pending"] [data-stage="pending"] .dot,
[data-state="in_review"] [data-stage="in_review"] .dot,
[data-state="resubmitted"] [data-stage="in_review"] .dot {
  background: var(--farmbank-gold);
}

/* A finished stage says so in its own colour. Leaving the text grey while only
   the dot turned teal made a completed step look like a skipped one. */
[data-state="in_review"] [data-stage="pending"],
[data-state="resubmitted"] [data-stage="pending"] {
  opacity: 1;
  color: var(--farmbank-teal);
}

[data-state="in_review"] [data-stage="pending"] .dot,
[data-state="resubmitted"] [data-stage="pending"] .dot {
  background: var(--farmbank-teal);
}

/* ...and in its own tense. The in-progress wording is the default; the done
   wording replaces it for exactly the stages the rules above mark as done. */
.kyc-liveness-stage [data-when="done"] { display: none; }

[data-state="in_review"] [data-stage="pending"] [data-when="active"],
[data-state="resubmitted"] [data-stage="pending"] [data-when="active"],
[data-state="approved"] .kyc-liveness-stage [data-when="active"] {
  display: none;
}

[data-state="in_review"] [data-stage="pending"] [data-when="done"],
[data-state="resubmitted"] [data-stage="pending"] [data-when="done"],
[data-state="approved"] .kyc-liveness-stage [data-when="done"] {
  display: inline;
}

/* Transitions only — the state above is what each row means. */
.kyc-liveness-stage {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  padding: 0.375rem 0;
  color: var(--farmbank-medium-gray);
  opacity: 0.45;
  transition: opacity var(--motion-step) var(--motion-ease),
              color var(--motion-step) var(--motion-ease);
}

.kyc-liveness-stage .dot {
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--farmbank-light-gray);
  transition: background var(--motion-step) var(--motion-ease);
}

/* Bottom sheet ------------------------------------------------------------ */

/* Geometry, here rather than in utility classes, for the same reason
   .min-h-[100dvh] is hand-written above: the served tailwind-full.css is a
   static, content-purged build, so every arbitrary-value class this needs
   (max-h-[85dvh], h-[100dvh], max-h-[85vh]) has no generated rule and would
   silently size nothing. Written as ids instead, where it can be seen to apply.

   dvh, with a vh fallback for anything that predates it. On iOS, vh is the LARGE
   viewport — the height you get with the url bar collapsed — so a sheet sized in
   vh puts its own bottom edge, and the button on it, behind the browser chrome.
   dvh tracks what is actually visible.

   85% rather than 100%: the page behind stays visible above the sheet, which is
   what makes it read as a panel over the app rather than a screen that has
   replaced it, and gives the backdrop something to be tapped on. A sheet shorter
   than its ceiling just sizes to its content. */
#default-modal {
  height: 100vh;
  height: 100dvh;
}

#default-modal-inner {
  max-height: 85vh;
  max-height: 85dvh;
}

@media (min-width: 640px) {
  #default-modal-inner {
    max-height: 85vh;
  }
}

/* The scrolling body of the sheet, and the bottom inset is the point of it.
   The sheet's last element is its only real control ("Got it" on both of the
   modals that use this), and with no padding under it the member scrolled to
   the end and found it flush against the bottom of the screen — underneath the
   iOS home indicator, which is a control you can see and cannot press. 2rem of
   clearance plus whatever the device says its own inset is.

   env() resolves to 0px on anything without an inset, so the desktop dialog and
   an older phone just get the 2rem. */
.fb-sheet-body {
  padding-bottom: 2rem;
  padding-bottom: calc(2rem + env(safe-area-inset-bottom));
  /* The sheet scrolls, the page behind it does not. Without this, reaching
     either end of the body hands the gesture to the document and the app scrolls
     under the open sheet. */
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

/* The sheet's own control gets room to breathe at the end of the copy, rather
   than being welded to the last paragraph. Both modals that use this container
   end with exactly this element, which is what makes it safe to name here
   rather than in each of them.

   NOT sticky. Pinning it to the bottom of the scroll port is the nicer pattern
   and was tried first, but the button is a fully rounded pill and any band
   drawn from its own box — box-shadow spread, a border — follows that radius,
   so the copy kept showing through underneath it at the corners. Doing it
   properly means lifting the button out of the scrolled content and into the
   sheet's frame, which is a change to both modal partials and not one to make
   blind. With the sheet capped at 85dvh and the body carrying a real bottom
   inset, scrolling to the end puts the button fully on screen with clearance
   under it, which is what was actually missing. */
#default-modal-content > button:last-child {
  margin-top: 2rem;
}


/* #default-modal is a sheet on a phone (mt-auto, rounded-t-xl, full width) and a
   centred dialog above sm. It is opened and closed by toggling Tailwind's
   .hidden and nothing else, from half a dozen places: the backdrop, the X, the
   drag handler, and the modal's own buttons. So it appeared and vanished, with
   the "Learn more" panel on the home screen — a sheet, on a phone, with a grab
   handle drawn at the top of it — being the one where that reads worst.

   The ENTRY is pure CSS: .hidden is display:none, and an animation runs the
   moment the element starts being rendered, which is exactly when the class
   comes off. The EXIT cannot be, for the same reason in reverse — display:none
   lands in the same frame and there is nothing left to animate — so it is held
   open by bindSheetMotion in motion.js, which is what puts .fb-sheet-closing on.

   Backdrop and sheet are separate animations on purpose. The dimming is a
   plain fade at any width; only the panel travels, and only where it is a
   sheet. */
#default-modal:not(.hidden):not(.fb-sheet-closing) {
  animation: fb-sheet-backdrop-in var(--motion-step) var(--motion-ease) both;
}

#default-modal:not(.hidden):not(.fb-sheet-closing) > #default-modal-inner {
  animation: fb-sheet-in var(--motion-page) var(--motion-page-ease) both;
}

/* Outranks Tailwind's .hidden (an id and a class against one class), which is
   what lets the exit run on a sheet whose caller has already marked it hidden.
   display is restated rather than inherited: .hidden won the display property,
   so something has to give it back, and `flex` is what the element's own
   utilities set it to. */
#default-modal.fb-sheet-closing {
  display: flex;
  /* The sheet is on its way out and must not take a click on the way. This is
     also the safety net for the whole mechanism: every failure mode of the JS
     that holds this class on — an animationend that never fires, a backstop
     that is cancelled wrongly, a bug in the observer — leaves a transparent,
     viewport-sized, position:fixed element at z-50 over the page. Without this
     the app would look completely normal and respond to nothing. With it, the
     worst case is an invisible element that clicks pass straight through. */
  pointer-events: none;
  animation: fb-sheet-backdrop-out var(--motion-step) var(--motion-ease) both;
}

#default-modal.fb-sheet-closing > #default-modal-inner {
  animation: fb-sheet-out var(--motion-step) var(--motion-page-ease) both;
}

@keyframes fb-sheet-backdrop-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes fb-sheet-backdrop-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}

/* Travels its own height, so it starts fully off the bottom edge whatever the
   content makes it — a fixed offset would show the sheet already half open on a
   short panel and clip a tall one. */
@keyframes fb-sheet-in {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}

@keyframes fb-sheet-out {
  from { transform: translateY(0); }
  to   { transform: translateY(100%); }
}

/* Above sm it is a centred dialog, not a sheet: sliding it off the bottom of
   the viewport would send it travelling half the screen for a box that sits in
   the middle of it. It scales up from just under full size instead, which is
   the same gesture at a scale that suits the shape. */
@media (min-width: 640px) {
  @keyframes fb-sheet-in {
    from { opacity: 0; transform: scale(0.96); }
    to   { opacity: 1; transform: scale(1); }
  }

  @keyframes fb-sheet-out {
    from { opacity: 1; transform: scale(1); }
    to   { opacity: 0; transform: scale(0.96); }
  }
}

/* Cross-page navigation --------------------------------------------------- */

/* Everything above animates WITHIN a screen. This is the other half: moving
   BETWEEN screens. Every flow in the product that is not the single-document
   wizard changes screen by navigating — sign in -> 2FA -> home, settings ->
   verify -> edit -> confirm -> done — and each of those was a white flash and a
   fresh paint, which is what made an otherwise finished flow feel like a
   website rather than an app.

   Cross-document view transitions, so this is the browser animating the real
   navigation: no router, no interception, no JS on the critical path. Browsers
   without support (Firefox, older Safari) navigate exactly as they do today and
   get the @supports fallback at the bottom of this block instead. Nothing about
   what loads, what a link does, or what the back button means changes either
   way — which is the whole reason for doing it here rather than boosting the
   app onto client-side navigation.

   Opted in inside prefers-reduced-motion: no-preference rather than being
   neutralised by the reduced-motion block below: ::view-transition-* pseudos
   are not descendants of anything and `*` never reaches them, so the only
   reliable way to honour the preference is to not start the transition. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }
}

/* The UA default blends the two snapshots together. That reads fine for a pure
   crossfade and badly for a slide — mid-travel you see both screens' text
   through each other. Opaque snapshots, one passing the other. */
::view-transition-image-pair(root) {
  isolation: auto;
}

/* The UA also animates the group itself, at its own 250ms. For a root-only
   transition both snapshots are the same viewport rectangle so that animation
   moves nothing — but leaving it 30ms short of the two below is a mismatch
   waiting to be noticed the first time anything else gets a
   view-transition-name. Measured running alongside ours in Chrome. */
::view-transition-group(root) {
  animation-duration: var(--motion-page);
}

::view-transition-old(root),
::view-transition-new(root) {
  mix-blend-mode: normal;
  /* Longer than the default 250ms UA crossfade would be, and on our own curve,
     so a screen change is paced like every other motion in the product. */
  animation-duration: var(--motion-page);
  animation-timing-function: var(--motion-page-ease);
}

/* Neutral: the new screen rises and fades in OVER the old one, which stays put
   and stays opaque. This is what runs when the direction is unknown — a form
   POST that renders a new screen, a redirect, a browser that reports no
   navigation type at all. It reads correctly whichever way the member is
   actually going, which is why it is the default rather than a guessed slide.

   The old snapshot no longer fades out, and that is the fix for "it still feels
   like a reload". Two viewport-sized snapshots fading through each other in
   opposite directions both pass through partial opacity at the same moment, so
   for the middle of the transition neither screen is solid and the page's own
   background shows through the pair. On a phone, where this neutral rule was
   running for EVERY navigation (see the Navigation API note in
   shared/_head.html.erb), that dip is the whole screen going pale and coming
   back — which is exactly what a page reload looks like. Holding the outgoing
   screen at full opacity underneath means there is always something solid to
   see, and the arriving screen simply covers it. */
::view-transition-old(root) {
  animation-name: fb-page-hold;
}

::view-transition-new(root) {
  animation-name: fb-page-in;
}

@keyframes fb-page-hold {
  from { opacity: 1; }
  to   { opacity: 1; }
}

@keyframes fb-page-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Directional: a push, the way a phone does it.
   The incoming screen travels the FULL width and stays fully opaque; the screen
   it replaces slides a short way in the same direction underneath it, also
   fully opaque. Nothing cross-fades.

   That last part is the whole point. Two viewport-sized snapshots fading
   through each other is what made the first pass read as a smear: for the
   middle third of the transition every line of text on the page was drawn
   twice, half-transparent, a few percent apart. Moving them past each other
   opaque means the eye tracks one screen leaving and one arriving, which is
   what "smooth" actually is here.

   It leaves no gap, which is why the trailing screen only needs to move a
   little: at time t the arriving screen's left edge is at (1-t)·100% and the
   leaving screen still covers out to 100% - 18%·t, which is always further
   right. The seam between them is always flush.

   The types are set by the inline script in shared/_head.html.erb from the
   navigation type — the only thing the browser will not tell CSS on its own.

   Percentages, not pixels: a snapshot is viewport-sized, and a fixed offset
   that reads as a glide on a phone reads as a twitch on a desktop. */
html:active-view-transition-type(fb-forward)::view-transition-old(root) {
  animation-name: fb-page-exit-left;
}

html:active-view-transition-type(fb-forward)::view-transition-new(root) {
  animation-name: fb-page-enter-right;
}

/* Going back, the screen being left is the one that MOVES, so it has to travel
   over the top of the one coming back. Without this the returning screen paints
   above it (new is above old by default) and the leaving screen is simply
   covered up rather than seen to leave. */
html:active-view-transition-type(fb-back)::view-transition-old(root) {
  z-index: 1;
  animation-name: fb-page-exit-right;
}

html:active-view-transition-type(fb-back)::view-transition-new(root) {
  animation-name: fb-page-enter-left;
}

/* Forward: the new screen comes in over the top, the old one eases back. */
@keyframes fb-page-enter-right {
  from { transform: translateX(100%); }
  to   { transform: translateX(0); }
}

@keyframes fb-page-exit-left {
  from { transform: translateX(0); }
  to   { transform: translateX(-18%); }
}

/* Back: the same two moves played the other way round — the old screen is the
   one that leaves, and the one underneath returns from where it was parked. */
@keyframes fb-page-exit-right {
  from { transform: translateX(0); }
  to   { transform: translateX(100%); }
}

@keyframes fb-page-enter-left {
  from { transform: translateX(-18%); }
  to   { transform: translateX(0); }
}

/* In place: the screen the member is already on, rendered again.
   A refused code, a wrong password, a form that failed validation, a reload.
   Nothing has moved, so nothing animates.

   It is still a transition, and that is the entire point of this block. A
   cross-document view transition holds the outgoing screen on the display until
   the incoming one is ready to paint, which is what covers the blank frame
   between two documents. Turning the transition OFF for these navigations
   removed the slide and put a visible page refresh in its place. Turning the
   MOTION off keeps the cover and loses the movement.

   With no animation on either half the incoming snapshot, which paints above
   the outgoing one, is simply there: one clean frame, no travel, no crossfade,
   no flash. The only thing the member sees change is whatever the server
   marked red. */
html:active-view-transition-type(fb-in-place)::view-transition-group(root),
html:active-view-transition-type(fb-in-place)::view-transition-old(root),
html:active-view-transition-type(fb-in-place)::view-transition-new(root) {
  animation: none;
}

/* Browsers with no view transitions still get the arrival half, so a navigation
   resolves rather than snapping. Deliberately opacity ONLY — #main-content
   wraps the whole app including position:fixed children (the cookie banner),
   and a transform on an ancestor makes those fixed elements resolve against the
   ancestor instead of the viewport, which would jump them for the length of the
   animation. There is no exit half: the old document is already gone by the
   time anything here could run. */
@supports not (view-transition-name: none) {
  @media (prefers-reduced-motion: no-preference) {
    #main-content {
      animation: fb-page-arrive var(--motion-page) var(--motion-ease) both;
    }
  }
}

@keyframes fb-page-arrive {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Link feedback ----------------------------------------------------------- */

/* Anchors that are laid out as buttons get the same press as a submit control,
   so a link that starts a navigation is seen to have been pressed while the
   server is still answering. motion.js decides which anchors qualify and never
   changes an anchor's `display` to do it — an inline link simply doesn't get
   the press, because scale() on an inline box does nothing anyway. */
a.kyc-cta-press {
  transform-origin: center;
}

/* Reduced motion --------------------------------------------------------- */

/* Ships with the tokens, not as a follow-up: a KYC flow that cannot be
   completed without tolerating motion is a compliance problem. Durations go to
   zero app-wide; every state above still resolves to its final appearance. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }

  /* ...except things whose whole job is to say "still working".
     This file is loaded app-wide, so the rule above reaches every Tailwind
     animate-spin / animate-pulse in the product — QR payment, check deposit,
     online transfer, upload progress, the liveness button. Frozen on frame one
     a spinner reads as a hang, which is worse than showing none at all.
     Reduced motion means don't move things around; it does not mean take away
     progress feedback, and a rotating loader is accepted under it. */
  .animate-spin,
  .animate-pulse,
  .kyc-upload-bar.is-working > i,
  .kyc-liveness-icon::after {
    animation-duration: 1s !important;
    animation-iteration-count: infinite !important;
  }
}

/* Currency symbol on display amounts ------------------------------------- */

/* THE GAP IS A MARGIN, NOT A SPACE CHARACTER, AND THAT IS THE ENTIRE POINT.

   This gap used to be a space inside AmountFormatter, and a space between the
   sign and the digits is a line-break opportunity: a success screen was
   reported reading "you've created a ₱" on one line and "5,000,000.00 time
   deposit" on the next. Closing it up fixed the break everywhere, at the cost
   of the gap everywhere — including the big balances, where there is no running
   copy to break and where the sign ends up jammed against the first digit.

   A margin restores the gap on exactly those balances and cannot bring the bug
   back, because there is no character there to break at. AmountFormatter is
   untouched, so every amount inside a sentence keeps the closed-up form.

   IT IS ALSO COVERING FOR A MISSING GLYPH, and that is worth knowing before
   anyone tunes this value. Aeonik has no ₱: the three faces in public/fonts
   carry 201 codepoints between them — Latin, Greek and Cyrillic letters, digits,
   and only four punctuation marks (space, comma, hyphen, period). No currency
   symbol of any kind, and no apostrophe, question mark or percent either. So the
   sign has always rendered in the system fallback, Helvetica on iOS and Roboto
   on Android, sitting next to Aeonik digits.

   Verified by measurement, not by document.fonts.check(), which answers for the
   whole stack including fallbacks and so reports the peso as available: at
   100px, ₱ measures the same 66.5px in 'Aeonik-Pro' as in plain sans-serif —
   identical, because the fallback draws both — while the digit 8 measures 59px
   against 55.6px, because Aeonik really does draw digits.

   Two typefaces with no space between them read as one word set in two fonts,
   which is most of why the jammed-up version looked wrong. The gap separates
   them and takes the worst of it out. It is NOT the fix. The fix is a complete
   Aeonik containing U+20B1, which is an asset to obtain, not a rule to write. */
.amount-symbol {
  margin-right: 0.14em;
}
