/* Delight layer — motion only, no layout.
 *
 * Isolated on purpose: every rule here is an ADDITION triggered by a class the
 * delight.js module adds and removes. Nothing here restyles an existing
 * element, so deleting this file and its <link> reverts the app exactly, the
 * same way the staff redesign layer works.
 *
 * Rules for anything added here:
 *   1. Never animate a money FIGURE's position — a number sliding around while
 *      somebody reads it is how you get "the app took my money" support chats.
 *      Numbers may count, glow or bloom in place. They may not fly.
 *   2. Everything is under 600ms. This is a wallet, not a slot machine.
 *   3. Everything stops under prefers-reduced-motion (block at the bottom).
 */

/* ---------------------------------------------------------------- pressing */
/* A press should feel like pressing something physical. Scale only — no
 * translate — so nothing shifts under the thumb mid-tap. */
.fx-press {
  animation: fxPress 180ms cubic-bezier(.3, .8, .4, 1);
}
@keyframes fxPress {
  0%   { transform: scale(1); }
  45%  { transform: scale(.955); }
  100% { transform: scale(1); }
}

/* The ripple starts where the finger actually landed (--fx-x/--fx-y set in JS),
 * because a ripple from the centre of a wide button looks like a glitch. */
.fx-ripple-host { position: relative; overflow: hidden; }
.fx-ripple {
  position: absolute;
  left: var(--fx-x, 50%);
  top: var(--fx-y, 50%);
  width: 12px; height: 12px;
  margin: -6px 0 0 -6px;
  border-radius: 50%;
  background: currentColor;
  opacity: .22;
  pointer-events: none;
  animation: fxRipple 520ms cubic-bezier(.2, .7, .3, 1) forwards;
}
@keyframes fxRipple {
  to { transform: scale(26); opacity: 0; }
}

/* ------------------------------------------------------------- money moments */
/* Counting numbers get a subtle warmth so the eye follows the change without
 * the digits moving. Paired with the count-up in JS. */
.fx-counting { animation: fxCounting 520ms ease-out; }
@keyframes fxCounting {
  0%   { transform: scale(1);    filter: brightness(1); }
  35%  { transform: scale(1.045); filter: brightness(1.18); }
  100% { transform: scale(1);    filter: brightness(1); }
}

/* Money ARRIVING is the one moment worth a real flourish. */
.fx-received { animation: fxReceived 700ms cubic-bezier(.2, .9, .3, 1); }
@keyframes fxReceived {
  0%   { transform: scale(.86); opacity: .3; }
  40%  { transform: scale(1.09); opacity: 1; }
  70%  { transform: scale(.98); }
  100% { transform: scale(1); }
}

/* A coin arcs from the button to the balance on a deposit. Positioned by JS. */
.fx-coin {
  position: fixed;
  z-index: 9999;
  width: 26px; height: 26px;
  border-radius: 50%;
  display: grid; place-items: center;
  font-size: 15px;
  background: radial-gradient(circle at 32% 30%, #ffe9a3, #f6c344 58%, #d69a12);
  box-shadow: 0 3px 10px rgba(214, 154, 18, .45), inset 0 -2px 4px rgba(0,0,0,.18);
  pointer-events: none;
  will-change: transform, opacity;
}

/* Confetti for a genuinely good moment. Each piece gets its own drift/spin/delay
 * from JS so it never reads as a repeating pattern. */
.fx-confetti {
  position: fixed;
  z-index: 9999;
  width: 9px; height: 14px;
  border-radius: 2px;
  pointer-events: none;
  will-change: transform, opacity;
  animation: fxConfetti var(--fx-dur, 1400ms) cubic-bezier(.15, .6, .35, 1) forwards;
}
@keyframes fxConfetti {
  0%   { transform: translate3d(0,0,0) rotate(0deg); opacity: 1; }
  100% { transform: translate3d(var(--fx-dx, 0), var(--fx-dy, 60vh), 0)
                    rotate(var(--fx-spin, 540deg)); opacity: 0; }
}

/* ------------------------------------------------------------------ status */
/* A tick that DRAWS itself reads as "it completed" far better than one that
 * simply appears. */
.fx-tick { display: inline-block; }
.fx-tick path {
  stroke-dasharray: 32;
  stroke-dashoffset: 32;
  animation: fxDraw 420ms cubic-bezier(.6, .1, .3, 1) forwards;
}
@keyframes fxDraw { to { stroke-dashoffset: 0; } }

/* Refusals shake horizontally only — a vertical shake reads as a crash. */
.fx-nope { animation: fxNope 420ms cubic-bezier(.36, .07, .19, .97); }
@keyframes fxNope {
  10%, 90% { transform: translateX(-2px); }
  20%, 80% { transform: translateX(3px); }
  30%, 50%, 70% { transform: translateX(-5px); }
  40%, 60% { transform: translateX(5px); }
}

/* Something worth noticing, without a sound: one slow breath. */
.fx-attention { animation: fxAttention 1.6s ease-in-out 2; }
@keyframes fxAttention {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.03); }
}

/* --------------------------------------------------------------- screens */
/* Sheets and screens get a soft rise. Short, because this fires constantly. */
.fx-rise { animation: fxRise 260ms cubic-bezier(.2, .8, .3, 1); }
@keyframes fxRise {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* List rows arrive one after another. JS sets --fx-i per row; the cap in JS
 * stops a long history turning into a slow cascade. */
.fx-stagger { animation: fxRise 260ms cubic-bezier(.2,.8,.3,1) both;
              animation-delay: calc(var(--fx-i, 0) * 38ms); }

/* The QR scanner locking onto a code. */
.fx-locked { animation: fxLocked 420ms cubic-bezier(.2, .9, .3, 1); }
@keyframes fxLocked {
  0%   { transform: scale(1);   box-shadow: 0 0 0 0 rgba(52, 199, 89, .5); }
  50%  { transform: scale(1.04); box-shadow: 0 0 0 12px rgba(52, 199, 89, 0); }
  100% { transform: scale(1);   box-shadow: 0 0 0 0 rgba(52, 199, 89, 0); }
}

/* ------------------------------------------------------- the cute details */
/* Idle wiggle on the joystick, so the games button has a pulse of its own.
 * Rare on purpose (7s) — a thing that moves constantly stops being noticed and
 * starts being irritating. */
.fx-idle-wiggle { animation: fxWiggle 7s ease-in-out infinite; }
@keyframes fxWiggle {
  0%, 88%, 100% { transform: rotate(0deg); }
  90%  { transform: rotate(-9deg); }
  93%  { transform: rotate(7deg); }
  96%  { transform: rotate(-4deg); }
}

/* An empty screen should feel friendly rather than broken. */
.fx-float { animation: fxFloat 3.4s ease-in-out infinite; }
@keyframes fxFloat {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-6px); }
}

/* --------------------------------------------------- accessibility opt-out */
/* Someone who has asked their phone for less motion has asked THIS app too.
 * Sounds are governed separately, by the existing sound settings. */
@media (prefers-reduced-motion: reduce) {
  .fx-press, .fx-ripple, .fx-counting, .fx-received, .fx-coin, .fx-confetti,
  .fx-tick path, .fx-nope, .fx-attention, .fx-rise, .fx-stagger, .fx-locked,
  .fx-idle-wiggle, .fx-float {
    animation: none !important;
    transition: none !important;
  }
  .fx-coin, .fx-confetti { display: none !important; }
}

/* ------------------------------------------------- a poor line, a cheap phone
   app.js publishes the connection class on <html data-net>. A device on 2G or
   with Data Saver on is usually also a device that cannot spare the CPU for
   thirty confetti pieces, so the expensive decoration stops there. Everything
   that communicates STATE — press, counting, refusals — keeps working, because
   those are feedback, not decoration. */
:root[data-net="lite"] .fx-confetti,
:root[data-net="lite"] .fx-coin{display:none!important}
:root[data-net="lite"] .fx-ripple{animation-duration:220ms}
:root[data-net="lite"] .fx-idle-wiggle,
:root[data-net="lite"] .fx-float{animation:none}

/* ------------------------------------------------------- feeling like an app
   Two browser behaviours are what make a WebView feel like a web page:

     * the grey/blue flash box the engine paints over anything tapped, and
     * long-press selecting the label inside a button, complete with handles
       and a "Copy / Share" callout.

   Neither exists in a native app, so both go — but only on CHROME. Message
   text, amounts, receipt numbers and wallet tags stay selectable, because
   people genuinely copy those and taking that away is worse than the flash. */
html{-webkit-tap-highlight-color:transparent;tap-highlight-color:transparent}

/* Naming every control that must not be selectable was the wrong shape: the
   list can only ever be as complete as the last time somebody remembered to
   add to it, and it kept missing things — the Search in chat button, the pin,
   the joystick. So the rule is inverted. NOTHING is selectable, and the few
   places where selecting is the point ask for it back below.

   The old rule also set user-select on `button *` but never touch-callout,
   and on iOS it is touch-callout that raises the Copy / Look Up bubble. That
   is why holding an icon still felt like holding a web page. */
html, body{
  -webkit-user-select:none;user-select:none;
  -webkit-touch-callout:none;
}

/* Typing, pasting and copying — the places where a long press should work.
   Inputs especially: without this iOS cannot place a cursor or offer Paste,
   which would break entering a $tag or a PIN. */
input, textarea, select, [contenteditable="true"], [contenteditable=""],
.selectable-message, .selectable-message *,
[data-selectable], [data-selectable] *{
  -webkit-user-select:text;user-select:text;
  -webkit-touch-callout:default;
}

/* The tag on the Receive screen is shown so it can be copied — one press takes
   the whole thing.
   Two declarations on purpose. Safari does not support the `all` VALUE, so on
   an iPhone it dropped that declaration, the tag inherited `none` from the rule
   above, and the one thing on the screen that exists to be copied could not be
   touched. `text` is declared first as a floor: a browser that understands
   `all` takes the later one, a browser that does not still gets a selectable
   tag. Caught by the WebKit run in e2e/tests/native-feel.spec.js. */
.wallet-receive-tag{
  -webkit-user-select:text;user-select:text;
  -webkit-user-select:all;user-select:all;
  -webkit-touch-callout:default;
}

/* The receipt number is the one thing a customer is ever asked to quote back
   to us — "which payment?" — so it has to be copyable. Deny-by-default had
   quietly taken that away, which an earlier comment in this file had promised
   would not happen. The amount and the note go with it: they are prose and
   figures on a document, not controls. */
.receipt-ref dd, .receipt-rows dd, .receipt-note, .receipt-amount{
  -webkit-user-select:text;user-select:text;
  -webkit-touch-callout:default;
}

/* Dragging an image out of a button is another web-page giveaway. */
button img, .tap-button img, .icon-button img{-webkit-user-drag:none;user-drag:none;pointer-events:none}

/* ------------------------------------------------- transaction row badges
   An icon, a word and a colour on every row. The badge is a circle so it reads
   at a glance from arm's length, which is how most people check a balance. */
.money-row{gap:12px}
.money-badge{
  flex:0 0 auto;width:40px;height:40px;border-radius:50%;
  display:grid;place-items:center;
  font-size:19px;font-weight:700;line-height:1;
  background:rgba(0,0,0,.06);color:#3c3c43;
}
.money-badge.tone-in   {background:rgba(28,150,70,.14); color:#1c8f46}
.money-badge.tone-out  {background:rgba(0,122,255,.13); color:#0a6fd8}
.money-badge.tone-wait {background:rgba(255,149,0,.16); color:#a35c00}
.money-badge.tone-warn {background:rgba(255,59,48,.14); color:#c0271d}
.money-badge.tone-free {background:rgba(175,82,222,.14);color:#8e3fbe}
.money-badge.tone-off  {background:rgba(120,120,128,.16);color:#6d6d72}

/* Cancelled reads as struck through and faded, not merely a different colour. */
.money-row.is-off{opacity:.62}
.money-row.is-off .money-right strong{text-decoration:line-through}

/* Something still happening should look unsettled. */
.money-row.is-wait .money-badge{animation:fxAttention 2.4s ease-in-out infinite}

@media (prefers-color-scheme: dark){
  :root:not([data-theme="light"]) .money-badge{background:rgba(255,255,255,.09);color:#d1d1d6}
  :root:not([data-theme="light"]) .money-badge.tone-in   {background:rgba(48,209,88,.18); color:#4cd964}
  :root:not([data-theme="light"]) .money-badge.tone-out  {background:rgba(10,132,255,.20);color:#6cb4ff}
  :root:not([data-theme="light"]) .money-badge.tone-wait {background:rgba(255,159,10,.20);color:#ffb340}
  :root:not([data-theme="light"]) .money-badge.tone-warn {background:rgba(255,69,58,.20); color:#ff6961}
  :root:not([data-theme="light"]) .money-badge.tone-free {background:rgba(191,90,242,.20);color:#d08cff}
  :root:not([data-theme="light"]) .money-badge.tone-off  {background:rgba(255,255,255,.10);color:#9a9a9f}
}
:root[data-theme="dark"] .money-badge{background:rgba(255,255,255,.09);color:#d1d1d6}
:root[data-theme="dark"] .money-badge.tone-in   {background:rgba(48,209,88,.18); color:#4cd964}
:root[data-theme="dark"] .money-badge.tone-out  {background:rgba(10,132,255,.20);color:#6cb4ff}
:root[data-theme="dark"] .money-badge.tone-wait {background:rgba(255,159,10,.20);color:#ffb340}
:root[data-theme="dark"] .money-badge.tone-warn {background:rgba(255,69,58,.20); color:#ff6961}
:root[data-theme="dark"] .money-badge.tone-free {background:rgba(191,90,242,.20);color:#d08cff}
:root[data-theme="dark"] .money-badge.tone-off  {background:rgba(255,255,255,.10);color:#9a9a9f}

/* --------------------------------------------------- confirm a payment
   A full screen, not a small dialog. The whole point is that it cannot be
   dismissed by muscle memory, so it takes over and asks one question. */
/* A <dialog> now, so it lands in the top layer above the send sheet. That
   brings the UA's own dialog styling with it — border, padding, auto margins,
   a max-width and max-height — all of which have to go, and display has to be
   tied to [open] or a closed dialog would sit there visible. */
.confirm-pay{
  position:fixed;inset:0;z-index:9998;
  border:0;margin:0;width:100%;height:100%;max-width:none;max-height:none;
  display:none;place-items:center;padding:24px;
  color:var(--text);
  background:var(--panel,#fff);
  opacity:0;transform:translateY(12px);
  transition:opacity .2s ease,transform .2s cubic-bezier(.2,.8,.3,1);
  overflow-y:auto;
}
.confirm-pay[open]{display:grid}
.confirm-pay.is-open{opacity:1;transform:none}
/* The panel itself is opaque and covers everything; the backdrop only shows
   during the fade, so it stays plain rather than adding a second surface. */
.confirm-pay::backdrop{background:rgba(0,0,0,.45)}
.confirm-inner{width:100%;max-width:380px;text-align:center}

.confirm-kicker{font-size:15px;color:var(--muted,#6d6d72);margin-bottom:22px}

.confirm-face{
  width:96px;height:96px;border-radius:50%;object-fit:cover;display:block;margin:0 auto 16px;
  background:var(--soft,#eef3fb);
}
.confirm-initials{
  display:grid;place-items:center;font-size:38px;font-weight:700;color:var(--primary,#0084ff);
}

.confirm-name{font-size:24px;font-weight:700;line-height:1.25}
.confirm-tag{font-size:17px;color:var(--primary,#0084ff);margin-top:4px;font-weight:600}

/* The number, then the same number in words underneath. */
.confirm-amount{
  margin-top:26px;font-size:52px;font-weight:800;letter-spacing:-.03em;
  font-variant-numeric:tabular-nums;line-height:1;
}
.confirm-words{margin-top:10px;font-size:18px;color:var(--muted,#6d6d72)}
.confirm-note{
  margin-top:16px;font-size:16px;line-height:1.45;color:var(--muted,#6d6d72);
  background:var(--soft,#f2f4f8);border-radius:14px;padding:12px 14px;
}

/* Big, and a long way from the way out — nothing destructive sits next to it. */
.confirm-go{
  margin-top:32px;width:100%;min-height:60px;border:0;border-radius:16px;
  background:var(--primary,#0084ff);color:#fff;
  font:inherit;font-size:19px;font-weight:700;cursor:pointer;
}
.confirm-go:active{transform:scale(.99)}
.confirm-back{
  margin-top:14px;width:100%;min-height:52px;border:0;background:none;
  color:var(--muted,#6d6d72);font:inherit;font-size:17px;cursor:pointer;
}
@media (prefers-reduced-motion: reduce){
  .confirm-pay{transition:none;transform:none}
}

/* ==========================================================================
   READABILITY  —  bigger text, honest contrast, targets you cannot miss
   --------------------------------------------------------------------------
   Written for the oldest customer on the platform, on the theory that what is
   comfortable at 70 is not worse at 25.

   --fc-text is the app's Text size setting multiplied by whatever the PHONE is
   set to (measured in app.js, because px ignores the system setting). Every
   size below is written against it, so one control moves the whole app instead
   of the three chat rules it used to move.
   ========================================================================== */
:root{--fc-os:1;--fc-text:calc(var(--fzoom,1)*var(--fc-os))}

/* The three that already scaled — now on the combined figure, and a step up. */
.message-list .bubble{font-size:calc(15.5px*var(--fc-text))}
.composer textarea{font-size:calc(16.5px*var(--fc-text))}
.typing-indicator,.reply-preview{font-size:calc(13.5px*var(--fc-text))}

/* Money. Every figure a customer might act on. */
.money-left strong{font-size:calc(17.5px*var(--fc-text))}
.money-right strong{font-size:calc(19px*var(--fc-text))}
.money-left small,.money-right small{font-size:calc(14px*var(--fc-text))}
.money-day{font-size:calc(13.5px*var(--fc-text))}
.money-empty,.wa-empty{font-size:calc(16px*var(--fc-text))}
.receipt-amount{font-size:calc(40px*var(--fc-text))}
.receipt-title{font-size:calc(18.5px*var(--fc-text))}
.receipt-sub,.receipt-rows dt{font-size:calc(14.5px*var(--fc-text))}
.receipt-rows dd{font-size:calc(15.5px*var(--fc-text))}
.receipt-note{font-size:calc(14.5px*var(--fc-text))}

/* Wallet. */
.wa-balance{font-size:calc(46px*var(--fc-text))}
.wa-tile{font-size:calc(15.5px*var(--fc-text))}
.wa-section{font-size:calc(13.5px*var(--fc-text))}
.wa-row-main strong,.wa-game-main strong{font-size:calc(16.5px*var(--fc-text))}
.wa-row-main small,.wa-game-main small{font-size:calc(14.5px*var(--fc-text))}
.wa-primary{font-size:calc(17.5px*var(--fc-text))}
.wa-link{font-size:calc(16.5px*var(--fc-text))}
.wa-note,.wa-error{font-size:calc(14.5px*var(--fc-text))}
.confirm-amount{font-size:calc(52px*var(--fc-text))}
.confirm-words{font-size:calc(17px*var(--fc-text))}

/* Chat furniture. 11px was too small to be worth printing. */
.message-meta,.pin-mark{font-size:calc(12px*var(--fc-text))}
.brand-copy strong{font-size:calc(19px*var(--fc-text))}
.brand-copy>span{font-size:calc(14px*var(--fc-text))}
.settings-card,.wide-button,.theme-row,.toggle-row{font-size:calc(16px*var(--fc-text))}
.install-help{font-size:calc(14px*var(--fc-text))}

/* Long lines are harder to track back from at any age; loosen them up. */
.message-list .bubble,.money-empty,.wa-empty,.wa-note{line-height:1.5}

/* --------------------------------------------------------------- contrast
   Secondary text was #65676b on white — legible, but it is doing real work in
   this app: it carries the date, the game name, who a payment was with. It is
   only "secondary" in the visual sense, so it is now dark enough to read in
   sunlight and with an ageing eye, while still ranking below the primary line.
   Every theme measures at least 7:1 (AAA) against its own background.

   Scoped carefully. Cute, Warm, Ocean and Business are light palettes with no
   dark form — they keep their light palette on a dark phone, so lifting their
   secondary text to a pale grey would put pale grey on pale pink. Only the
   themes that actually go dark get the dark value. */
:root{--muted:#4b4f56}
:root[data-style-theme="messenger"]{--muted:#4b4f56}
/* styles.css sets this one with two attributes, so it needs two attributes to
   answer it — same specificity, later file, this wins. */
:root[data-style-theme="messenger"][data-color-mode="light"]{--muted:#4b4f56}
:root[data-style-theme="business"]{--muted:#41565e}
:root[data-style-theme="cute"]{--muted:#77455c}
:root[data-style-theme="warm"]{--muted:#6d5138}
:root[data-style-theme="ocean"]{--muted:#385a66}

/* Light-on-dark reads heavier than it measures, so these lift rather than
   darken. The media query covers "follow the phone" and the moment before
   app.js has resolved the setting; the attribute rules cover an explicit
   choice, which has to win over the phone. */
@media (prefers-color-scheme: dark){
  :root:not([data-style-theme]):not([data-theme="light"]),
  :root[data-style-theme="messenger"]:not([data-color-mode="light"]){--muted:#c2c6cc}
}
:root[data-style-theme="messenger"][data-color-mode="dark"],
:root[data-style-theme="midnight"]{--muted:#c2c6cc}

/* ----------------------------------------------------------- tap targets
   The whole row, everywhere. A 20px checkbox is a fine target for somebody
   with steady hands and good eyes and a bad one for everybody else — so the
   row around it does the tapping, and the row tells you it did. */
.theme-row,.toggle-row{
  min-height:60px;padding:12px 4px;border-radius:12px;
  cursor:pointer;-webkit-tap-highlight-color:transparent;
}
.theme-row:active,.toggle-row:active{background:var(--input)}
.toggle-row input[type="checkbox"]{width:26px;height:26px;flex:0 0 26px}
.theme-row select{min-height:44px;font-size:calc(15.5px*var(--fc-text))}
.identifier-row{min-height:56px}
.identifier-row button{min-height:44px}
.wide-button{min-height:56px;display:flex;align-items:center}
.money-row{min-height:68px}
.wa-row{min-height:68px}
.wa-game{min-height:76px}
/* Anything that is a row is a target for its whole width, not just its text.
   .wide-button is NOT in this list, and must not be: the menu it lives in has
   no padding of its own, so those buttons inset themselves with a 16px margin
   each side and a width of calc(100% - 32px). Forcing 100% here made every one
   of them a full viewport wide starting 16px in — so the main menu ran 32px
   past its own right edge and could be dragged sideways. */
.money-row,.wa-row,.wa-game{width:100%}

/* The Text size control itself has to be usable by the person who needs it,
   which means it cannot be small. */
.zoom-row .zoom-controls{display:flex;align-items:center;gap:10px}
.zoom-row .zoom-controls button{
  min-width:46px;min-height:46px;border-radius:12px;
  border:1px solid var(--border);background:var(--input);color:var(--text);
  font-size:22px;line-height:1;cursor:pointer;
}
.zoom-row .zoom-controls button:disabled{opacity:.4}
#zoomLevelLabel{min-width:74px;font-size:calc(15px*var(--fc-text));font-weight:700}

/* Empty screens that teach. Roomy on purpose: this is the one moment somebody
   is reading rather than scanning, so it is given the space of a real screen. */
.fc-empty{
  display:flex;flex-direction:column;align-items:center;
  padding:34px 22px 30px;text-align:center;
}
.fc-empty-icon{font-size:calc(42px*var(--fc-text));line-height:1;margin-bottom:14px;opacity:.9}
.fc-empty-title{font-size:calc(18px*var(--fc-text));font-weight:700;color:var(--text)}
.fc-empty-body{
  margin:9px 0 0;max-width:34ch;         /* short lines are easier to track back */
  font-size:calc(15.5px*var(--fc-text));line-height:1.55;color:var(--muted);
}
.fc-empty-action{width:100%;max-width:320px;margin-top:22px}

/* ==========================================================================
   ONE PERSON  —  the whole story between you and somebody, full screen
   --------------------------------------------------------------------------
   It used to be a bottom sheet with a face, a run-on sentence of totals and
   four buttons of three different styles stacked on top of each other. Nothing
   said which of them you were meant to press.

   Now it reads top to bottom in the order the questions get asked: who is
   this, what has passed between us, what do I want to do, what happened when.
   ========================================================================== */
.person-bar{
  display:grid;grid-template-columns:44px 1fr 44px;align-items:center;
  gap:4px;padding:6px 6px calc(6px) 6px;
  padding-top:calc(6px + env(safe-area-inset-top));
  border-bottom:1px solid var(--border);background:var(--panel);
}
.person-bar strong{
  text-align:center;font-size:calc(17px*var(--fc-text));font-weight:650;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.person-back{
  width:44px;height:44px;border:0;border-radius:50%;background:transparent;
  color:var(--primary);font-size:30px;line-height:1;cursor:pointer;
}
.person-back:active{background:var(--input)}
.person-bar-spacer{width:44px}

.person-scroll{
  flex:1;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;
  padding:0 16px calc(28px + env(safe-area-inset-bottom));
}

/* Who they are. */
.wallet-person-head{
  display:flex;flex-direction:column;align-items:center;gap:14px;
  padding:22px 0 18px;
}

/* What has passed between you. Two figures, not one sentence — they answer two
   different questions, and a number is read far faster than a clause. */
.person-stats{display:grid;grid-template-columns:1fr 1fr;gap:10px;width:100%}
.person-stat{
  display:flex;flex-direction:column;align-items:center;gap:3px;
  padding:14px 10px;border-radius:16px;background:var(--input);
}
.person-stat small{font-size:calc(13px*var(--fc-text));color:var(--muted)}
.person-stat strong{
  font-size:calc(20px*var(--fc-text));font-weight:700;
  font-variant-numeric:tabular-nums;
}

/* What you can do. One filled button, one outlined, and the two rare settings
   demoted to chips so they stop competing with paying somebody. */
.wallet-person-actions{display:flex;flex-direction:column;gap:10px;padding-bottom:6px}
.wallet-person-actions .primary-button,
.wallet-person-actions .secondary-button{
  min-height:54px;font-size:calc(16.5px*var(--fc-text));font-weight:600;
}
.person-minor{display:flex;gap:10px;margin-top:2px}
.person-chip{
  flex:1;min-height:46px;border:1px solid var(--border);border-radius:14px;
  background:transparent;color:var(--text);cursor:pointer;
  font-size:calc(14.5px*var(--fc-text));font-weight:600;
}
.person-chip:active{background:var(--input)}
.person-chip.is-on{
  border-color:transparent;background:color-mix(in srgb,var(--primary) 14%,transparent);
  color:var(--primary);
}

/* What happened when. */
.person-list-title{
  margin:24px 0 2px;font-size:calc(13px*var(--fc-text));font-weight:700;
  letter-spacing:.04em;text-transform:uppercase;color:var(--muted);
}
#walletPersonList .money-day{background:var(--bg,var(--panel))}
#walletPersonList:empty{display:none}

/* This dialog is in the shared "wallet sheet" rule, which rounds the top
   corners and slides up from the bottom. Both belong to a sheet: on a full
   screen the radius leaves slivers of the page behind showing at the top, and
   rising from the bottom still reads as something you have to dismiss. Going
   deeper into an app moves sideways, so it does. Same specificity as the rule
   it answers, later in the cascade. */
/* Deliberately slower than the rest of the app. These three are the screens
   somebody opens to READ — the full history, one person, one receipt — and a
   page that takes a moment to arrive is a page you have arrived at. The quick
   snap the rest of the wallet uses is right for a control and wrong for a
   destination.

   620ms against the app's usual 280-420. This is about as far as it goes:
   much past ~650ms the settle stops reading as weight and starts reading as
   the app being busy, and somebody taps a second time.
   The easing does the work: it covers most of the distance immediately and
   spends the rest settling, so it reads as weight, not delay. */
#walletPersonDialog[open], #walletReceiptDialog[open]{
  border-radius:0;
  animation:waPersonIn 620ms var(--wa-enter,cubic-bezier(.16,1,.3,1)) both;
}
@keyframes waPersonIn{
  from{opacity:0;transform:translateX(26px)}
  to{opacity:1;transform:none}
}

/* The whole history is still a sheet, so it keeps rising from the bottom —
   the same slower timing, the direction it already had. */
#walletHistoryDialog[open]{
  animation:waSheetUp 620ms var(--wa-enter,cubic-bezier(.16,1,.3,1)) both;
}

/* The scrim has to take just as long. Fading it in at the old speed left the
   background dimmed and waiting while the panel was still on its way. */
#walletPersonDialog[open]::backdrop,
#walletReceiptDialog[open]::backdrop,
#walletHistoryDialog[open]::backdrop{
  animation:waScrim 620ms var(--wa-enter,cubic-bezier(.16,1,.3,1)) both;
}

/* Leaving stays quick, and now leaves the way it came. The shared rule slid
   these DOWN, which fought an entrance that came from the side. Kept under the
   320ms failsafe in closeDialog, or the dialog would be forced shut mid-slide. */
#walletPersonDialog.is-closing, #walletReceiptDialog.is-closing{
  animation:waPersonOut 260ms var(--wa-exit,cubic-bezier(.4,0,1,1)) both;
}
@keyframes waPersonOut{
  from{opacity:1;transform:none}
  to{opacity:0;transform:translateX(26px)}
}
#walletHistoryDialog.is-closing{
  animation:waSheetDown 260ms var(--wa-exit,cubic-bezier(.4,0,1,1)) both;
}

@media (prefers-reduced-motion: reduce){
  /* waFadeOnly, not none: every other wallet sheet fades over 120ms with motion
     turned down, and these three popping instantly would be the odd ones out.
     The keyframes live in styles.css inside this same query, so they are
     available exactly when this rule applies. */
  #walletPersonDialog[open], #walletReceiptDialog[open],
  #walletHistoryDialog[open]{animation:waFadeOnly 120ms linear both}
  /* closeDialog already skips the exit animation when motion is reduced, but
     these rules sit later in the cascade than the ones in styles.css that
     cancel it — so without this they would win if that guard ever moved. */
  #walletPersonDialog.is-closing, #walletReceiptDialog.is-closing,
  #walletHistoryDialog.is-closing{animation:none}
  /* The scrim was left out of this and kept its 620ms fade, so on a phone with
     motion turned down the page snapped in while the background was still
     darkening behind it. A fade is not motion, but it still has to agree with
     what it is fading behind. */
  #walletPersonDialog[open]::backdrop,
  #walletReceiptDialog[open]::backdrop,
  #walletHistoryDialog[open]::backdrop{animation:waScrim 120ms linear both}
}

/* The receipt, full screen. The card keeps its own edges and sits on the page
   with room around it — a document, which is what it is. The buttons moved
   OUT of the card: they are things you do about the receipt, not part of it,
   and printing them inside made the card look like it ran on forever. */
.receipt-scroll{
  flex:1;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;
  padding:18px 16px calc(28px + env(safe-area-inset-bottom));
}
.receipt-scroll .receipt{max-width:520px;margin:0 auto}
.receipt-actions{
  display:flex;flex-direction:column;gap:10px;
  max-width:520px;margin:18px auto 0;
}
.receipt-actions .primary-button,
.receipt-actions .secondary-button{
  min-height:54px;font-size:calc(16.5px*var(--fc-text));font-weight:600;
}
.receipt-actions .person-chip{min-height:46px}



/* ==========================================================================
   THE GAMES BUTTON  —  findable, then out of the way
   --------------------------------------------------------------------------
   It sat in a row of grey header icons and read as decoration. Customers were
   being talked through finding it in chat, one at a time, which is the clearest
   possible sign it was invisible.

   Two changes, and deliberately no more: a filled shape so it stops being a
   grey glyph among grey glyphs, and — for somebody who has never opened it —
   a slow breath and a short label pointing at it. Both stop the moment it is
   tapped once, and never come back on any device.
   ========================================================================== */
.game-button{
  position:relative;
  background:linear-gradient(140deg,#7c4dff,#e8579a);
  border-radius:50%;
  box-shadow:0 2px 10px color-mix(in srgb,#7c4dff 45%,transparent);
}
/* styles.css builds the old look by spinning a rainbow halo behind the button
   and covering the middle with a panel-coloured disc — .game-glow plus
   .game-button::after. The gradient above replaces that whole design, so both
   have to go.
   They were left in place at first, and the breathing-ring rule happened to
   override the disc, so it looked right. The first tap removed the marker class,
   the disc came back over the gradient, and with the emoji sitting at the same
   z-index the disc covered that too: a panel-coloured circle on a
   panel-coloured bar. The button had not moved. It had been painted out. */
.game-button::after{content:none}
.game-button .game-glow{display:none}

.game-button .game-emoji{
  /* Above the ring, whether or not the ring is there. */
  position:relative;z-index:2;
  font-size:calc(20px*var(--fc-text));
  filter:drop-shadow(0 1px 1px rgba(0,0,0,.35));
}
.game-button:active{transform:scale(.94)}

/* The breath. Only while it has never been opened, and gentle enough to catch
   the eye without pulling at it — this sits above a chat somebody is reading. */
.game-button.games-live::after{
  content:'';position:absolute;inset:-4px;border-radius:50%;
  border:2px solid color-mix(in srgb,#e8579a 80%,transparent);
  animation:gameBreath 1.9s ease-out infinite;
  pointer-events:none;
}
/* The ripple rides on top of the button's own scale, so it reaches further
   than 1.5 suggests. It is a 2px line that is already fading by the time it
   crosses anything, which is why it can be this wide without making a mess. */
@keyframes gameBreath{
  0%{transform:scale(1);opacity:.95}
  55%,100%{transform:scale(1.5);opacity:0}
}

/* The button itself breathes with the ring. The ring alone is a thin outline
   two shades off the header — it reads at a glance but not out of the corner
   of an eye, which is where this has to work: the customer is reading a chat,
   not auditing the header. A shape that changes SIZE gets noticed there.

   Slow on purpose. Fast is an alarm; this is an invitation, and it may be on
   screen for days before anybody taps it. The shadow swells with the scale so
   it reads as one thing lifting rather than a button twitching. */
.game-button.games-live{
  animation:gamePulse 1.9s cubic-bezier(.34,1.4,.64,1) infinite;
  /* It now grows wider than the gap to its neighbours, so it has to grow OVER
     them rather than under. */
  z-index:3;
  /* Promoted to its own layer: this repaints continuously above a scrolling
     chat, and on a mid-range Android that is the difference between a swell
     and a stutter. */
  will-change:transform;
}
/* A double beat, then a rest — not a breath.
   A sine in and out is what a UI does when it does not really want anything;
   the eye adapts to it in seconds and it stops registering. Two quick hits
   followed by nearly a second of stillness reads as something asking, and it
   cannot be tuned out the same way, because the rest keeps resetting it. */
@keyframes gamePulse{
  0%      {transform:scale(1)    translateY(0);
           box-shadow:0 2px 10px color-mix(in srgb,#7c4dff 45%,transparent)}
  14%     {transform:scale(1.24) translateY(-3px);
           box-shadow:0 10px 26px color-mix(in srgb,#e8579a 75%,transparent)}
  26%     {transform:scale(1.02) translateY(0);
           box-shadow:0 2px 10px color-mix(in srgb,#7c4dff 45%,transparent)}
  38%     {transform:scale(1.18) translateY(-2px);
           box-shadow:0 8px 22px color-mix(in srgb,#e8579a 65%,transparent)}
  52%,100%{transform:scale(1)    translateY(0);
           box-shadow:0 2px 10px color-mix(in srgb,#7c4dff 45%,transparent)}
}

/* The joystick rocks inside the disc, on a cycle twice as long as the breath.
   Two motions on the same period lock together and become one movement the eye
   files away after a minute; offset, they keep catching it. Most of this
   keyframe is deliberately still — the rock happens once per pass, then waits. */
/* The joystick shakes on the same beat as the disc, deliberately in sync.
   The earlier version offset them so the movement stayed quiet — the point now
   is the opposite, so they land together as one hit and the glyph settles
   through a few decaying swings rather than snapping back. */
.game-button.games-live .game-emoji{
  animation:gameRock 1.9s cubic-bezier(.34,1.4,.64,1) infinite;
}
@keyframes gameRock{
  0%      {transform:rotate(0deg)   scale(1)}
  10%     {transform:rotate(-18deg) scale(1.2)}
  22%     {transform:rotate(15deg)  scale(1.15)}
  33%     {transform:rotate(-10deg) scale(1.1)}
  44%     {transform:rotate(6deg)   scale(1.05)}
  54%,100%{transform:rotate(0deg)   scale(1)}
}

/* A press still has to feel like a press. A running animation beats an ordinary
   declaration on the same property, so both :active rules — the button's
   scale(.94) in this file and the emoji's scale(.85) in styles.css — would be
   silently ignored while the pulse runs. Stopping the animations hands the
   property back. It only matters for a press that never completes: a real tap
   clears .games-live and everything here stops. */
.game-button.games-live:active{animation:none;transform:scale(.94)}
.game-button.games-live:active .game-emoji{animation:none}

/* The label. Says what is behind the button rather than "tap here" — somebody
   who does not want games should be able to read it and decide not to. */
/* Named fc-games-coach, NOT game-hint: .game-hint already exists 58 times
   across this app as ordinary centred help text, and styling it as a floating
   dark tooltip turned every one of those into a mess. Caught only because a
   browser check read back the wrong element's words. */
.fc-games-coach{
  position:absolute;z-index:60;
  padding:8px 12px;border-radius:12px;
  background:#1c1c1e;color:#fff;
  font-size:calc(13.5px*var(--fc-text));font-weight:600;
  box-shadow:0 8px 24px rgba(0,0,0,.35);
  animation:gameHintIn 420ms cubic-bezier(.16,1,.3,1) both;
  pointer-events:none;max-width:62vw;
}
.fc-games-coach::before{
  content:'';position:absolute;top:-5px;width:10px;height:10px;
  background:inherit;transform:rotate(45deg);
  /* Follows the button, not the label: on a narrow phone the label gets nudged
     inwards to stay on screen and a centred arrow would point at nothing. */
  left:calc(var(--arrow-x, 50%) - 5px);
}
@keyframes gameHintIn{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}

@media (prefers-reduced-motion: reduce){
  .game-button.games-live::after{animation:none;opacity:.7}
  .fc-games-coach{animation:none}
  /* Somebody who has asked their phone to stop moving things has usually asked
     for a reason. The button still has to stand out, so it keeps the larger
     size and the brighter shadow — it just holds them still instead of
     breathing. */
  .game-button.games-live{animation:none;transform:scale(1.12);
    box-shadow:0 6px 20px color-mix(in srgb,#e8579a 70%,transparent)}
  .game-button.games-live .game-emoji{animation:none}
  .game-button.games-live::after{transform:scale(1.12);opacity:.9}
}
