Million-Dollar Details · EP02Insight1 min read

The $900 million checkbox

Citibank meant to pay $7.8 million in interest. One box ticked, two settings missing, and the whole loan went out. Try not to make the same mistake.

01

Your turn

A simplified form inspired by the case, not Citi’s real software. Your task: pay only the interest.

Task: pay $7,800,000 interest. Nothing else.

LOAN PAYMENT · REF 4417-RV

02

The same task, designed better

One clear line for the interest, the dangerous option needs press and hold, and the total updates live.

You’re sending

$7,800,000

03

The checkbox, ready to copy

Plain HTML, CSS and JavaScript. Hold to confirm: the border fills, then the red dot drops in and becomes the check.

hold-check.html
<!-- Hold-to-confirm checkbox · metic.studio · Million-Dollar Details EP 02 -->
<button class="hold-check" type="button" role="checkbox" aria-checked="false">
  <span class="hold-check__box">
    <svg viewBox="0 0 30 30" aria-hidden="true">
      <path class="hold-check__edge" pathLength="1"
        d="M15 .75H21A8.25 8.25 0 0 1 29.25 9V21A8.25 8.25 0 0 1 21 29.25H9A8.25 8.25 0 0 1 .75 21V9A8.25 8.25 0 0 1 9 .75Z"/>
      <circle class="hold-check__dot" cx="9" cy="15.5" r="3.4"/>
      <path class="hold-check__tick" pathLength="1" d="M9 15.5L13.3 19.8 21.5 10.5"/>
    </svg>
  </span>
  <span>Pay off the whole loan<small>hold to confirm</small></span>
</button>

<style>
  .hold-check {
    --ink: #121214; --line: rgb(18 18 20 / .32); --accent: #ff3e3e; --lens: cubic-bezier(.23, 1, .32, 1);
    all: unset; display: inline-flex; align-items: center; gap: 14px; min-height: 48px; cursor: pointer;
    font: 500 17px/1.25 system-ui, sans-serif; -webkit-tap-highlight-color: transparent; touch-action: manipulation;
  }
  .hold-check small { display: block; font: 12.5px ui-monospace, monospace; color: var(--accent); }
  .hold-check__box {
    position: relative; width: 30px; height: 30px; box-sizing: border-box; border: 1.5px solid var(--line); border-radius: 9px;
    transition: border-color .3s var(--lens) .1s, transform .16s var(--lens);
  }
  .hold-check__box svg { position: absolute; inset: -1.5px; width: 30px; height: 30px; overflow: visible; }
  /* the border fills while you hold */
  .hold-check__edge { fill: none; stroke: var(--accent); stroke-width: 2.6; stroke-linecap: round;
    stroke-dasharray: 1; stroke-dashoffset: 1; transition: opacity .35s var(--lens) .2s; }
  .hold-check__tick { fill: none; stroke: var(--accent); stroke-width: 2.8; stroke-linecap: round; stroke-linejoin: round;
    stroke-dasharray: 1; stroke-dashoffset: 1; transition: stroke-dashoffset .22s ease-in; }
  .hold-check__dot { fill: var(--accent); transform-box: fill-box; transform-origin: center; transform: translateY(-22px) scale(0); }
  .hold-check.is-holding .hold-check__box { transform: scale(.96); }
  /* confirmed: the red dot drops in and becomes the check */
  .hold-check[aria-checked="true"] .hold-check__box { border-color: var(--ink); }
  .hold-check[aria-checked="true"] .hold-check__edge { opacity: 0; }
  .hold-check[aria-checked="true"] .hold-check__dot { animation: hold-check-drop .5s var(--lens) .08s forwards; }
  .hold-check[aria-checked="true"] .hold-check__tick { stroke-dashoffset: 0; transition: stroke-dashoffset .32s var(--lens) .28s; }
  .hold-check.is-shaking .hold-check__box { animation: hold-check-shake .32s var(--lens); }
  .hold-check:focus-visible .hold-check__box { outline: 2px solid var(--accent); outline-offset: 3px; }
  @keyframes hold-check-drop {
    0% { transform: translateY(-22px) scale(1); }
    32% { transform: translateY(0) scale(1.15, .8); }
    45% { transform: translateY(0) scale(1); }
    100% { transform: translateY(0) scale(0); }
  }
  @keyframes hold-check-shake { 25% { transform: translateX(-3px); } 60% { transform: translateX(2px); } }
  @media (prefers-reduced-motion: reduce) { .hold-check * { transition-duration: .01ms !important; animation-duration: .01ms !important; } }
</style>

<script>
  // Hold 900 ms to confirm. Let go early and the border runs back. One click unchecks.
  document.querySelectorAll('.hold-check').forEach((btn) => {
    const edge = btn.querySelector('.hold-check__edge'), HOLD = 900
    let t0 = 0, p = 0, raf = 0, holding = false
    const set = (v) => (edge.style.strokeDashoffset = 1 - v)
    const check = (on) => btn.setAttribute('aria-checked', String(on))
    const frame = (now) => {
      if (!holding) return
      p = Math.min(1, (now - t0) / HOLD); set(p)
      if (p === 1) { holding = false; btn.classList.remove('is-holding'); check(true); setTimeout(() => set(0), 700); return }
      raf = requestAnimationFrame(frame)
    }
    const start = () => {
      if (btn.getAttribute('aria-checked') === 'true') return check(false)
      holding = true; t0 = performance.now(); btn.classList.add('is-holding'); raf = requestAnimationFrame(frame)
    }
    const stop = () => {
      if (!holding) return
      holding = false; cancelAnimationFrame(raf); btn.classList.remove('is-holding')
      btn.classList.remove('is-shaking'); void btn.offsetWidth; btn.classList.add('is-shaking')
      const from = p, s = performance.now()
      const back = (now) => { const q = Math.min(1, (now - s) / 320); set(from * Math.pow(1 - q, 3)); if (q < 1) requestAnimationFrame(back) }
      requestAnimationFrame(back)
    }
    btn.addEventListener('pointerdown', (e) => { e.preventDefault(); start() })
    ;['pointerup', 'pointerleave', 'pointercancel'].forEach((ev) => btn.addEventListener(ev, stop))
    btn.addEventListener('keydown', (e) => { if ((e.key === ' ' || e.key === 'Enter') && !e.repeat) { e.preventDefault(); start() } })
    btn.addEventListener('keyup', (e) => { if (e.key === ' ' || e.key === 'Enter') stop() })
  })
</script>

What happened

On 11 August 2020, Citibank was supposed to send $7.8 million in interest to the lenders of a loan to Revlon. In the bank’s loan software, this only worked through a workaround: book the principal as paid and send it to an internal account.

The operator ticked the “principal” box and entered the internal account. He thought that kept the money inside the bank. It didn’t: two more settings, “front” and “fund”, needed the same account. Two colleagues reviewed it and approved it.

The whole loan went out, principal plus interest, about $900 million. In February 2021 a judge ruled that the lenders who refused to send it back could keep around $500 million. In September 2022 the appeals court reversed that, and Citi got the money back.

What would have saved them

  • One clear choice instead of three cryptic boxes.
  • Labels in plain language, not system codes.
  • The amount, big. The one number that matters.
  • A total that updates live while you choose.
  • Hold to confirm for the dangerous option. Let go early, nothing happens.

Sources

Want details like this on your website?

The lens shows what lies beneath. Hold a finger on the page to look for yourself.