:root{
  /* PARCHI - dark app, paper receipt.

     The interface is dark because a payments app lives on a phone at a
     counter, at night, one-handed. The parchi itself stays paper: a receipt
     is a physical white slip, and on a dark ground it reads like one - the
     brightest thing on the screen is the thing you were given.

     Blue is the action. On dark it lifts to a mid blue so a filled button
     separates from the ground and still carries white text.
     Yellow is the joy - scanner brackets, sparkles, the mark, the "Paid!"
     moment. On dark it finally has room: 13:1 on the ground. It still never
     carries a control, because a yellow button reads as caution.
     Green/red stay as signals: money in, money out, failed.

     Surfaces are separated by a hairline border, not by shadow. Shadow is
     reserved for things that genuinely float - sheets, the receipt - so
     depth means something when it appears. Radii are small and consistent;
     the palette is near-neutral so the two brand colours do all the work.
     Every text pair here clears 4.5:1. */

  /* ---- the ramps ------------------------------------------------------
     Every colour below used to be a single flat value, which is why
     hover / pressed / disabled kept getting eyeballed per screen. These
     are the five ramps everything else is built from: seven steps each,
     100 lightest to 700 darkest, luminance strictly decreasing, so a
     "one step down" always means the same thing everywhere.

     The palette already *was* a ramp - it just was not written down. Every
     semantic token underneath maps onto a step here rather than inventing
     a value, and the mapping is named in the comment beside it.

     `-surface` is deliberately NOT a step. It is the hue mixed down into
     the ground for a tinted background, so it sits off the ramp and is
     only ever a fill behind something else, never a foreground. */
  --blue-100:#DFE5FC; --blue-200:#B6C4F8; --blue-300:#93A9F5;
  --blue-400:#5E7BE8; --blue-500:#3355DA; --blue-600:#2843BC;
  --blue-700:#1E3392; --blue-surface:#1C2440;

  --yellow-100:#FFF4B8; --yellow-200:#FFE97A; --yellow-300:#FFDA00;
  --yellow-400:#E0BE00; --yellow-500:#B8991E; --yellow-600:#8A7318;
  --yellow-700:#574910; --yellow-surface:#33301C;

  --green-100:#C9F2DB; --green-200:#8FE0B4; --green-300:#43C77E;
  --green-400:#2FA866; --green-500:#248851; --green-600:#1B683E;
  --green-700:#123F27; --green-surface:#16301F;

  --red-100:#FFD9D5; --red-200:#FFAEA6; --red-300:#FF6B5E;
  --red-400:#E24C3F; --red-500:#B93A2F; --red-600:#8E2B22;
  --red-700:#5C1D17; --red-surface:#3A1D19;

  /* slate carries the ground as well as the type, so it runs longer */
  --slate-100:#F0F2F8; --slate-200:#C6CBDC; --slate-300:#A2A8BC;
  --slate-400:#8A90A6; --slate-500:#767C90; --slate-600:#4A5064;
  --slate-700:#2A2F40; --slate-800:#171B29; --slate-900:#101320;
  --slate-950:#0A0C13;

  /* ---- what the ramps mean -------------------------------------------- */
  --bg:var(--slate-950);
  --surface:var(--slate-900);
  --surface-alt:var(--slate-800);
  --hairline:rgba(233,237,250,0.10);
  --hairline-2:rgba(233,237,250,0.16);
  --border:rgba(233,237,250,0.11);

  /* THE TWO-TIER ACCENT RULE. Blue and yellow are not interchangeable and
     the split is not decorative:

       BLUE  = actionable. Anything you tap that moves you forward - button
               fills, links, selected states, the pay hero. If it is blue,
               it does something.
       YELLOW = attention. Things to notice, not to press - the scan
               brackets, the onboarding progress bar, an unread dot, a
               first-time-payee flag, the mark. A yellow button would read
               as caution, so yellow never fills a control.

     One deliberate exception, and it is the only one: the torch toggle goes
     yellow when it is ON, because a torch that is on emits yellow light.
     The colour is reporting a physical state, not advertising the tap. */
  --blue:var(--blue-500);        /* button fill: 5.6:1 with white text */
  --blue-deep:var(--blue-600);   /* pressed / hover */
  --blue-bright:var(--blue-300); /* icons and links on dark: 8.6:1 on ground */
  --blue-tint:var(--blue-surface);
  --yellow:var(--yellow-300);    /* 14:1 on the ground - never a control */
  --yellow-dim:var(--yellow-500);/* yellow as *text*. Full yellow measures 14:1,
                             level with the primary type, so a note set in it
                             shouts over the thing it is annotating. 7:1 keeps
                             it a signal and puts it back below the title. */
  --yellow-soft:var(--yellow-surface);

  --ink:var(--slate-100);        /* body text: 17:1 on the ground */
  --gray:var(--slate-400);
  --text-soft:var(--slate-300);  /* 8.2:1 */
  /* A text token has to be measured against the lightest surface it will
     ever sit on, not against the ground. slate-500 cleared 4.7 on the
     ground and was then used on cards (4.45) and on the unread-notification
     tint (3.68), so two thirds of its real uses were failing a check the
     comment claimed it passed. slate-400 clears 4.5 on all three. */
  --text-faint:var(--slate-400);

  --green:var(--green-300);
  --green-soft:var(--green-surface);
  --alert:var(--red-300);   /* lifted from the light-mode red so it stays
                        legible on a dark ground, and still nowhere near
                        the brand yellow */
  --alert-soft:var(--red-surface);
  --dark:#0A0C16;

  /* paper: the parchi keeps its own light world, whatever the app does */
  --paper:#FBF8F4;
  --paper-ink:#161616;
  --paper-soft:#6B6960;
  /* 2.79:1 on paper - it was set by eye against the cream and never
     measured. #767266 is the same grey one step down and clears 4.5. */
  --paper-faint:#767266;
  /* The paper had been borrowing the DARK app's red and green, which are
     lifted specifically so they survive a near-black ground - on cream they
     measure 2.6 and 2.0. The parchi is the one screen somebody might carry
     into a bank branch, so it gets its own pair off the same ramps, chosen
     against paper: red-500 at 5.35, green-600 at 6.41. */
  --paper-alert:var(--red-500);
  --paper-green:var(--green-600);
  --paper-rule:rgba(22,22,22,0.10);
  --paper-rule-2:rgba(22,22,22,0.18);
  --paper-blue:#173A93;   /* the blue that works ON paper, not on the app */

  /* A card is defined by its edge, not by a bevel. --raise is a hairline;
     --raise-lg is the only one that actually casts, and it is for things
     that genuinely float above the page. --press just deepens the edge. */
  --raise:inset 0 0 0 1px var(--border);
  --raise-lg:inset 0 0 0 1px var(--border), 0 16px 32px -20px rgba(0,0,0,0.9);
  --press:inset 0 0 0 1px var(--hairline-2), inset 0 1px 2px rgba(0,0,0,0.4);

  /* Satoshi, self-hosted as one variable file. The stack used to lead with
     SF Pro and fall back to Inter, which meant the product looked like the
     system it ran on rather than like itself - and every export, shot on
     Windows, was Inter anyway. Satoshi is the brand face (it is what the
     brand sheet was always set in), so the app and the identity are finally
     one typeface.

     One family, two tokens. SF Pro needed a Text and a Display cut because
     it ships optical sizes; Satoshi has one design across the axis, so both
     tokens resolve to it. The names stay so call sites still read as intent
     - display where fit matters, text where reading does.

     Satoshi carries the rupee sign, which was the thing to check before
     committing: a UPI app whose currency symbol falls back to another face
     on every amount is worse off than one with a duller typeface. It has no
     Indic coverage, but neither did Inter, so the language picker falls back
     to the system exactly as it did before. */
  --font-text:'Satoshi', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  --font-display:'Satoshi', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;

  /* radius system: 12 small elements, 20 cards, 28 hero */
  /* Material 3's shape scale, adopted wholesale: 0 · 4 · 8 · 12 · 16 · 28 ·
     full. Three of the four values here were already on it, and taking the
     rest closed out every ad-hoc 6 / 9 / 10 / 14 / 18 / 20 / 26 left in the
     file - the drift that made the app read as several systems at once.

     Two rules on top of the scale, both ours:
       · a thing's corner follows its size tier
       · a nested thing steps down one tier from its parent
     M3's *component* defaults are deliberately NOT adopted - see the note
     on .btn. The scale is a measuring system; the pill buttons are a look. */
  --radius-xs:4px;      /* hairline details - the sheet handle, tiny marks */
  --radius-sm:8px;      /* icon wells and chips: nested, so one tier down */
  --radius-icon:8px;    /* alias kept so call sites still read as wells */
  --radius-ctl:12px;    /* anything you tap that is a full-height control */
  --radius-md:12px;     /* cards and rows - same tier as controls */
  --radius-lg:16px;     /* the hero, the viewfinder, the scan tile */
  --radius-xl:28px;     /* sheets - the only things that own the screen */
  --radius-full:999px;  /* pills: status chips, toggles */
}

@font-face{
  font-family:'Satoshi';
  src:url('assets/satoshi-variable.woff2') format('woff2-variations');
  font-weight:300 900;   /* one variable file covers 500/600/700/800 exactly */
  font-style:normal;
  font-display:swap;
}

*{box-sizing:border-box; -webkit-tap-highlight-color:transparent;}
html,body{margin:0;padding:0;}
/* Showcase backdrop only — the page behind the phone, never the app.
   The same dot grid the case-study deck itself uses: a plain paper ground
   with a faint 24px grid of dots, nothing decorative competing with the
   device. The device is the only thing on the page; the grid just gives it
   somewhere to sit. */
body{
  background-color:#FFFFFF;
  background-image:radial-gradient(circle, rgba(10,10,10,0.13) 1.1px, transparent 1.1px);
  background-size:24px 24px;
  /* SF Pro where it exists -- it ships with iOS and macOS and cannot be
     served as a webfont, so Inter carries everywhere else. Inter was drawn
     as an SF-alike, so the metrics and the feel survive the fallback.
     Two stacks, because SF Pro is two fonts: Text below roughly 20px, where
     the wider apertures and looser tracking keep small copy readable, and
     Display above it, where the tighter fit stops headlines looking gappy.
     `-apple-system` leads both because Apple platforms pick the optical size
     themselves; the explicit names only matter on machines where SF Pro is
     installed manually. */
  font-family:var(--font-text);
  display:flex; align-items:center; justify-content:center;
  padding:14px 12px; min-height:100vh; overflow-x:auto; overflow-y:hidden;
  /* overflow-y:hidden here is load-bearing, not tidy-up: setting only
     overflow-x leaves overflow-y at its default `visible`, and per spec a
     `visible` on one axis paired with anything else on the other computes
     to `auto` - making this flex-centered body a vertical scroll
     container nobody intended. Locked down by default because an
     autofocus() call on a field that is still off-screen mid-transition
     (a sheet sliding up) makes the browser go looking for exactly this
     kind of ancestor to scroll into view, and a flex-centered one answers
     with a few-hundred-px jump that has nothing to scroll back to. Fixed
     at both ends: focus({preventScroll:true}) so it doesn't ask, and this
     so there is nothing to answer even if something else ever does. */
}
/* fitDevice() (script.js) always fills the width and adds this class
   exactly when that makes the phone run taller than the window - at that
   point scrolling to reach the rest of it is correct (nobody minds
   scrolling down an app taller than their screen), so overflow-y turns on
   only then, and align-items switches to flex-start so the phone starts
   flush at the top rather than centred, which would leave you scrolling
   up to reach content that starts above the fold as much as down to
   reach what runs past it. On a window with room on both axes this class
   never gets added, so nothing here changes from the rule above. */
body.overflowing{ align-items:flex-start; overflow-y:auto; }
/* 506px, not the 900px breakpoint the rest of this file's mobile rules use,
   and not fitDevice()'s own width-fill threshold either - a third, wider
   number, chosen as a conservative width below which .restart-hint and
   .skip-intro stop trusting a fixed viewport inset to clear the phone and
   anchor to the device frame instead (see the media queries on each below).
   That anchoring is safe at any width - the button sits flush against the
   frame's own edge no matter how big or small the frame turns out to be -
   so 506px only needs to be wide enough that pinning to the viewport corner
   above it stays clear of the phone too, not an exact fit to fitDevice()'s
   current formula.
   Both buttons get the same top/bottom reserve here (44px = a 38px button
   plus a 6px gap), on purpose - they used to be asymmetric (bottom-only)
   back when only .restart-hint anchored to the frame and .skip-intro's
   fixed corner was considered safe on its own. Once .skip-intro anchors to
   the frame too, giving it less reserve than .restart-hint would misalign
   the two, which is the whole reason this pair exists: so they read as one
   matched pair of page-corner controls instead of two independently-placed
   buttons. Must stay equal to the -44px each button uses and the same
   506px threshold fitDevice()'s own reserve uses - if any of the three
   moves, move all three. */
@media (max-width: 506px){ body{ padding-top:44px; padding-bottom:44px; } }
/* The dot grid was static texture, nothing more. This is the same grid,
   same colour, same 24px spacing - not a second pattern, not a colour
   gradient (the standing "no gradients, no glow" rule is about colour;
   this never introduces one) - just a second, brighter copy of the exact
   dots, revealed only inside a soft circle that follows the pointer via
   a mask, so the page notices you without ever drawing a shape that isn't
   already there. No-op on touch: there is no cursor to follow, and below
   900px there is no page margin around the phone to see it in anyway. */
body::after{
  /* z-index:-1, not 0 or unset - .device has no explicit z-index of its own,
     and this must never paint over it (or the two page-chrome buttons) if
     the pointer happens to be resting over the phone itself when it moves */
  content:""; position:fixed; inset:0; z-index:-1; pointer-events:none;
  /* must align exactly with body's own dot grid below - same size, same
     position (both default to the box's top-left origin), so the brighter
     copy lands precisely on top of the dots it is meant to highlight
     rather than drawing a second, offset grid */
  background-image:radial-gradient(circle, rgba(10,10,10,0.5) 1.1px, transparent 1.1px);
  background-size:24px 24px;
  -webkit-mask-image:radial-gradient(circle 180px at var(--mx,-999px) var(--my,-999px), #000 0%, transparent 100%);
  mask-image:radial-gradient(circle 180px at var(--mx,-999px) var(--my,-999px), #000 0%, transparent 100%);
}
button{ font-family:inherit; border:none; background:none; padding:0; margin:0; text-align:inherit; color:inherit; }
input{ font-family:inherit; }

/* Lives on the page, not in the device, so it never counts as a screen -
   a small escape hatch for a demo visitor who just wants the app, not the
   twelve-step registration. Top-right, clear of the phone's own bottom
   controls (Continue, Done, Pay) rather than bottom-centre, which on any
   window not much taller than the device itself sat on top of them.
   Fades away the moment onboarding finishes on its own, and comes back if
   setup is ever replayed. Used to hide entirely below 900px wide, on the
   reasoning that there was no margin beside the phone to put it in and the
   product's own in-onboarding Skip link covered a real phone anyway - that
   link was later removed as a portfolio distraction (it showed on every
   exported onboarding screen for no reason a case study needed), which
   made this the only way to skip left, so it can no longer just disappear
   there. Text on desktop; the mobile rule below collapses it to an
   icon-only tap target the same way .restart-hint does. */
.skip-intro{
  position:fixed; top:22px; right:22px;
  z-index:60; font-family:var(--font-text); font-size:12.5px; font-weight:600;
  /* was a white pill on the page's own near-white dot-grid ground - barely
     readable at a glance. Dark, matching the app it sits beside, reads
     clearly against the light background instead. */
  color:#F2F0EA; background:var(--dark); border:1px solid rgba(255,255,255,0.12);
  padding:10px 16px; border-radius:999px; cursor:pointer;
  box-shadow:0 10px 24px -12px rgba(10,10,10,0.45);
  transition:opacity .22s ease, transform .22s ease, background .14s ease;
}
.skip-intro svg{ display:none; }
.skip-intro:hover{ background:#171A26; }
.skip-intro:active{ transform:scale(0.97); }
.skip-intro.hidden{ opacity:0; pointer-events:none; transform:translateY(-8px); }
@media (max-width: 900px){
  .skip-intro{
    width:38px; height:38px; padding:0; top:14px; right:14px;
    display:flex; align-items:center; justify-content:center;
    font-size:0;
  }
  .skip-intro svg{ display:block; width:17px; height:17px; stroke:#F2F0EA; }
}
/* Mirrors .restart-hint's own 506px rule below exactly - same frame,
   same right:0, same 44px offset, just flipped to the top edge - so the
   two read as a matched pair instead of drifting apart once the phone
   stops filling the width and .restart-hint (below) switches off the
   viewport corner onto the frame's own edge. See the fuller reasoning on
   the body media query above .device-frame's rule, and on .restart-hint's
   own rule below. */
@media (max-width: 506px){
  .skip-intro{ position:absolute; right:0; top:-44px; bottom:auto; }
}

/* Bottom-right rather than under the skip button, so the two never compete
   for the same corner. On desktop this is prose naming a keyboard shortcut,
   not really a click target (the shortcut is the point) - it is a <button>
   underneath either way, but cursor stays default here and only becomes a
   real tap target in the mobile rule below. Icon hidden by default: it only
   shows once there is no shortcut to name and the button IS the whole
   feature (see the mobile rule). */
.restart-hint{
  position:fixed; bottom:22px; right:22px; cursor:default;
  z-index:60; font-family:var(--font-text); font-size:12px; font-weight:600;
  color:#F2F0EA; background:var(--dark); border:1px solid rgba(255,255,255,0.12);
  padding:9px 14px; border-radius:999px;
  box-shadow:0 10px 24px -12px rgba(10,10,10,0.45);
  opacity:0.82;
}
.restart-hint svg{ display:none; }
/* Below 900px there is no keyboard to use the text hint's own shortcut with,
   so the text itself is dead weight - but the underlying need (get back to
   a clean demo) is if anything more common on a phone, since that is where
   a visitor is most likely to just be poking around. Same corner, same
   dark treatment, collapsed to an icon-only tap target instead of prose. */
@media (max-width: 900px){
  .restart-hint{
    width:38px; height:38px; padding:0; bottom:14px; right:14px;
    display:flex; align-items:center; justify-content:center;
    font-size:0; opacity:1; cursor:pointer;
  }
  .restart-hint svg{ display:block; width:17px; height:17px; stroke:#F2F0EA; }
}
/* 426px is a different, tighter threshold than the 900px rule above, and
   deliberately so: below 900px .restart-hint only changes its STYLING
   (icon instead of text), and that alone would suggest 426px (fitDevice()'s
   own width-fill threshold in script.js) as this rule's cutoff too. It
   isn't: the phone is still centred, not full-width, above 426px, but the
   margin beside it does not clear the button's own 38px plus its 14px
   inset until the gap is at least 104px wider than the phone itself -
   506px, not 426. `position:fixed` at a viewport corner is safe anywhere
   above that.
   Below 506px, one or both of two things stops being true - full margin,
   or a window tall enough that the phone does not run past it - and
   `position:fixed` cannot account for either, because a fixed element sits
   at a constant point on the VIEWPORT with no idea what part of a
   scrolled, over-height phone currently occupies that point, or how close
   a centred one sits to the edge. On a real device this put the button
   directly on top of the Broadband tile in Quick Actions, meaning a tap
   meant for the app could land on the demo's own restart control instead -
   and the same fixed-corner logic put it somewhere different, but just as
   accidental, on every other screen. .skip-intro gets the mirrored version
   of this same rule (see just above it) - it never had the scroll-overlap
   problem restart-hint had, since it only shows during onboarding and the
   top-right corner there is always the status bar's own icons, never a
   real control, but it still needs to move once .restart-hint moves or the
   two end up anchored to two different edges (viewport vs. frame) and
   drift visibly out of alignment with each other - which is exactly what
   shipped briefly and had to be corrected.
   The fix is to stop being fixed to the viewport and instead sit relative
   to the device itself: the button is a DOM child of .device-frame
   (index.html), so `position:absolute` here resolves against that frame,
   not the page. Placed just outside its bottom edge (-44px = the button's
   38px plus a 6px gap) with `right:0` to align with the frame's own right
   edge, it now scrolls together with the device as one unit and sits in
   the empty page margin reserved for exactly this (44px on body's bottom
   padding - see the media query near .overflowing above, which must stay
   in sync with the -44px here). Wherever the device scrolls to, the button
   scrolls with it, so it can never land on top of whatever app screen
   happens to be showing. */
@media (max-width: 506px){
  .restart-hint{ position:absolute; right:0; bottom:-44px; top:auto; }
}

/* Top-left, answering the one question nothing else on the page did: what
   is this. No pill/card treatment like the two above - those are UI chrome
   (a button, a shortcut hint), this is closer to a strapline, so it sits
   directly on the page the way a headline would. */
.demo-intro{
  position:fixed; top:26px; left:28px; z-index:60; max-width:280px;
}
.demo-mark{
  display:flex; align-items:center; gap:8px;
  font-family:var(--font-display); font-size:19px; font-weight:800;
  letter-spacing:-0.02em; color:#0F1220;
}
.demo-mark img{ display:block; }
.demo-intro p{
  margin:8px 0 0; font-family:var(--font-text); font-size:13px; line-height:1.5;
  font-weight:500; color:#5E5E5E;
}

/* ---------- first paint ----------
   Everything on the page used to just appear. A short, staggered rise on
   load - branding first, phone right after, the two page-chrome buttons
   trailing - reads as one considered entrance instead of a document that
   finished parsing. Each keeps `both` so the element holds its start/end
   state outside the animation's own window, and nothing here touches
   .device's children - the sheet/view transitions inside the phone are
   untouched and start from their own normal resting state regardless. */
@keyframes pageIn{ from{ opacity:0; transform:translateY(10px); } to{ opacity:1; transform:none; } }
.demo-intro{ animation:pageIn .6s cubic-bezier(.22,.8,.28,1) both; }
.skip-intro{ animation:pageIn .6s cubic-bezier(.22,.8,.28,1) .16s both; }
.restart-hint{ animation:pageIn .6s cubic-bezier(.22,.8,.28,1) .16s both; }
/* .device gets no entrance animation at all, not even opacity. This dates
   back to when fitDevice() (script.js) scaled the phone with a live,
   JS-controlled `zoom` instead of `transform` (see the .device-frame
   comment there for why that changed) - running ANY animation on an
   element at the same moment its `zoom` was set or changing was a real,
   if intermittent, Chromium rendering bug: the two composed into a
   genuinely skewed or rotated paint instead of the plain motion the
   animation asked for. fitDevice() no longer touches `zoom` at all, which
   likely lifts this restriction, but there is no upside urgent enough to
   risk re-verifying that right before a release - the phone simply being
   present the instant fitDevice() has sized it works fine as-is. */
.device{ animation:none; }

/* Mobile overrides pageIn with its own animation below - must come after
   the block above in source order, since both set the same property on
   the same selector at equal specificity and the later one wins. There is
   no spare margin beside the phone on a real phone screen (it fills the
   width within about 10-18px either side) to hold this permanently the
   way desktop does, so it does not try to: a full-width bar over the top
   of the phone for the same beat the product's own splash already runs
   (OB_STEPS[0].auto, 1100ms in script.js), then it slides up and out
   rather than sitting on top of "Choose your language" underneath. Timed
   a little past that so it clears before the next screen, not during. */
@media (max-width: 900px){
  .demo-intro{
    display:flex; flex-direction:column; align-items:center; text-align:center;
    top:0; left:0; right:0; max-width:none;
    padding:16px 20px 14px; background:var(--dark);
    animation:introSplash 1.7s cubic-bezier(.32,.72,0,1) both;
  }
  .demo-mark{ color:#F2F0EA; }
  .demo-intro p{ color:rgba(242,240,234,0.68); max-width:34ch; }
  @keyframes introSplash{
    0%, 70%{ opacity:1; transform:translateY(0); }
    100%{ opacity:0; transform:translateY(-100%); }
  }
  @media (prefers-reduced-motion:reduce){ .demo-intro{ animation:none; display:none; } }
}
@media (prefers-reduced-motion:reduce){
  .demo-intro, .skip-intro, .restart-hint{ animation:none; }
}

/* The flex child body actually centres. fitDevice() (script.js) sets this
   to the device's current on-screen footprint (402x874 times whatever
   scale it lands on), so the flex layout only ever reserves exactly the
   space the (visually scaled) phone occupies - .device inside stays a
   constant, unscaled 402x874 box and is scaled with `transform`, not
   resized, so this wrapper's own box is what actually shrinks.
   This used to be one element: .device itself carried both the sizing
   and a live `zoom` (script.js set `el.style.zoom` directly). Two
   independent real bugs traced back to that: a phone that rendered
   visibly skewed when its entrance animation and a `zoom` write landed
   in the same frame, and - the one that sent this to a second attempt -
   a hairline seam between the screen and its own bezel that only showed
   at specific combinations of the browser's own page-zoom (Ctrl +/-) and
   this element's zoom, because both stack through the same non-standard
   `zoom` pipeline. `transform: scale()` doesn't compound with page zoom
   the way `zoom` does and has none of `zoom`'s other quirks - the reason
   it wasn't used from the start (see old comment this replaced) is that
   transform does not shrink an element's own layout box, which would
   have left the *unscaled* 874px reserved in the flex layout regardless
   of how small the phone actually rendered. That's exactly what this
   wrapper solves: it carries the reserved space, .device carries the
   transform, and the two are always kept in sync by fitDevice(). */
.device-frame{ position:relative; }
.device{
  width:402px; height:874px;   /* iPhone 16 Pro, logical - never resized, only ever transform-scaled */
  transform-origin:top left;
  background:var(--bg);
  border-radius:44px; overflow:hidden; position:relative;
  /* bezel, hairline, then one honest shadow cast downward — a halo around
     the whole device reads as a glow effect, not as weight on a surface */
  box-shadow:
    0 0 0 10px #0A0C12,
    0 0 0 11px rgba(233,237,250,0.10),
    0 34px 70px -24px rgba(0,0,0,0.85),
    0 8px 22px -12px rgba(0,0,0,0.7);
}

/* A phone is 402x874 and most windows are not, so fitDevice() in script.js
   scales it to fit instead of making you scroll past a clipped phone or
   overflow past a narrow one - not a CSS breakpoint, because it picks
   which axis to fit depending on which one is actually short: a wide
   landscape window (any ordinary laptop) shrinks by height so the whole
   device is visible with nothing to scroll for; a narrow one (a real
   phone screen) fills width instead and lets the page scroll for the
   rest, rather than shrinking small enough to also satisfy height. No
   fixed set of breakpoints could do that for every aspect ratio a real
   window comes in, which is why this lives in JS and not here. */

/* ---------- screens ---------- */
.view{
  position:absolute; inset:0; background:var(--bg);
  display:flex; flex-direction:column;
  transform:translateX(100%);
  transition:transform .36s cubic-bezier(.32,.72,0,1);
}
.view-home{ transform:translateX(0); z-index:1; }
.view-scan{ z-index:2; background:var(--dark); }
.view-amount{ z-index:3; }
.view-success{ z-index:4; }
.view-history{ z-index:5; }
.view-account{ z-index:6; }
.view-notif{ z-index:7; }
/* every view needs an explicit layer. Without one it falls to z-index:auto
   and slides in *behind* home, which looks exactly like a dead button. */
.view-khata{ z-index:8; }
.view-devices{ z-index:9; }
.view.active{ transform:translateX(0); }

.view-home .scroll{ position:relative; z-index:1; }

.scroll{ flex:1; overflow-y:auto; overflow-x:hidden; scrollbar-width:none; padding-bottom:104px; }
.scroll::-webkit-scrollbar{display:none;}

.statusbar{
  display:flex; justify-content:space-between; align-items:center;
  padding:18px 26px 4px; flex-shrink:0;
  font-size:14px; font-weight:600; color:var(--ink);
}
.statusbar.light{ color:#FFFFFF; }
.statusbar .icons{display:flex; align-items:center;}

.stagger{ animation:rise .5s cubic-bezier(.22,.8,.28,1) both; animation-delay:calc(var(--i) * 45ms); }
@keyframes rise{ from{ opacity:0; transform:translateY(10px); } to{ opacity:1; transform:none; } }

/* ============================================================
   1. home
   ============================================================ */
.app-header{ display:flex; align-items:center; gap:12px; padding:12px 20px 18px; }
.avatar-btn{
  width:42px;height:42px;border-radius:50%; flex-shrink:0; overflow:hidden;
  border:1.5px solid var(--hairline-2); cursor:pointer; line-height:0;
  transition:transform .14s ease, border-color .18s ease;
  box-shadow:var(--raise);
}
.avatar-btn img{ display:block; width:100%; height:100%; }
.avatar-btn:hover{ border-color:var(--blue); }
.avatar-btn:active{ transform:scale(0.92); }
.search-bar{
  flex:1; display:flex; align-items:center; gap:9px;
  height:42px; padding:0 15px; border-radius:var(--radius-ctl);
  background:var(--surface); border:1px solid var(--hairline);
  color:var(--text-faint); cursor:pointer; text-align:left;
  box-shadow:var(--raise);
  transition:background .16s ease, transform .12s ease;
}
.search-bar:hover{ background:var(--surface-alt); }
.search-bar:active{ transform:scale(0.98); }
.search-bar span{ font-size:13.5px; font-weight:500; color:var(--text-soft); }
.ghost-btn{
  width:42px;height:42px;border-radius:var(--radius-ctl);
  background:var(--surface); border:1px solid var(--hairline);
  box-shadow:var(--raise);
  display:flex;align-items:center;justify-content:center;
  cursor:pointer; position:relative;
  transition:background .16s ease, transform .12s ease;
}
.ghost-btn svg{ stroke:var(--ink); }
.ghost-btn:hover{ background:var(--surface-alt); }
.ghost-btn:active{ transform:scale(0.93); }
.dot-badge{
  position:absolute; top:8px; right:8px; width:8px;height:8px;border-radius:50%;
  background:var(--alert); border:2px solid var(--surface);
  animation:pulse 2.4s ease-in-out infinite;
}
.dot-badge.hidden{ display:none; }
@keyframes pulse{ 0%,100%{ box-shadow:0 0 0 0 rgba(194,65,12,0.4);} 50%{ box-shadow:0 0 0 5px rgba(194,65,12,0);} }

/* Scan hero — the strongest thing on the screen, and the only large field
   of colour in the app. Flat blue: it is big enough to carry weight without
   a gradient, and the yellow brackets inside it are the only ornament. */
.scan-hero{
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  text-align:center; gap:14px;
  width:calc(100% - 40px); margin:0 20px;
  padding:24px 20px 22px; min-height:175px; border-radius:var(--radius-lg);
  /* a shallow lift rather than a gradient wash: light lands on the top edge
     and falls away, which is what makes the panel read as a surface with
     something sitting on it instead of a rectangle of colour */
  background:
    radial-gradient(120% 90% at 50% -20%, rgba(255,255,255,0.16), rgba(255,255,255,0) 60%),
    linear-gradient(180deg, #3D5EE4 0%, var(--blue) 58%, #2C49C4 100%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.22),
    inset 0 0 0 1px rgba(255,255,255,0.10);
  cursor:pointer; position:relative; overflow:hidden;
  transition:transform .14s cubic-bezier(.3,.8,.3,1), box-shadow .18s ease;
}
.scan-hero:active{ transform:scale(0.97); }
/* The brackets used to float on the flat blue with nothing holding them, and
   their 6px corner against a 3.5px stroke read as a mis-struck arc. Now they
   sit on a piece of frosted glass - the viewfinder becomes an object on the
   panel rather than a decal printed on it, and the corner radius is tuned to
   the stroke so the turn actually looks drawn. */
.scan-frame{
  position:relative; width:94px; height:94px; flex-shrink:0;
  display:flex; align-items:center; justify-content:center;
  border-radius:var(--radius-lg);
  background:rgba(255,255,255,0.10);
  backdrop-filter:blur(9px) saturate(130%);
  -webkit-backdrop-filter:blur(9px) saturate(130%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.34),
    inset 0 0 0 1px rgba(255,255,255,0.16),
    0 6px 18px -8px rgba(6,12,40,0.45);
}
.scan-frame .corner{
  position:absolute; width:21px; height:21px;
  border:3px solid var(--yellow);
  animation:cornerBreathe 3.6s ease-in-out infinite;
}
.scan-frame .corner.tl{ top:17px; left:17px;  border-right:0; border-bottom:0; border-radius:8px 0 0 0; }
.scan-frame .corner.tr{ top:17px; right:17px; border-left:0;  border-bottom:0; border-radius:0 8px 0 0; animation-delay:.15s; }
.scan-frame .corner.bl{ bottom:17px; left:17px;  border-right:0; border-top:0; border-radius:0 0 0 8px; animation-delay:.3s; }
.scan-frame .corner.br{ bottom:17px; right:17px; border-left:0;  border-top:0; border-radius:0 0 8px 0; animation-delay:.45s; }
/* a shallow breath, not a blink - it should feel alive, not urgent */
@keyframes cornerBreathe{ 0%,100%{opacity:1;} 50%{opacity:.72;} }
.scan-copy{ display:flex; flex-direction:column; align-items:center; gap:4px; text-align:center; }
.scan-copy .s1{ font-size:25px; font-weight:800; color:#FFFFFF; letter-spacing:-0.025em; }
.scan-copy .s2{ font-size:13px; font-weight:500; color:rgba(255,255,255,0.85); }
.scan-go{ display:none; }

/* buttons
   Material's shape *scale* is adopted; Material's button *default* is not.
   M3 ships buttons at `full` - the stadium pill - and on this app that
   would be a costume: the whole system is flat hairline rectangles carrying
   a paper receipt, and pill buttons would make it read as stock Android
   wearing a parchi. Twelve is the control tier, and the control tier is
   what a button is. A scale is a measuring system; a component default is
   somebody else's taste. */
.btn{
  display:flex; align-items:center; justify-content:center; gap:8px;
  position:relative;
  padding:15px 0; border-radius:var(--radius-ctl);
  font-size:13.5px; font-weight:700; letter-spacing:-0.005em; cursor:pointer;
  transition:transform .12s ease, background .16s ease, box-shadow .18s ease;
}
/* Three states, and the distance between them is value and shape, never
   opacity. A 60%-alpha blue and a 100% blue read the same at a glance on a
   near-black ground - the gap has to be perceptual, not numeric.

     enabled  filled blue-500, white text, a blue-400 hairline on top so the
              button has a lit edge and separates from the ground
     disabled unfilled: it drops to the surface tier and keeps only a
              hairline. A filled thing and an outlined thing are never
              confused for one another the way two blues are
     loading   a third treatment, not a reuse of either: the fill stays
              (the button is still live, it is working) but the label is
              replaced by a spinner and the fill steps back one to blue-600

   blue-400 is not used as the fill even though it separates better,
   because white on blue-400 is under 4.5:1 and the label matters more
   than the edge. The edge is bought with the inset instead. */
.btn-primary{
  background:var(--blue); color:#FFFFFF;
  box-shadow:inset 0 1px 0 rgba(255,255,255,0.16),
             inset 0 0 0 1px var(--blue-400);
}
.btn-primary:hover{ background:var(--blue-deep); }
.btn-primary:active{ box-shadow:var(--press); transform:translateY(1px); }
.btn-primary:disabled{
  background:transparent; color:var(--text-faint);
  box-shadow:inset 0 0 0 1px var(--hairline); cursor:default;
}
.btn-primary.is-loading{
  background:var(--blue-deep); color:transparent; cursor:default;
  box-shadow:inset 0 0 0 1px var(--blue-500);
}
.btn-primary.is-loading::after{
  content:""; position:absolute;
  width:16px; height:16px; border-radius:50%;
  border:2px solid rgba(255,255,255,0.28);
  border-top-color:#FFFFFF;
  animation:btnSpin .7s linear infinite;
}
.btn-primary.is-loading > *{ visibility:hidden; }
@keyframes btnSpin{ to{ transform:rotate(360deg); } }
.btn-quiet{ background:transparent; color:var(--ink); box-shadow:inset 0 0 0 1px var(--hairline-2); }
.btn-quiet:hover{ background:var(--surface-alt); }
.btn-quiet:active{ box-shadow:var(--press); transform:translateY(1px); }
.btn:active:not(:disabled){ transform:scale(0.97); }
.btn-block{ width:100%; }
/* one button, not two. Scanning is the hero and this is the other way to
   pay; Receive had three doors already and receiving is a merchant habit,
   not a consumer one. */
.pay-row{ display:grid; grid-template-columns:1fr; gap:11px; padding:14px 20px 0; }

.row-card{
  display:flex; align-items:center; gap:8px;
  width:calc(100% - 40px); margin:11px 20px 0;
  padding:8px; border-radius:var(--radius-md);
  background:var(--surface); border:1px solid var(--hairline);
  box-shadow:var(--raise);
}
.rc-main{
  flex:1; display:flex; align-items:center; gap:12px; min-width:0;
  padding:6px; cursor:pointer; text-align:left; transition:opacity .12s ease;
}
.rc-main:active{ opacity:.6; }
/* Two identical cards sitting on top of each other had two different icon
   treatments - a solid blue tile and a grey one - which is the kind of
   mismatch that makes a screen feel assembled from parts. Peers look alike.
   The quiet well also keeps blue spent on the hero and the buttons, where
   it means "act", instead of scattering it down the page. */
.rc-ic{
  width:42px;height:42px;border-radius:var(--radius-icon); flex-shrink:0;
  background:var(--surface-alt); color:var(--blue-bright);
  display:flex;align-items:center;justify-content:center;
}
.rc-txt{ flex:1; min-width:0; display:flex; flex-direction:column; gap:2px; }
.rc-1{ font-size:13.5px; font-weight:700; color:var(--ink); }
.rc-2{ font-size:11px; font-weight:500; color:var(--text-faint); }
/* separate tap target from "check balance" — this one picks the account */
.rc-bank-select{
  width:36px; height:36px; border-radius:var(--radius-icon); flex-shrink:0;
  background:var(--surface-alt); color:var(--text-soft);
  display:flex; align-items:center; justify-content:center;
  cursor:pointer; transition:background .14s ease, transform .12s ease;
}
.rc-bank-select:hover{ background:var(--hairline-2); }
.rc-bank-select:active{ transform:scale(0.9); }

/* sections */
.section{ padding:22px 20px 0; }
.section-head{ display:flex; justify-content:space-between; align-items:center; margin-bottom:13px; }
.section-title{ font-size:15px; font-weight:700; color:var(--ink); letter-spacing:-0.015em; }
.section-link{
  display:flex; align-items:center; gap:2px;
  font-size:12px; font-weight:600; color:var(--gray);
  padding:9px 6px; margin:-9px -6px; cursor:pointer; transition:color .14s ease;
}
.section-link:hover{ color:var(--ink); }

/* fixed 4-col grid, wraps to a real second row — nothing scrolls or
   bleeds off the right edge */
/* A 1fr grid centres each 48px icon inside its own column, which adds an
   extra ~10px inset on top of the container's own side padding - so the
   outer icons sat further from the edge than the search bar and cards
   above them, which run flush to that same padding. Two explicit rows with
   space-between fixes both complaints at once: the outer icon's edge lands
   on the exact same line as everything above it, and the freed-up space
   becomes real gap between icons instead of being wasted as symmetric
   per-column inset. */
.quick-scroll{ display:flex; flex-direction:column; gap:14px; padding:2px 20px 4px; }
.quick-row{ display:flex; justify-content:space-between; }
.quick-chip{ display:flex; flex-direction:column; align-items:center; gap:8px; cursor:pointer; }
.quick-chip .ic{
  position:relative;
  width:48px;height:48px;border-radius:var(--radius-ctl); margin:0 auto;
  background:var(--surface); border:1px solid var(--hairline);
  box-shadow:var(--raise);
  display:flex;align-items:center;justify-content:center;
  transition:transform .16s cubic-bezier(.3,.8,.3,1), box-shadow .18s ease;
}
.quick-chip .ic svg{ stroke:var(--ink); }
.quick-chip:hover .ic{ background:var(--surface-alt); }
.quick-chip:active .ic{ transform:scale(0.9); }
.quick-chip .lb{ font-size:10.5px; font-weight:600; color:var(--text-soft); text-align:center; }

/* transaction rows are separated by a perforation, not a hairline — each
   past payment is a slip torn from the book. It is the same idea as the
   receipt's tear line, at a whisper, and it is the only place outside the
   success screen where the paper metaphor is allowed to show. */
.activity-list{ display:flex; flex-direction:column; }
.activity-row{
  display:flex; align-items:center; gap:12px; width:100%;
  padding:11px 0; border-bottom:1px dashed var(--hairline-2);
  cursor:pointer; transition:opacity .12s ease;
}
.activity-row:last-child{ border-bottom:none; }
.activity-row:active{ opacity:.55; }
.merchant-ic{
  width:42px;height:42px;border-radius:var(--radius-icon); flex-shrink:0;
  display:flex;align-items:center;justify-content:center;
  font-weight:700; font-size:15px;
  box-shadow:var(--raise);
}
.activity-mid{ flex:1; min-width:0; display:flex; flex-direction:column; text-align:left; }
.m-name{ font-size:13.5px; font-weight:600; color:var(--ink); }
.m-meta{ font-size:11.5px; color:var(--text-faint); margin-top:2px; }
.activity-amt{ display:flex; flex-direction:column; align-items:flex-end; }
.amt{ font-size:13.5px; font-weight:700; }
.amt.out{ color:var(--ink); }
.amt.in{ color:var(--green); }
.amt.failed{ color:var(--alert); }
.st{ font-size:10px; color:var(--text-faint); margin-top:2px; font-weight:500; }
.st.failed{ color:var(--alert); font-weight:600; }

.trust{
  display:flex; align-items:center; justify-content:center; gap:7px;
  margin:22px 20px 0; padding-top:18px; border-top:1px solid var(--hairline);
  font-size:10.5px; font-weight:500; color:var(--text-faint); text-align:center;
}
.trust svg{ stroke:var(--text-faint); }

/* tab bar — flat, four equal tabs */
.tabbar{
  position:absolute; left:0; right:0; bottom:0;
  background:rgba(15,18,32,0.88);
  backdrop-filter:blur(20px) saturate(160%);
  border-top:1px solid var(--hairline);

  display:flex; align-items:center; padding:10px 20px 26px; z-index:9;
}
/* paint() in script.js toggles this on every screen that isn't one of the
   three real tabs (home/history/account) - display:none, not visibility or
   opacity, so a hidden tab bar can never eat a tap meant for whatever
   full-bleed detail screen (scan, a payment, a receipt, settings, ...) is
   actually showing underneath it. */
.tabbar.hidden{ display:none; }
.tab-item{
  flex:1 1 0; display:flex; flex-direction:column; align-items:center; gap:5px;
  cursor:pointer; padding:6px 0; border-radius:var(--radius-sm); transition:background .15s ease;
}
.tab-item .lb{ font-size:10.5px; font-weight:600; color:var(--text-faint); transition:color .16s ease; }
.tab-item.active .lb{ color:var(--blue-bright); }
.tab-item svg{ stroke:var(--text-faint); transition:stroke .16s ease; }
.tab-item.active svg{ stroke:var(--blue-bright); }
/* the most-tapped control in the app had no visible press at all - a dark
   ink wash on a dark ground. Light-on-dark, and a scale so the tap reads
   even on the tab you are already on. */
.tab-item:active{ background:rgba(233,237,250,0.06); transform:scale(0.94); }
.tab-item{ transition:transform .1s ease, background .12s ease; }

/* ============================================================
   2. scanner (stays dark — this is a camera surface, not a page)
   ============================================================ */
.scan-topbar{ display:flex; align-items:center; justify-content:space-between; padding:14px 20px 0; }
.scan-heading{ font-size:14.5px; font-weight:700; color:#FFFFFF; }
.round-btn{
  width:38px;height:38px;border-radius:50%;
  background:rgba(255,255,255,0.14); border:1px solid rgba(255,255,255,0.18);
  display:flex;align-items:center;justify-content:center; cursor:pointer;
  transition:background .16s ease, transform .12s ease;
}
.round-btn:hover{ background:rgba(255,255,255,0.24); }
.round-btn:active{ transform:scale(0.92); }
.round-btn svg{ stroke:#FFFFFF; }
.round-btn.dark{ background:var(--surface); border:1px solid var(--hairline); box-shadow:var(--raise); }
.round-btn.dark svg{ stroke:var(--ink); }
.round-btn.dark:hover{ background:var(--surface-alt); }
.round-btn.on{ background:var(--yellow); border-color:var(--yellow); }

/* The brackets were 38px arms with a 26px corner radius, so twenty-six of
   every thirty-eight pixels were curve and only twelve were straight. Four
   marks that are almost entirely curve, sitting exactly on the panel's own
   26px border, stop reading as brackets and start reading as one rounded
   rectangle with four notches chewed out of it. Longer arms and a tighter
   turn give each mark a straight run to be recognised by.
   The panel's own border is gone for the same reason: the brackets are the
   frame, and a second outline behind them was the thing they were merging
   into. */
.viewfinder{
  position:relative; width:258px; height:258px;
  margin:34px auto 0; border-radius:var(--radius-lg);
  background:rgba(255,255,255,0.045);
  display:flex; align-items:center; justify-content:center; cursor:pointer;
}
.vf-corner{ position:absolute; width:52px; height:52px; border:3px solid var(--yellow); }
.vf-corner.tl{ top:0; left:0;  border-right:0; border-bottom:0; border-radius:var(--radius-lg) 0 0 0; }
.vf-corner.tr{ top:0; right:0; border-left:0;  border-bottom:0; border-radius:0 var(--radius-lg) 0 0; }
.vf-corner.bl{ bottom:0; left:0;  border-right:0; border-top:0; border-radius:0 0 0 var(--radius-lg); }
.vf-corner.br{ bottom:0; right:0; border-left:0;  border-top:0; border-radius:0 0 var(--radius-lg) 0; }
/* The sweep is depicting light, so it is allowed to glow -- but in its own
   colour. It used to carry a near-black blur left over from the orange
   palette, which read as a smudge rather than a beam. Solid through the
   middle with short fades at the ends, so it stays a line and not a haze. */
.vf-scanline{
  position:absolute; left:38px; right:38px; height:1.5px; border-radius:2px;
  /* it was the brightest thing on the screen and the frame is what you are
     meant to be looking at. Held back at the ends and dimmed in the middle,
     it still reads as a beam without out-shouting the brackets. */
  background:linear-gradient(90deg,
    rgba(255,218,0,0) 0%, rgba(255,218,0,0.62) 26%, rgba(255,218,0,0.62) 74%, rgba(255,218,0,0) 100%);
  box-shadow:0 0 5px rgba(255,218,0,0.16);
  animation:vfscan 2.6s cubic-bezier(.5,0,.5,1) infinite;
}
/* constant opacity: a dimmed yellow on a dark ground goes brown, which is
   what made the sweep look dirty. The travel carries the motion instead. */
/* travel stops short of the corner brackets -- a beam that touches them
   reads as a bar sitting on the frame, not as light moving inside it */
@keyframes vfscan{ 0%,100%{ transform:translateY(-84px); } 50%{ transform:translateY(84px); } }
/* One place decides how present this is. The stroke used to carry an alpha
   AND the wrapper an opacity, which multiplied out to 0.17 and left the
   glyph all but invisible - so the stroke is now plain white and this is the
   only dimmer. It is a placeholder for a QR that is not there yet, so it
   should read as a hint, not as content. */
.vf-ghost{ opacity:.42; }

/* Scan lock-on. Every payment path in this app starts by tapping the
   viewfinder, and until now that tap cut straight to the amount screen -
   no acknowledgement a code was even found. Every real scanner shows a
   catch; this is that beat. Kept to ~380ms: felt, not waited on. */
.viewfinder.locked{ animation:vfgrab .38s cubic-bezier(.3,1.4,.4,1); }
@keyframes vfgrab{ 0%{ transform:scale(1); } 40%{ transform:scale(0.95); } 100%{ transform:scale(1); } }
.viewfinder.locked .vf-corner{
  border-color:#FFFFFF; border-width:4px;
  transition:border-color .16s ease, border-width .16s ease;
}
.viewfinder.locked .vf-scanline{ opacity:0; transition:opacity .12s ease; }
.viewfinder.locked .vf-ghost{ opacity:.9; transition:opacity .16s ease; }
/* the ring is a one-shot event pulse, not the ambient glow the app
   otherwise refuses everywhere else - same idea as the notif dot's pulse,
   just fired once, for a real thing that just happened */
.viewfinder.locked::after{
  content:''; position:absolute; inset:0; border-radius:var(--radius-lg);
  animation:vfring .38s ease-out;
}
@keyframes vfring{ 0%{ box-shadow:0 0 0 0 rgba(255,218,0,0.45); } 100%{ box-shadow:0 0 0 14px rgba(255,218,0,0); } }
.scan-hint{ text-align:center; margin:20px 0 0; font-size:12.5px; font-weight:500; color:rgba(255,255,255,0.5); }
.scan-foot{ margin-top:auto; display:flex; gap:11px; padding:0 20px 40px; }
.scan-foot-btn{
  flex:1; display:flex; align-items:center; justify-content:center; gap:8px;
  padding:14px 0; border-radius:var(--radius-sm);
  background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.16);
  font-size:12.5px; font-weight:600; color:#FFFFFF; cursor:pointer;
  transition:background .16s ease, transform .12s ease;
}
.scan-foot-btn:hover{ background:rgba(255,255,255,0.18); }
.scan-foot-btn:active{ transform:scale(0.97); }

/* ============================================================
   3. amount
   ============================================================ */
.flow-topbar{ display:flex; align-items:center; justify-content:space-between; padding:14px 20px 0; }
.verified-chip{
  display:flex; align-items:center; gap:5px;
  font-size:11px; font-weight:700; color:var(--green);
  background:var(--green-soft); padding:6px 11px; border-radius:var(--radius-full);
}
.verified-chip svg{ stroke:var(--green); }
.payee{ display:flex; flex-direction:column; align-items:center; gap:4px; padding:16px 20px 0; }
.payee-avatar{
  width:54px;height:54px;border-radius:50%;
  background:var(--blue); color:#FFFFFF;
  display:flex;align-items:center;justify-content:center;
  font-size:17px; font-weight:800; margin-bottom:6px;

}
.payee-name{ font-size:18px; font-weight:700; color:var(--ink); letter-spacing:-0.02em; }
/* THE FLAG. One component, two shapes, used anywhere the app has to say
   "notice this" without saying "something is wrong" - red is for failure and
   this is not failure. It was a one-off on the pay screen; it is a component
   now because the moment you have a second one (the dispute sheet, which
   reminds you the app flagged this payee before you paid) two hand-made
   amber boxes start drifting apart.

     .payee-flag  the pill, for a short phrase beside a name
     .known-flag  the block, for a sentence that has to wrap

   Both take yellow because yellow is attention, and neither is tappable,
   which is exactly what yellow is allowed to mean. */
.payee-flag, .known-flag{
  align-items:center; gap:6px;
  font-weight:700; line-height:1.35;
  color:var(--yellow); background:var(--yellow-soft);
  box-shadow:inset 0 0 0 1px rgba(255,218,0,0.26);
}
.payee-flag{
  display:inline-flex;
  margin-top:10px; padding:6px 11px; border-radius:var(--radius-full);
  font-size:11.5px;
}
.known-flag{
  display:flex; align-items:flex-start;
  margin:2px 0 14px; padding:9px 12px; border-radius:var(--radius-sm);
  font-size:11.5px; font-weight:600; text-align:left;
}
.known-flag svg{ flex-shrink:0; margin-top:1px; }
.payee-flag[hidden], .known-flag[hidden]{ display:none; }
.verified-chip[hidden]{ display:none; }
.payee-id{ font-size:12px; font-weight:500; color:var(--text-faint); }

.amount-wrap{ flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:14px; }
.amount-field{
  display:flex; align-items:center; gap:2px;
  font-size:46px; font-weight:800; color:var(--ink); letter-spacing:-0.035em;
  font-variant-numeric:tabular-nums;
  transition:transform .12s cubic-bezier(.3,1.4,.5,1);
}
.amount-field.bump{ transform:scale(1.045); }
.amount-field .rupee{ font-size:32px; font-weight:700; opacity:.4; margin-right:2px; }
.caret{ width:2px; height:38px; border-radius:2px; background:var(--blue); animation:blink 1.1s steps(1) infinite; }
@keyframes blink{ 0%,49%{opacity:1;} 50%,100%{opacity:0;} }
.quick-amounts{ display:flex; gap:8px; }
.quick-amounts[hidden]{ display:none; }
/* the caption exists to explain where non-round numbers came from, so it is
   quiet and it leaves as soon as they are round again */
.qa-cap{ font-size:11px; font-weight:600; color:var(--text-faint); margin-bottom:8px; }
.qa-cap[hidden]{ display:none; }
/* a chip drawn from your own history reads as a fact, not a suggestion, so it
   takes the ink colour and a real edge - the round-number guesses stay quiet */
.qa.hist{ color:var(--ink); border-color:var(--hairline-2); }
.qa{
  font-size:12.5px; font-weight:700; color:var(--text-soft);
  background:var(--surface); border:1px solid var(--hairline);
  padding:8px 15px; border-radius:var(--radius-sm); cursor:pointer; transition:all .14s ease;
}
/* --blue is a fill colour (it is specced against white text). On a dark
   ground or on --blue-tint it drops to about 1.9:1 - --blue-bright is the
   token that exists for foreground use, and this rule plus five others
   below had it backwards. The border was still the light-mode blue too. */
.qa:hover{ color:var(--blue-bright); border-color:rgba(51,85,218,0.4); background:var(--blue-tint); }
.qa:active{ transform:scale(0.95); }
.note-chip{
  display:flex; align-items:center; gap:6px;
  font-size:12px; font-weight:600; color:var(--text-soft);
  background:transparent; border:1px dashed var(--hairline-2);
  padding:8px 14px; border-radius:var(--radius-sm); cursor:pointer; transition:all .16s ease;
}
.note-chip:hover{ color:var(--ink); border-color:var(--text-faint); }
.note-chip:active{ transform:scale(0.96); }

.amount-foot{ padding:0 20px 20px; display:flex; flex-direction:column; gap:10px; }
.bank-select{
  display:flex; align-items:center; gap:11px; padding:11px 14px; border-radius:var(--radius-sm);
  background:var(--surface); border:1px solid var(--hairline);
  box-shadow:var(--raise);
}
.bank-select svg{ stroke:var(--text-faint); }
.bs-logo{
  width:42px;height:42px;border-radius:var(--radius-icon); flex-shrink:0;
  background:var(--blue-tint); color:var(--blue-bright);
  display:flex;align-items:center;justify-content:center; font-size:13px; font-weight:800;
}
.bs-txt{ flex:1; display:flex; flex-direction:column; gap:1px; }
.bs-1{ font-size:12.5px; font-weight:600; color:var(--ink); }
/* was green when it carried a balance; it carries an instruction now, and
   green in this app means money arrived */
.bs-2{ font-size:10px; font-weight:700; color:var(--text-faint); letter-spacing:.04em; text-transform:uppercase; }

.keypad{ display:grid; grid-template-columns:repeat(3,1fr); gap:6px; }
.key{
  height:50px; border-radius:var(--radius-sm);
  display:flex; align-items:center; justify-content:center;
  font-size:21px; font-weight:600; color:var(--ink); cursor:pointer; background:transparent;
  transition:background .12s ease, transform .09s ease;
}
.key svg{ stroke:var(--ink); }
.key:hover{ background:rgba(233,237,250,0.05); }
.key:active{ background:var(--surface); box-shadow:var(--press); transform:scale(0.97); }
.key-blank{ cursor:default; }
.key-blank:hover{ background:transparent; }

/* ============================================================
   4. success — the parchi
   ============================================================
   The whole receipt is one slip of paper: straight top, torn bottom,
   a perforation across the middle where a shopkeeper would tear it, and
   a stamp pressed into it. The tear is a CSS mask, so the drop shadow is
   applied to a wrapper via filter — a box-shadow would be masked away
   with the paper and the silhouette would look pasted on. */
.success-body{ flex:1; overflow-y:auto; scrollbar-width:none; display:flex; flex-direction:column; align-items:center; padding:24px 20px 8px; }
/* auto margins rather than justify-content:center — if the slip ever grows
   taller than the viewport, auto margins collapse to 0 and it stays
   scrollable from the top, where centring would clip the head off-screen. */
/* printer slot and slip travel together, so the auto-centring lives on
   the stack rather than on the paper */
.print-stack{ width:100%; margin-block:auto; }
/* While it is actually feeding, the slip pins to the top instead of
   floating mid-screen -- a printer has a fixed slot, and a receipt
   materialising in the middle of empty space reads as a fade, not a
   machine. It drops back to centred once the feed finishes. */
.printing-on .print-stack{ margin-block:0 auto; }
.success-body::-webkit-scrollbar{display:none;}

.parchi-wrap{
  width:100%; position:relative;
  filter:drop-shadow(0 3px 6px rgba(0,0,0,0.5)) drop-shadow(0 22px 34px rgba(0,0,0,0.55));
}
.parchi{
  position:relative;
  background:var(--paper);
  padding:20px 18px 26px;
  /* The torn bottom edge. Cut with clip-path rather than a gradient mask:
     conic-gradient masks silently render as a straight edge in some
     engines, and this is the one detail on the screen that has to survive.
     The tooth depths are deliberately irregular — a regular zigzag reads as
     pinking shears, not as paper someone tore. Seeded, so it is identical on
     every render. The drop-shadow lives on .parchi-wrap because a
     box-shadow here would be clipped away with the paper and the silhouette
     would look pasted on. */
  clip-path:polygon(
    0% 0%,
    100% 0%,
    100% calc(100% - 7.0px),
    97.059% calc(100% - 0.3px),
    94.118% calc(100% - 8.4px),
    91.176% calc(100% - 0.2px),
    88.235% calc(100% - 7.9px),
    85.294% calc(100% - 0.8px),
    82.353% calc(100% - 5.8px),
    79.412% calc(100% - 1.1px),
    76.471% calc(100% - 5.7px),
    73.529% calc(100% - 1.0px),
    70.588% calc(100% - 5.8px),
    67.647% calc(100% - 0.2px),
    64.706% calc(100% - 7.4px),
    61.765% calc(100% - 1.8px),
    58.824% calc(100% - 6.1px),
    55.882% calc(100% - 0.5px),
    52.941% calc(100% - 8.3px),
    50.000% calc(100% - 2.1px),
    47.059% calc(100% - 8.1px),
    44.118% calc(100% - 0.9px),
    41.176% calc(100% - 9.9px),
    38.235% calc(100% - 0.1px),
    35.294% calc(100% - 9.4px),
    32.353% calc(100% - 0.6px),
    29.412% calc(100% - 6.1px),
    26.471% calc(100% - 0.3px),
    23.529% calc(100% - 6.9px),
    20.588% calc(100% - 1.8px),
    17.647% calc(100% - 6.3px),
    14.706% calc(100% - 1.3px),
    11.765% calc(100% - 8.4px),
    8.824% calc(100% - 0.8px),
    5.882% calc(100% - 8.0px),
    2.941% calc(100% - 0.1px),
    0% calc(100% - 5.8px)
  );
}
.view-success.active .parchi-wrap{ animation:slipIn .45s cubic-bezier(.2,.9,.3,1) both; }
@keyframes slipIn{ from{ transform:translateY(14px); opacity:0; } to{ transform:none; opacity:1; } }

.p-head{ display:flex; align-items:center; justify-content:space-between; padding-bottom:14px; border-bottom:1px solid var(--paper-rule); }
.p-brand{ display:flex; align-items:center; gap:6px; font-size:15px; font-weight:800; letter-spacing:-.02em; color:var(--paper-blue); }
.p-brand img{ display:block; }
.p-no{ font-size:10.5px; font-weight:600; letter-spacing:0.08em; color:var(--paper-faint); font-variant-numeric:tabular-nums; }

.p-amount{ margin-top:18px; font-size:36px; font-weight:800; color:var(--paper-ink); letter-spacing:-0.035em; font-variant-numeric:tabular-nums; line-height:1; }
.p-line{ font-size:13.5px; font-weight:500; color:var(--paper-soft); margin-top:7px; }
.p-line b{ font-weight:700; color:var(--paper-ink); }

/* the tear line. Notches are drawn as radial gradients inside the element
   so they survive the paper mask — half-circles hanging outside the box
   would simply be clipped away by it. */
.perf{
  position:relative; height:20px; margin:18px -18px 2px;
  background:
    radial-gradient(circle at 0 50%,    var(--bg) 7px, rgba(0,0,0,0) 7.5px),
    radial-gradient(circle at 100% 50%, var(--bg) 7px, rgba(0,0,0,0) 7.5px);
}
.perf::before{
  content:""; position:absolute; left:24px; right:24px; top:50%;
  border-top:1.5px dashed var(--paper-rule-2);
}

.receipt{ padding:2px 0 0; }
/* The rule goes BETWEEN rows, so it is drawn only on a row that still has a
   visible row after it. It used to be drawn on every row and removed from
   :last-child - but Note and Reported are always present and merely [hidden],
   so :last-child was a hidden row and the last VISIBLE row kept its rule.
   Every receipt in the app carried a hairline under its final line with
   nothing beneath it, and on the failed slip that stray rule landed 10px
   above the reversal block's dashed one - a doubled divider that made the
   block read as something bolted onto the receipt rather than part of it. */
.rr{ display:flex; align-items:center; justify-content:space-between; gap:12px; padding:11px 0; font-size:12.5px; }
.receipt .rr:not([hidden]):has(~ .rr:not([hidden])){ border-bottom:1px solid var(--paper-rule); }
.rr[hidden]{ display:none; }   /* [hidden] alone loses to display:flex */

/* the reversal claim: it belongs on the paper because it is part of the
   record, not a piece of app chrome sitting next to it. Ruled off rather
   than boxed - a box would read as an alert, and this is not an alert, it
   is a fact with a date on it. */
.reversal{
  margin-top:13px; padding-top:12px;
  border-top:1px dashed var(--paper-rule-2);
  text-align:left;
}
.reversal[hidden]{ display:none; }
/* one statement, not two. The amount is already at the top of the slip and
   again in "Failed - debited" directly above; saying "Rs420 left your
   account" as a heading made it the third time in four lines, which reads as
   the receipt labouring the point rather than answering the question the
   person actually has, which is when they get it back. */
.rv-head{ font-size:12.5px; line-height:1.5; color:var(--paper-soft); }
.rv-head b{ color:var(--paper-ink); font-weight:800; }
.rv-rule{ font-size:11px; line-height:1.5; color:var(--paper-faint); margin-top:7px; }
.rr span{ color:var(--paper-faint); font-weight:500; }
.rr b{ color:var(--paper-ink); font-weight:600; }
.rr b.ok{ color:var(--paper-green); }

/* the "Paid!" doodle — the payment moment.
   Hand-lettered rather than stamped: the receipt is the serious object, and
   this is the reaction to it. Blue, because it has to be readable; the
   sparkles carry the yellow. It lands a beat after the slip so the two read
   as two events, and the sparkles pop after that, so it feels drawn on
   rather than faded in. */
.paid-doodle{
  position:absolute; right:6px; top:88px;
  width:132px; height:76px;
  color:var(--paper-blue);
  transform:rotate(-9deg);
  pointer-events:none;
}
.pd-word{
  display:block;
  font-family:Kalam,'Segoe Script',cursive;
  font-size:46px; font-weight:700; line-height:1;
  color:var(--paper-blue);
  text-align:center;
}
.pd-rule{ display:block; width:112px; height:14px; margin:-2px auto 0; color:var(--paper-blue); }
.pd-sp{ position:absolute; display:block; }
.pd-sp1{ top:-14px; right:2px; }
.pd-sp2{ top:6px; left:-10px; }
.pd-sp3{ bottom:4px; right:12px; }

.view-success.active .pd-word{ animation:scrawl .34s cubic-bezier(.2,1.3,.35,1) .30s both; }
.view-success.active .pd-rule path{
  stroke-dasharray:130; stroke-dashoffset:130;
  animation:underline .34s ease-out .56s both;
}
.view-success.active .pd-sp{ animation:pip .34s cubic-bezier(.2,1.6,.4,1) both; }
.view-success.active .pd-sp1{ animation-delay:.62s; }
.view-success.active .pd-sp2{ animation-delay:.72s; }
.view-success.active .pd-sp3{ animation-delay:.80s; }

@keyframes scrawl{
  0%{   transform:rotate(-16deg) scale(.5); opacity:0; }
  100%{ transform:rotate(0deg)   scale(1);  opacity:1; }
}
@keyframes underline{ to{ stroke-dashoffset:0; } }
@keyframes pip{
  0%{   transform:scale(0) rotate(-40deg); opacity:0; }
  100%{ transform:scale(1) rotate(0deg);   opacity:1; }
}
@media (prefers-reduced-motion:reduce){
  .view-success.active .pd-word,
  .view-success.active .pd-sp{ animation:none; }
  .view-success.active .pd-rule path{ animation:none; stroke-dashoffset:0; }
}

.success-foot{ padding:14px 20px 30px; display:flex; flex-direction:column; gap:9px; flex-shrink:0; }
.dispute-btn{ display:block; width:100%; text-align:center; padding:4px 0 2px; }
.dispute-btn[hidden]{ display:none; }   /* display:block outranks [hidden] */
.dispute-btn:disabled{ color:var(--text-faint); cursor:default; }
.dispute-btn:disabled:hover{ color:var(--text-faint); }

/* the reason list on the report sheet - one tap picks it, same selected
   treatment as the bank picker so it reads as the same kind of choice */
.dispute-reasons{ display:flex; flex-direction:column; gap:8px; margin-bottom:12px; }
.dispute-opt{
  width:100%; padding:13px 14px; border-radius:var(--radius-sm); cursor:pointer;
  background:var(--surface); box-shadow:var(--raise); text-align:left;
  font-size:13.5px; font-weight:600; color:var(--ink);
}
.dispute-opt.active{ box-shadow:var(--press), inset 0 0 0 1.5px var(--blue-bright); background:var(--blue-tint); color:var(--blue-bright); }

/* a reported transaction carries that fact wherever it shows up, not just
   on its own receipt */
.st-flag{
  display:block; margin-top:2px; font-size:9.5px; font-weight:700;
  letter-spacing:.03em; text-transform:uppercase; color:var(--text-faint);
}

/* ============================================================
   5. history
   ============================================================ */
.page-topbar{ display:flex; align-items:center; justify-content:space-between; gap:12px; padding:14px 20px 14px; flex-shrink:0; }
.page-title{ font-size:17px; font-weight:700; color:var(--ink); letter-spacing:-0.02em; }
.text-btn{ font-size:12.5px; font-weight:600; color:var(--gray); cursor:pointer; padding:8px; margin:-8px; }
.text-btn:hover{ color:var(--ink); }

.search-wrap{
  position:relative; display:flex; align-items:center; gap:9px;
  margin:0 20px 12px; padding:12px 13px; border-radius:var(--radius-sm);
  background:var(--surface); border:1px solid var(--hairline);
  flex-shrink:0; transition:border-color .16s ease;
}
.search-wrap:focus-within{ border-color:var(--blue); }
.search-ic{ flex-shrink:0; stroke:var(--text-faint); }
.search-clear svg{ stroke:var(--text-faint); }
.search-wrap input{ flex:1; border:none; outline:none; background:transparent; font-size:13px; font-weight:500; color:var(--ink); }
.search-wrap input::placeholder{ color:var(--text-faint); }
.search-clear{ display:none; cursor:pointer; padding:2px; }
.search-clear.show{ display:flex; }

.filter-row{ display:flex; gap:7px; overflow-x:auto; padding:0 20px 14px; scrollbar-width:none; flex-shrink:0; }
.filter-row::-webkit-scrollbar{display:none;}
.filter-chip{
  flex:0 0 auto; font-size:12px; font-weight:600; color:var(--text-soft);
  background:var(--surface); border:1px solid var(--hairline);
  padding:8px 14px; border-radius:var(--radius-sm); cursor:pointer; transition:all .16s ease;
  box-shadow:var(--raise);
}
.filter-chip:hover{ color:var(--ink); }
.filter-chip.active{ background:var(--blue); color:#FFFFFF; border-color:var(--blue); }

/* 104px bottom padding, not 30 - matches .scroll's own reserve for the tab
   bar (see script.js paint()): History now keeps that chrome visible like
   Home and Account already did, and without this the last row or two sat
   underneath it, unreadable and untappable. */
.history-list{ flex:1; overflow-y:auto; scrollbar-width:none; padding:0 20px 104px; }
.history-list::-webkit-scrollbar{display:none;}
.h-group{ font-size:11px; font-weight:700; color:var(--text-faint); letter-spacing:.05em; text-transform:uppercase; padding:14px 0 6px; }
.history-empty{ display:none; flex-direction:column; align-items:center; gap:10px; padding:60px 20px; color:var(--text-faint); font-size:13px; font-weight:500; }
.history-empty svg{ stroke:var(--text-faint); }
.history-empty.show{ display:flex; }
.history-empty span{ color:var(--ink); font-weight:700; font-size:14px; }
.history-empty small{ font-size:12.5px; line-height:1.5; max-width:26ch; text-align:center; margin-top:-4px; }

/* ============================================================
   6. account
   ============================================================ */
.acct-head{ display:flex; flex-direction:column; align-items:center; padding:6px 20px 22px; }
.acct-avatar{
  width:72px;height:72px;border-radius:50%; overflow:hidden; line-height:0;
  border:2px solid var(--hairline-2); margin-bottom:12px;
  box-shadow:0 4px 14px -6px rgba(20,20,15,0.2);
}
.acct-avatar img{ display:block; width:100%; height:100%; }
.acct-name{ font-size:18px; font-weight:700; color:var(--ink); letter-spacing:-0.02em; }
.acct-phone{ font-size:12.5px; font-weight:500; color:var(--text-faint); margin-top:2px; }
.acct-qr-btn{
  display:flex; align-items:center; gap:7px; margin-top:13px;
  font-size:12.5px; font-weight:700; color:var(--blue-bright);
  background:var(--blue-tint); border:1px solid rgba(51,85,218,0.3);
  padding:9px 16px; border-radius:var(--radius-sm); cursor:pointer; transition:all .16s ease;
}
.acct-qr-btn:hover{ background:#FFE1CB; }
.acct-qr-btn:active{ transform:scale(0.96); }

/* Refer & earn used to be a solid blue promo banner - the comment in the
   markup said "GPay-style", which was the tell. It was the loudest object on
   a settings page, a third card treatment on a screen that already had two,
   and a growth banner sitting inside an app whose argument is restraint. It
   is a row now, like everything else it belongs beside. */

.acct-group{ margin:0 20px 16px; }
.ag-label{ font-size:10.5px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; color:var(--text-faint); padding:0 4px 8px; }
.acct-row{
  display:flex; align-items:center; gap:12px; width:100%;
  padding:13px 14px; background:var(--surface);
  border:1px solid var(--hairline); border-bottom:none;
  cursor:pointer; transition:background .14s ease;
  box-shadow:var(--raise);
}
.acct-row:first-of-type{ border-radius:var(--radius-md) var(--radius-md) 0 0; }
.acct-row:last-of-type{ border-radius:0 0 var(--radius-md) var(--radius-md); border-bottom:1px solid var(--hairline); }
.acct-group .acct-row:only-of-type{ border-radius:var(--radius-md); }
.acct-row:hover{ background:var(--surface-alt); }
.as-toggle{ cursor:default; }
.as-toggle:hover{ background:var(--surface); }
.ar-ic{ width:42px;height:42px;border-radius:var(--radius-icon); flex-shrink:0; background:var(--blue-tint); display:flex;align-items:center;justify-content:center; }
.ar-ic svg{ stroke:var(--blue-bright); }
.ar-txt{ flex:1; display:flex; flex-direction:column; gap:2px; text-align:left; }
.ar-txt b{ font-size:13.5px; font-weight:600; color:var(--ink); }
.ar-txt small{ font-size:11px; font-weight:500; color:var(--text-faint); }
.ar-go{ display:flex; }
.ar-go svg{ stroke:var(--text-faint); }
.switch{ width:48px;height:29px;border-radius:var(--radius-full); background:#DEDCD6; position:relative; cursor:pointer; flex-shrink:0; transition:background .22s ease; }
.switch i{ position:absolute; top:3px; left:3px; width:23px;height:23px;border-radius:50%; background:#FFFFFF; box-shadow:0 1px 3px rgba(0,0,0,0.2); transition:transform .22s cubic-bezier(.3,.9,.3,1); }
.switch.on{ background:var(--green); }
.switch.on i{ transform:translateX(19px); }
.acct-foot{ text-align:center; font-size:11px; font-weight:500; color:var(--text-faint); padding:6px 0 20px; }

/* ============================================================
   7. notifications
   ============================================================ */
.notif-group{ font-size:11px; font-weight:700; color:var(--text-faint); letter-spacing:.05em; text-transform:uppercase; padding:14px 20px 8px; }
.notif{ display:flex; gap:12px; padding:13px 20px; position:relative; width:100%; text-align:left; }
/* only the notification that leads somewhere gets a chevron and a press
   state - the rest are statements, and a tap target on a statement is a
   dead control by another name */
button.notif{ cursor:pointer; }
button.notif:active{ background:var(--surface-alt); }
.n-go{ align-self:center; flex-shrink:0; color:var(--text-faint); display:flex; }
.notif.unread{ background:var(--blue-tint); }
.notif.unread::before{ content:""; position:absolute; left:9px; top:50%; transform:translateY(-50%); width:5px;height:5px;border-radius:50%; background:var(--yellow); }
.n-ic{ width:42px;height:42px;border-radius:var(--radius-icon); flex-shrink:0; display:flex;align-items:center;justify-content:center; }
.n-alert{ background:var(--alert-soft); }
.n-alert svg{ stroke:var(--alert); }
.n-in{ background:var(--green-soft); }
.n-in svg{ stroke:var(--green); }
.n-bill,.n-sec{ background:var(--surface-alt); }
.n-bill svg,.n-sec svg{ stroke:var(--text-soft); }
.n-txt{ flex:1; display:flex; flex-direction:column; gap:2px; }
.n-txt b{ font-size:13px; font-weight:600; color:var(--ink); letter-spacing:-0.005em; }
.n-txt small{ font-size:11.5px; font-weight:500; color:var(--text-soft); line-height:1.45; }
.n-time{ font-size:10.5px; font-weight:500; color:var(--text-faint); margin-top:3px; }

/* ============================================================
   sheets
   ============================================================ */
/* scrim was warm brown-black (0.4) left over from the light build: over a
   #0F1220 ground it barely separated the sheet from the page. Cool, and
   deep enough that the sheet is clearly the only live thing. */
.sheet-overlay{ position:absolute; inset:0; background:rgba(4,6,14,0.62); opacity:0; pointer-events:none; transition:opacity .25s ease; z-index:20; }
.sheet-overlay.open{ opacity:1; pointer-events:auto; }
/* sheets float, so they are allowed a real shadow */
.sheet{
  position:absolute; left:0; right:0; bottom:0;
  background:var(--bg); border-radius:var(--radius-xl) var(--radius-xl) 0 0; padding:14px 22px 30px;
  transform:translateY(100%); transition:transform .38s cubic-bezier(.32,.72,0,1);
  box-shadow:0 -20px 40px -20px rgba(0,0,0,0.24);
}
.sheet-overlay.open .sheet{ transform:translateY(0); }
.sheet-handle{ width:36px; height:4px; border-radius:var(--radius-xs); margin:0 auto 16px; background:var(--hairline-2); }
.sheet-title{ font-size:16px; font-weight:700; color:var(--ink); text-align:center; margin-bottom:4px; letter-spacing:-0.015em; }
.sheet-sub{ font-size:12px; color:var(--text-faint); text-align:center; margin-bottom:20px; }

/* send money */
.contact-list{ display:flex; flex-direction:column; gap:8px; margin-bottom:16px; }
.contact{
  display:flex; align-items:center; gap:12px; width:100%;
  padding:11px 13px; border-radius:var(--radius-sm);
  background:var(--surface); border:1px solid var(--hairline);
  cursor:pointer; transition:background .14s ease, transform .12s ease;
}
.contact:hover{ background:var(--surface-alt); }
.contact:active{ transform:scale(0.98); }
.c-ic{
  width:38px;height:38px;border-radius:50%; flex-shrink:0;
  background:var(--blue); color:#FFFFFF;
  display:flex;align-items:center;justify-content:center; font-size:13px; font-weight:800;
}
.c-txt{ display:flex; flex-direction:column; gap:1px; text-align:left; }
.c-txt b{ font-size:13.5px; font-weight:600; color:var(--ink); }
.c-txt small{ font-size:11px; font-weight:500; color:var(--text-faint); }
.upi-input{ display:flex; gap:9px; }
.upi-input input{
  flex:1; padding:14px 14px; border-radius:var(--radius-sm);
  background:var(--surface); border:1px solid var(--hairline);
  font-size:13px; font-weight:500; color:var(--ink); outline:none;
  transition:border-color .16s ease;
}
.upi-input input::placeholder{ color:var(--text-faint); }
.upi-input input:focus{ border-color:var(--blue); }
.upi-input .btn{ padding:0 20px; }

.pin-head{ text-align:center; }
.pin-bank{ display:inline-block; margin-bottom:10px; font-size:10.5px; font-weight:700; letter-spacing:.05em; text-transform:uppercase; color:var(--text-faint); }
.pin-dots{ display:flex; justify-content:center; gap:16px; margin-bottom:6px; }
.pin-dots i{ width:13px;height:13px;border-radius:50%; border:1.6px solid var(--text-faint); transition:all .18s cubic-bezier(.3,1.4,.5,1); }
.pin-dots i.filled{ background:var(--blue); border-color:var(--blue); transform:scale(1.12); }
.pin-error{ height:18px; text-align:center; font-size:11.5px; font-weight:600; color:var(--alert); }
.sheet-cancel{ display:block; width:100%; margin-top:10px; padding:12px 0; text-align:center; font-size:13px; font-weight:600; color:var(--text-soft); cursor:pointer; border-radius:12px; transition:background .14s ease; }
/* was a dark ink wash - invisible on a dark app. Every press state in here
   has to be light-on-dark, same as .key:hover. */
.sheet-cancel:hover{ background:rgba(233,237,250,0.05); }

/* ============================================================
   authorising
   ============================================================
   The gap this fills: the PIN used to hand straight to the receipt in
   380ms, which is the one moment in the app that lied. A UPI debit is a
   round trip through NPCI to the payer bank and back, and the two or
   three seconds it takes is the only time the user has no control over
   the outcome - the whole reason "failed but debited" exists at all.
   Skipping it also threw away the app's own argument: this screen names
   the stage the money is at, so when a payment does get stuck later, the
   user has already been shown where it got stuck. */
.proc-overlay{
  position:absolute; inset:0; z-index:40;
  background:var(--bg);
  display:flex; align-items:center; justify-content:center;
  opacity:0; pointer-events:none; transition:opacity .2s ease;
}
.proc-overlay.open{ opacity:1; pointer-events:auto; }
.proc-inner{ display:flex; flex-direction:column; align-items:center; text-align:center; padding:0 40px; }

.proc-ring{ position:relative; width:72px; height:72px; margin-bottom:26px; }
.proc-spin{ width:100%; height:100%; animation:procturn 1.1s linear infinite; }
.proc-track{ stroke:var(--hairline-2); stroke-width:3; }
.proc-arc{
  stroke:var(--blue-bright); stroke-width:3; stroke-linecap:round;
  stroke-dasharray:52 149; transition:opacity .18s ease;
}
@keyframes procturn{ to{ transform:rotate(360deg); } }

.proc-tick{
  position:absolute; inset:0; margin:auto; width:34px; height:34px;
  color:var(--green); opacity:0; transform:scale(.6);
  transition:opacity .18s ease, transform .34s cubic-bezier(.3,1.5,.4,1);
}
/* the ring does not fade out and a tick fade in over it - it stops turning
   and the tick lands inside it, so it reads as the same object resolving */
.proc-overlay.done .proc-spin{ animation:none; }
.proc-overlay.done .proc-arc{ opacity:0; }
.proc-overlay.done .proc-track{ stroke:var(--green); transition:stroke .2s ease; }
.proc-overlay.done .proc-tick{ opacity:1; transform:scale(1); }

.proc-amt{
  font-family:var(--font-display);
  font-size:30px; font-weight:800; color:var(--ink);
  letter-spacing:-0.03em; font-variant-numeric:tabular-nums; margin-bottom:8px;
}
/* the stage line changes twice; crossfading it stops the layout from
   flickering as the string length changes */
.proc-stage{
  min-height:19px; font-size:13px; font-weight:600; color:var(--text-soft);
  transition:opacity .16s ease;
}
.proc-stage.swap{ opacity:0; }
/* the resolved line steps up to full ink - it is a result now, not a status
   update. It stays neutral rather than going green: in this app green means
   money arrived, and this is money leaving. */
.proc-overlay.done .proc-stage{ color:var(--ink); font-weight:700; }
.proc-warn{
  margin-top:22px; font-size:11px; font-weight:700; letter-spacing:.05em;
  text-transform:uppercase; color:var(--text-faint);
  transition:opacity .2s ease;
}
.proc-overlay.done .proc-warn{ opacity:0; }

.balance-amount{ position:relative; height:44px; font-size:36px; font-weight:800; color:var(--ink); text-align:center; letter-spacing:-0.035em; font-variant-numeric:tabular-nums; }
.balance-amount .ba-masked,
.balance-amount .ba-real{ position:absolute; inset:0; transition:opacity .25s ease; }
.balance-amount .ba-masked{ opacity:1; letter-spacing:2px; }
.balance-amount .ba-real{ opacity:0; }
.balance-amount.revealed .ba-masked{ opacity:0; }
.balance-amount.revealed .ba-real{ opacity:1; }
/* the eye sits inside the number's own row, not under it - it is a property
   of the figure, not another thing on the sheet */
.ba-eye{
  position:absolute; right:0; top:50%; transform:translateY(-50%);
  width:44px; height:44px; border-radius:var(--radius-sm);
  display:flex; align-items:center; justify-content:center;
  color:var(--text-faint); cursor:pointer;
  transition:color .16s ease, background .16s ease;
}
.ba-eye:hover{ color:var(--text-soft); background:var(--surface-alt); }
.ba-eye:active{ transform:translateY(-50%) scale(0.92); }
.ba-eye .eye-shut{ display:none; }
.balance-amount.revealed .ba-eye .eye-open{ display:none; }
.balance-amount.revealed .ba-eye .eye-shut{ display:block; }
.balance-meta{ font-size:11.5px; font-weight:500; color:var(--green); text-align:center; margin-top:6px; margin-bottom:22px; }

.qr-panel{ width:196px; margin:0 auto; padding:10px; background:var(--surface); border:1px solid var(--hairline); border-radius:var(--radius-md); line-height:0; }
.qr-panel img{ display:block; width:176px; height:176px; }
.qr-id{ text-align:center; margin:12px 0 20px; font-size:13.5px; font-weight:700; color:var(--ink); }

.generic-icon{ width:52px;height:52px;border-radius:var(--radius-icon); margin:0 auto 14px; background:var(--blue-tint); display:flex;align-items:center;justify-content:center; }
.generic-icon svg{ stroke:var(--blue-bright); fill:none; }
/* sheets opened from something other than a tile have no glyph to borrow -
   an empty tinted square is worse than no square */
.generic-icon.hidden{ display:none; }

/* ---------- focus + motion ---------- */
/* every text field already draws its own focus treatment (a border-color
   change on the wrapper, via :focus-within) and sets outline:none on the
   input itself to suppress exactly this ring - but `.device :focus-visible`
   beats that by specificity (two classes/pseudo-classes vs one class + one
   type), so the browser's own chunky offset ring was drawing on top of the
   field's real border, doubled up. Text inputs opt out here instead; every
   other control (buttons, tabs, icons) still gets the ring, which is what
   it is actually for. */
.device :focus-visible:not(input):not(textarea):not([contenteditable]){ outline:2px solid var(--blue); outline-offset:3px; border-radius:var(--radius-ctl); }
.view-scan :focus-visible{ outline-color:#FFFFFF; outline-offset:2px; }
.tab-item:focus-visible{ outline-offset:-2px; }

@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{
    animation-duration:.001ms !important; animation-iteration-count:1 !important;
    transition-duration:.001ms !important; scroll-behavior:auto !important;
  }
  .vf-scanline, .caret{ display:none; }
  .stagger{ opacity:1; transform:none; }
}

/* ---- states the app can actually reach -------------------------------- */
/* Insufficient balance is a real outcome, so it gets a real treatment:
   caught before the PIN sheet, said in words, and pointing at the fix. */
.amount-error{
  max-height:0; overflow:hidden; opacity:0;
  font-size:12.5px; font-weight:600; color:var(--alert);
  text-align:center;
  transition:max-height .2s ease, opacity .2s ease, margin .2s ease;
}
.amount-error.show{ max-height:44px; opacity:1; margin-top:12px; }
.amount-field.shake{ animation:shake .34s cubic-bezier(.36,.07,.19,.97); }
@keyframes shake{
  10%,90%{ transform:translateX(-2px); }
  20%,80%{ transform:translateX(4px); }
  30%,50%,70%{ transform:translateX(-7px); }
  40%,60%{ transform:translateX(7px); }
}
@media (prefers-reduced-motion:reduce){ .amount-field.shake{ animation:none; } }

/* a note that exists reads as filled-in, not as an invitation */
.note-chip.has-note{
  border-style:solid; border-color:var(--blue);
  color:var(--blue-bright); background:var(--blue-tint);
}

/* bank picker */
.bank-list{ display:flex; flex-direction:column; gap:8px; margin-bottom:6px; }
.bank-row{
  display:flex; align-items:center; gap:12px; width:100%;
  padding:12px 14px; border-radius:var(--radius-sm);
  background:var(--surface-alt); cursor:pointer; text-align:left;
  transition:background .14s ease;
  box-shadow:var(--raise);
}
.bank-row:hover{ background:var(--surface-alt); }
.bank-row.selected{ background:var(--blue-tint); box-shadow:var(--press), inset 0 0 0 1.5px var(--blue-bright); }
.bank-row .bs-logo{ flex-shrink:0; }
.bank-row .br-txt{ flex:1; min-width:0; display:flex; flex-direction:column; gap:2px; }
.bank-row .br-1{ font-size:13.5px; font-weight:700; color:var(--ink); }
.bank-row .br-2{ font-size:11.5px; font-weight:500; color:var(--text-faint); }
.bank-row .br-tick{ flex-shrink:0; opacity:0; color:var(--blue-bright); }
/* the add row is an option, not an action - dashed edge so it reads as the
   empty slot at the end of the list rather than a fifth account */
.bank-add{ background:transparent; box-shadow:none; border:1px dashed var(--hairline-2); }
.bank-add .br-1{ color:var(--text-soft); }
.bs-add{ background:var(--surface-alt); color:var(--text-soft); }
.bank-row.selected .br-tick{ opacity:1; }

/* `.text-btn` gives small links a bigger invisible tap target via
   `padding:8px; margin:-8px` on all sides - symmetric, so it cancels out
   for an inline link. This is block-level and `width:100%` instead, where
   the negative margin doesn't cancel out: with width fixed rather than
   auto, `margin-left:-8px` still shifts the whole box left, but there is
   no following content for `margin-right:-8px` to reclaim, so the box
   (and the centered text inside it) rendered 8px left of true center
   under the button above it. Reset all four sides, not just top. */
.form-cancel{ display:block; width:100%; text-align:center; margin:12px 0 0; padding:8px; font-size:13px; }
.form-cancel:hover{ color:var(--ink); }

/* a receipt being reviewed is a record, not a celebration */
.view-success.review .paid-doodle{ display:none; }
.rr b.failed{ color:var(--paper-alert); }
.rr b.pending{ color:var(--paper-soft); }

/* ---- printing the parchi ---------------------------------------------
   A thermal printer does not slide paper out smoothly, it ratchets it out
   one roller-step at a time. So the reveal is a steps() animation on a
   clip-path rather than an ease: 18 discrete feeds, which is what sells it
   as a machine instead of a fade. The wrapper carries the clip, the slip
   keeps its own torn-edge clip, and the drop-shadow follows both because
   filter is applied after clipping. */
.printer{
  width:calc(100% + 8px); margin:0 -4px; height:0;
  border-radius:var(--radius-md) var(--radius-md) 3px 3px;
  background:linear-gradient(180deg, #3C4360 0%, #1D2340 55%, #12172A 100%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.09),
    inset 0 -3px 6px rgba(0,0,0,0.65),
    0 4px 10px rgba(0,0,0,0.5);
  opacity:0; transition:height .26s ease, opacity .26s ease;
  position:relative;
}
/* two vents, so the block reads as a housing with something running inside
   it rather than a plain bar */
.printer .vent{
  position:absolute; left:15px; top:8px; width:20px; height:2px; border-radius:1px;
  background:rgba(255,255,255,0.08);
  box-shadow:0 5px 0 rgba(255,255,255,0.08);
}
/* the slot the parchi actually feeds out of */
.printer::before{
  content:""; position:absolute; left:16px; right:16px; bottom:5px; height:4px;
  border-radius:2px; background:#05070E;
  box-shadow:inset 0 1px 2px rgba(0,0,0,0.9);
}
/* a status light, dark until the roller is actually running - the one cue
   that this is a machine and not decoration */
.printer::after{
  content:""; position:absolute; right:13px; top:11px; width:6px; height:6px;
  border-radius:50%; background:#3A4160; transition:background .2s ease, box-shadow .2s ease;
}
.printing-on .printer{ height:36px; opacity:1; margin-bottom:-8px; }
.printing-on .printer::after{ background:var(--yellow); box-shadow:0 0 7px 1.5px rgba(255,218,0,0.7); }

/* has to out-specify `.view-success.active .parchi-wrap`, which already owns
   the animation property for the slip's entrance */
/* Paper leaves a thermal printer header-first, so the slip is uncovered
   from the top down, its leading edge crawling out from under the machine.
   The printer sits above the paper and overlaps it, so there is no seam
   where the slip begins - it can only have come out of the slot. */
.printer{ z-index:2; }
.parchi-wrap{ z-index:1; }
.view-success.active .parchi-wrap.printing{
  /* kept in step with PRINT_DUR in script.js - the sound is synthesised off
     that same constant, so the two must move together */
  animation:feed 2.1s steps(18, end) both;
}
@keyframes feed{
  from{ clip-path:inset(0 0 100% 0); }
  to  { clip-path:inset(0 0 0 0); }
}
/* and the slip is dragged down a step at a time as the rollers bite, so
   the paper is moving rather than being wiped in */
.view-success.active .parchi-wrap.printing .parchi{ animation:feedpull 2.1s steps(18, end) both; }
@keyframes feedpull{
  from{ transform:translateY(-13px); }
  to  { transform:translateY(0); }
}
@media (prefers-reduced-motion:reduce){
  .view-success.active .parchi-wrap.printing,
  .view-success.active .parchi-wrap.printing .parchi{ animation:none; }
  .printing-on .printer{ height:24px; }
}

/* Once a slip exists in your hand, sharing it is the likely next move, so
   the button stops being one of two equals and starts asking for the tap. */
.btn-quiet.emphasis{
  background:var(--blue-tint); color:var(--blue-bright);
  box-shadow:inset 0 0 0 1.5px var(--blue-bright);
  animation:emphPop .42s cubic-bezier(.2,1.3,.35,1) both;
}
@keyframes emphPop{
  0%{ transform:scale(1); }
  55%{ transform:scale(1.045); }
  100%{ transform:scale(1); }
}

/* ---------- share, redacted ----------
   A payment screenshot is the proof people forward on WhatsApp, and the
   thing they forward carries their VPA and their bank account with it.
   The slip you share is not the slip you keep: the reference that proves
   the payment stays, everything someone could act on is struck out. */
.share-preview{
  background:var(--paper); border-radius:var(--radius-sm);
  padding:15px 15px 16px; margin-bottom:12px;
}
.sp-top{ display:flex; align-items:baseline; justify-content:space-between; gap:10px; margin-bottom:11px; }
.sp-amt{ font-family:var(--font-display); font-size:23px; font-weight:800; color:var(--paper-ink); letter-spacing:-0.03em; }
.sp-to{ font-size:12px; font-weight:600; color:var(--paper-soft); text-align:right; }
.sp-rows{ border-top:1px solid var(--paper-rule); padding-top:10px; display:flex; flex-direction:column; gap:7px; }
.sp-row{ display:flex; align-items:center; justify-content:space-between; gap:12px; font-size:11.5px; }
.sp-row span{ color:var(--paper-soft); font-weight:500; }
.sp-row b{ color:var(--paper-ink); font-weight:700; font-variant-numeric:tabular-nums; }
/* the dash standing in for a redacted value. It is a placeholder for text
   that is deliberately absent, not text - so it takes paper-faint rather
   than inventing a lighter grey of its own, which is what it had been. */
.sp-row.hidden-row b{ color:var(--paper-faint); letter-spacing:0.06em; }
.sp-why{
  display:flex; align-items:flex-start; gap:9px; padding:11px 12px; margin-bottom:14px;
  background:var(--surface-alt); border-radius:var(--radius-sm);
  font-size:11.5px; line-height:1.5; color:var(--text-soft); text-align:left;
}
.sp-why svg{ flex-shrink:0; stroke:var(--text-soft); margin-top:1px; }

/* The nudge preview deliberately does NOT use the paper world. Paper means
   "a payment happened" - a khata entry is a note between two people, not a
   transaction, and dressing it as a parchi would claim something that is not
   true. So it renders as what it actually is: a message, on the app's own
   dark ground. */
/* THE TWO REMINDERS. Same sheet, two jobs that must never be confused:
   one sends a message to another person, the other rings a bell on this
   phone. Copy alone cannot carry that - by the twentieth time nobody reads
   the sheet, they recognise it.

   So the split rides on the app's own colour rule rather than a decorative
   accent. Sending is an outbound act, and outbound is blue - the same blue
   the ledger already uses for money leaving. A reminder to yourself acts on
   nobody and moves nothing; it is pure attention, so it is yellow, which is
   exactly what yellow is allowed to mean here. */
.nudge-mark{
  width:34px; height:34px; border-radius:var(--radius-sm);
  display:flex; align-items:center; justify-content:center;
  margin:0 auto 12px;
}
.nudge-mark .nm-self{ display:none; }
.nudge-mark.self .nm-them{ display:none; }
.nudge-mark.self .nm-self{ display:block; }
.nudge-mark.them{ background:var(--blue-tint); color:var(--blue-bright); }
.nudge-mark.self{ background:var(--yellow-soft); color:var(--yellow); }

.nudge-preview{
  background:var(--surface-alt); border-radius:var(--radius-sm);
  padding:12px 13px 11px; margin-bottom:12px; text-align:left;
  box-shadow:inset 0 0 0 1px var(--border);
}
/* a message going out gets the outbound edge; the self-reminder keeps the
   plain surface, because it is a notification and notifications are plain */
.nudge-preview.them{ box-shadow:inset 0 0 0 1px rgba(147,169,245,0.26); }
.nudge-preview.self .np-who{ background:var(--yellow-soft); color:var(--yellow); }
.np-head{ display:flex; align-items:center; gap:7px; margin-bottom:8px; }
.np-who{
  width:19px; height:19px; border-radius:50%; flex-shrink:0;
  background:var(--blue-tint); color:var(--blue-bright);
  font-size:9px; font-weight:800; letter-spacing:0.02em;
  display:flex; align-items:center; justify-content:center;
}
.np-app{ font-size:11px; font-weight:800; color:var(--ink); letter-spacing:0.02em; }
.np-now{ font-size:11px; font-weight:500; color:var(--text-faint); }
/* the draft is editable because it goes out in the user's name - an app that
   writes in your voice and will not let you change the words is putting
   sentences in your mouth */
.np-body{
  font-size:13px; line-height:1.5; color:var(--ink); font-weight:500;
  outline:none; border-radius:var(--radius-sm); margin:0 -5px; padding:3px 5px;
  transition:background .15s ease, box-shadow .15s ease;
}
.np-body:hover{ background:rgba(233,237,250,0.04); }
.np-body:focus{ background:rgba(233,237,250,0.06); box-shadow:inset 0 0 0 1px var(--hairline-2); }
.np-body[contenteditable="false"]{ margin:0; padding:0; }
.np-body[contenteditable="false"]:hover{ background:none; }
.np-acts{ display:flex; gap:16px; margin-top:10px; padding-top:9px; border-top:1px solid var(--hairline); }
/* these are a drawing of the recipient's buttons, not buttons - the preview
   is a picture of another phone, so nothing in it should invite a tap */
.np-act{ font-size:11.5px; font-weight:700; color:var(--text-faint); }
.np-act-go{ color:var(--blue-bright); }

.foot-row{ display:flex; gap:9px; }
.foot-row .btn{ flex:1 1 0; min-width:0; }

/* ============================================================
   khata - the ledger of who owes what
   ============================================================ */
.khata-card{
  display:flex; flex-direction:column; gap:12px; width:calc(100% - 40px);
  margin:12px 20px 0; padding:15px 17px 16px;
  background:var(--surface); border-radius:var(--radius-md);
  box-shadow:var(--raise); cursor:pointer; text-align:left;
  transition:transform .14s ease;
}
.khata-card:active{ transform:scale(0.99); }
.kc-head{ display:flex; align-items:center; justify-content:space-between; }
.kc-title{ font-size:13.5px; font-weight:700; color:var(--ink); }
.kc-go{ display:flex; align-items:center; gap:3px; font-size:12px; font-weight:600; color:var(--blue-bright); }
.kc-split{ display:flex; align-items:center; gap:14px; }
.kc-half{ flex:1; display:flex; flex-direction:column; gap:3px; min-width:0; }
.kc-div{ width:1px; align-self:stretch; background:var(--hairline); }
.kc-lb{ font-size:11px; font-weight:600; color:var(--text-faint); }
.kc-amt{ font-size:19px; font-weight:800; letter-spacing:-0.02em; font-variant-numeric:tabular-nums; }
.kc-amt.get{ color:var(--green); }
.kc-amt.owe{ color:var(--ink); }
.rc-note{
  flex-shrink:0; align-self:center; margin-right:12px; white-space:nowrap;
  font-size:11px; font-weight:600; letter-spacing:.01em;
  color:var(--text-faint);
}
/* only a date that has passed earns colour on Home. Yellow is the joy
   colour in this system (the mark, the "Paid!" doodle) - using it for "you
   owe someone and it's late" was a mismatch dressed up as "the only other
   brand colour available." Blue already means act in this app; a passed
   due date is exactly that, an invitation to act, not a celebration. */
.rc-note.late{ color:var(--blue-bright); font-weight:700; }
.rc-note[hidden]{ display:none; }
/* .khata-ic used to override .rc-ic; they match now, so it carries nothing */
.kc-due[hidden]{ display:none; }

.page-head{ display:flex; align-items:center; gap:12px; padding:6px 20px 14px; flex-shrink:0; }
.page-head .page-title{ flex:1; text-align:center; }

.khata-sum{
  display:flex; align-items:center; gap:16px;
  margin:0 20px 16px; padding:16px 18px;
  background:var(--surface); border-radius:var(--radius-md); box-shadow:var(--raise);
}
.ks-half{ flex:1; display:flex; flex-direction:column; gap:4px; }
.ks-div{ width:1px; align-self:stretch; background:var(--hairline); }
.ks-lb{ font-size:11.5px; font-weight:600; color:var(--text-faint); }
.ks-amt{ font-size:23px; font-weight:800; letter-spacing:-0.025em; font-variant-numeric:tabular-nums; }
.ks-amt.get{ color:var(--green); }
/* red means a payment failed. Owing somebody is just a number, so it
   takes the plain text colour and lets the words carry the urgency. */
.ks-amt.owe{ color:var(--ink); }

.khata-scroll{ flex:1; overflow-y:auto; scrollbar-width:none; padding:0 20px; }
.khata-scroll::-webkit-scrollbar{ display:none; }
.khata-list{ display:flex; flex-direction:column; gap:10px; padding-bottom:6px; }

.khata-row{
  background:var(--surface); border-radius:var(--radius-md);
  padding:14px 16px; box-shadow:var(--raise);
  display:flex; flex-direction:column; gap:12px;
}
.kr-top{ display:flex; align-items:center; gap:12px; }
.kr-ic{
  width:40px; height:40px; border-radius:var(--radius-icon); flex-shrink:0;
  display:flex; align-items:center; justify-content:center;
  font-weight:700; font-size:14px;
}
.kr-txt{ flex:1; min-width:0; display:flex; flex-direction:column; gap:3px; }
.kr-name{ font-size:14px; font-weight:700; color:var(--ink); }
.kr-note{ font-size:11.5px; color:var(--text-faint); }
.kr-amt{ font-size:16px; font-weight:800; letter-spacing:-0.02em; font-variant-numeric:tabular-nums; }
.kr-amt.get{ color:var(--green); }
.kr-amt.owe{ color:var(--ink); }
/* Two states, told apart by whether colour is present at all rather than by
   how loud it is. An upcoming date is just information and takes the plain
   quiet chip; a passed one picks up the dimmed yellow. An earlier pass made
   late a solid yellow badge - it read as a warning label stuck on a person,
   which is the wrong tone for a ledger of friends and family. Calm still
   distinguishes; it just does not accuse. */
.kr-due{
  font-size:11px; font-weight:600; color:var(--text-faint);
  background:var(--surface-alt); padding:3px 8px; border-radius:var(--radius-full);
  align-self:flex-start;
}
.kr-due.late{
  color:var(--blue-bright); background:var(--blue-tint); font-weight:700;
}
.kr-acts{ display:flex; gap:8px; }
.kr-acts .btn{ flex:1; padding:10px 12px; font-size:12.5px; }

.seg{
  display:flex; gap:4px; padding:4px; margin-bottom:14px;
  background:var(--surface-alt); border-radius:var(--radius-sm);
}
.seg-item{
  flex:1; padding:9px 10px; border-radius:var(--radius-sm); cursor:pointer;
  font-size:12.5px; font-weight:700; color:var(--text-soft); text-align:center;
  transition:background .16s ease, color .16s ease;
}
.seg-item.active{ background:var(--blue); color:#FFFFFF; }

.field-row{ margin-bottom:9px; }
.field-row input{
  width:100%; padding:13px 14px; border-radius:var(--radius-sm);
  background:var(--surface-alt); border:1.5px solid transparent;
  font-size:13.5px; font-weight:500; color:var(--ink); outline:none;
}
.field-row input::placeholder{ color:var(--text-faint); }
.field-row input:focus{ border-color:var(--blue); }

.empty-state{ display:none; text-align:center; padding:40px 20px; }
.empty-state.show{ display:block; }
/* History's empty state already had an icon; Khata's did not - same role,
   same treatment, so it stops being the one blank-slate screen in the app
   with no glyph at all. */
.es-icon{ color:var(--text-faint); margin-bottom:12px; }
.es-icon svg{ display:block; margin:0 auto; }
.es-title{ font-size:15px; font-weight:700; color:var(--ink); }
.es-sub{ font-size:13px; color:var(--text-faint); margin-top:5px; }

/* anything set 20px or larger takes the Display cut */
.amount-field, .p-amount, .balance-amount, .ob-title, .ks-amt,
.payee-name, .success-amount, .es-title{
  font-family:var(--font-display);
}

/* ============================================================
   onboarding — the real UPI registration
   ============================================================
   A progress bar, not carousel pips: this is a setup you are getting
   through, and telling someone how much is left is the single kindest
   thing a long regulated flow can do. */
.onboarding{
  position:absolute; inset:0; z-index:40;
  background:var(--bg); color:var(--ink);
  display:flex; flex-direction:column;
  transition:opacity .32s ease, transform .32s cubic-bezier(.32,.72,0,1);
}
.onboarding.done{ opacity:0; transform:translateY(-14px); pointer-events:none; }

/* splash carries no progress bar, back or skip - those are chrome for a
   registration flow the user is "getting through"; the splash is not a step
   in that flow, it is the beat before it starts */
.onboarding[data-slide="0"] .ob-head,
.onboarding[data-slide="0"] .ob-foot{ visibility:hidden; }

.ob-head{ display:flex; align-items:center; gap:14px; padding:6px 20px 18px; flex-shrink:0; }
.ob-back{ flex-shrink:0; }
.ob-back[hidden]{ visibility:hidden; display:flex; }
.ob-bar{ flex:1; height:4px; border-radius:3px; background:var(--surface-alt); overflow:hidden; }
.ob-bar i{
  display:block; height:100%; width:12.5%; border-radius:3px;
  background:var(--yellow);
  transition:width .34s cubic-bezier(.32,.72,0,1);
}
/* The short steps - number, name, passcode - used ~160px of a 519px body and
   left the rest as a void above a thumb-pinned button, which reads as a
   screen that failed to load the rest of itself. `margin-block:auto` centres
   a step only when there is room to; on the tall ones (the 130-bank list,
   the PIN step) the auto margins collapse to zero and nothing moves. Using
   auto margins rather than justify-content matters here, because a centred
   flex child that overflows gets its top clipped and its scroll-up stolen. */
.ob-body{ flex:1; overflow-y:auto; scrollbar-width:none; padding:0 24px;
          display:flex; flex-direction:column; }
.ob-step.on{ margin-block:auto; width:100%; }
.ob-body::-webkit-scrollbar{ display:none; }
.ob-step{ display:none; padding-top:8px; }
.ob-step.on{ display:block; animation:obIn .3s cubic-bezier(.32,.72,0,1) both; }
@keyframes obIn{ from{ opacity:0; transform:translateY(10px); } to{ opacity:1; transform:none; } }
@media (prefers-reduced-motion:reduce){ .ob-step.on{ animation:none; } .ob-bar i{ transition:none; } }

.ob-title{
  font-size:27px; line-height:1.2; font-weight:800; letter-spacing:-0.03em;
  margin:0 0 10px; text-wrap:balance;
}
.ob-sub{ font-size:14px; line-height:1.55; color:var(--text-soft); margin:0 0 24px; max-width:34ch; }
.ob-sub b{ color:var(--ink); font-weight:700; }

.ob-note{
  display:flex; align-items:flex-start; gap:9px; margin-top:20px;
  font-size:12px; line-height:1.5; color:var(--text-faint);
}
.ob-note svg{ flex-shrink:0; margin-top:1px; }

/* 0 · splash. One-shot, not ambient - the mark settles in once and the
   wordmark follows a beat later, then the auto-advance in OB_STEPS[0]
   moves on. No loading copy: there is nothing real to wait on, so the
   screen doesn't pretend there is. */
.ob-splash{
  height:100%; display:none; flex-direction:column; align-items:center;
  justify-content:center; gap:4px; padding-top:0;
}
.ob-step.ob-splash.on{ display:flex; }
.splash-mark{ animation:splashMark .5s cubic-bezier(.3,1.15,.4,1) both; }
.splash-word{
  font-family:var(--font-display); font-size:34px; font-weight:800;
  letter-spacing:-0.02em; color:var(--ink);
  /* opacity:1 as the resting value, not 0 -- the animation's own fill-mode
     still holds it at 0 through the delay and the run, but if animation is
     stripped out entirely (prefers-reduced-motion, or a static capture)
     this is what's left, and it must be the finished state, not the start
     of one that never plays. */
  opacity:1; animation:splashWord .4s ease .2s both;
}
@keyframes splashMark{ from{ opacity:0; transform:scale(.86); } to{ opacity:1; transform:scale(1); } }
@keyframes splashWord{ from{ opacity:0; transform:translateY(4px); } to{ opacity:1; transform:none; } }
@media (prefers-reduced-motion:reduce){ .splash-mark, .splash-word{ animation:none; opacity:1; } }

/* 1 · language. Both names, because someone who reads only Devanagari needs
   the native one and someone helping them set the phone up needs the Latin. */
.lang-grid{ display:grid; grid-template-columns:repeat(2,1fr); gap:8px; padding-bottom:8px; }
.lang{
  display:flex; flex-direction:column; align-items:flex-start; gap:2px;
  padding:12px 14px; border-radius:var(--radius-sm); cursor:pointer;
  background:transparent; box-shadow:var(--raise);
  text-align:left; transition:background .14s ease, box-shadow .14s ease;
}
.lang-native, .lg-native{ font-size:15px; font-weight:600; color:var(--ink); line-height:1.25; }
.lg-latin{ font-size:11.5px; font-weight:500; color:var(--text-faint); }
.lang:hover{ background:var(--surface); }
.lang.active{ background:var(--blue); box-shadow:inset 0 0 0 1px var(--blue); }
.lang.active .lg-native{ color:#FFFFFF; }
.lang.active .lg-latin{ color:rgba(255,255,255,0.72); }
.lang.shake{ animation:shake .34s cubic-bezier(.36,.07,.19,.97); }
@media (prefers-reduced-motion:reduce){ .lang.shake{ animation:none; } }

/* demo-only notice for the other 14 languages - see script.js comment.
   Reuses the amount-error grow/fade shape rather than a fixed toast, so it
   doesn't need its own overlay stacking or dismiss logic. */
.lang-note{
  max-height:0; opacity:0; overflow:hidden;
  text-align:center; font-size:12px; font-weight:600; color:var(--text-faint);
  transition:max-height .2s ease, opacity .2s ease, margin-top .2s ease;
}
.lang-note.show{ max-height:20px; opacity:1; margin-top:10px; }

/* 2 · number */
.phone-field{
  display:flex; align-items:center; gap:12px;
  padding:15px 16px; border-radius:var(--radius-sm);
  background:var(--surface-alt); border:1.5px solid transparent;
}
.phone-field:focus-within{ border-color:var(--blue); }
.phone-field.bad{ border-color:var(--alert); }
.pf-code{ font-size:17px; font-weight:700; color:var(--text-soft); }
.phone-field input{
  flex:1; min-width:0; border:none; outline:none; background:transparent;
  font-size:19px; font-weight:700; letter-spacing:0.06em; color:var(--ink);
  font-variant-numeric:tabular-nums;
}
.phone-field input::placeholder{ color:var(--text-faint); letter-spacing:0.06em; }

/* 4 · name - same field chrome as the phone step, but a name doesn't get
   tabular-nums letter-spacing (that's a numeric-input tell, wrong for a name) */
.name-field{
  padding:15px 16px; border-radius:var(--radius-sm);
  background:var(--surface-alt); border:1.5px solid transparent;
}
.name-field:focus-within{ border-color:var(--blue); }
.name-field input{
  width:100%; border:none; outline:none; background:transparent;
  font-size:17px; font-weight:700; color:var(--ink);
}
.name-field input::placeholder{ color:var(--text-faint); font-weight:500; }

/* 3 · device binding */
/* Art on an onboarding step hangs off the same left edge as the title and
   the body copy. It used to be centred while every word under it was
   left-aligned, which reads as two layouts sharing a screen - and it is the
   only step in the flow that did it. */
.bind-art{
  position:relative; width:118px; height:118px; margin:14px 0 30px;
  display:flex; align-items:center; justify-content:center;
  color:var(--yellow);
}
.bind-ring{
  position:absolute; inset:0; border-radius:50%;
  border:2px solid var(--yellow); opacity:.28;
  animation:bindPulse 2s ease-out infinite;
}
.bind-ring.r2{ animation-delay:1s; }
@keyframes bindPulse{
  from{ transform:scale(.55); opacity:.5; }
  to  { transform:scale(1);   opacity:0; }
}
@media (prefers-reduced-motion:reduce){ .bind-ring{ animation:none; opacity:.3; } }

/* 4 · app lock */
.ob-dots-row{ justify-content:center; margin:6px 0 18px; }
.ob-keypad{ padding:0; }

/* 5 · bank */
.ob-search{ margin-bottom:14px; }
.bank-pick{ display:flex; flex-direction:column; gap:8px; padding-bottom:10px; }
.bank-opt{
  display:flex; align-items:center; gap:12px; width:100%;
  padding:13px 14px; border-radius:var(--radius-sm); cursor:pointer;
  background:var(--surface); box-shadow:var(--raise); text-align:left;
}
.bank-opt.sel{ box-shadow:var(--press), inset 0 0 0 1.5px var(--blue-bright); background:var(--blue-tint); }
.bank-opt .bs-logo{ flex-shrink:0; }
/* a group header, not a card - it labels the run beneath it and takes up as
   little of the list as a label can */
.bank-head{ font-size:11px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:var(--text-faint); padding:12px 2px 4px; }
.bank-head:first-child{ padding-top:2px; }
.bank-none{ font-size:12.5px; line-height:1.55; color:var(--text-faint); padding:16px 2px; }
.bank-opt b{ font-size:13.5px; font-weight:700; color:var(--ink); }

/* 6 · account discovery */
.acct-found{
  display:flex; align-items:center; gap:13px; padding:16px;
  border-radius:var(--radius-md); background:var(--surface); box-shadow:var(--raise);
}
.acct-found[hidden]{ display:none; }
.af-txt{ flex:1; min-width:0; display:flex; flex-direction:column; gap:3px; }
/* the name from step 4, styled as an eyebrow above the bank name - the card
   below already reads "HDFC Bank / Savings 4471"; this line answers whose
   account it is without out-competing the bank name for attention */
.af-name{ font-size:12px; font-weight:600; color:var(--text-faint); text-transform:uppercase; letter-spacing:.04em; }
.af-1{ font-size:14px; font-weight:700; color:var(--ink); }
.af-2{ font-size:12px; color:var(--text-faint); font-variant-numeric:tabular-nums; }
.af-tick{ color:var(--green-300); }
.af-tick svg{ display:block; }

/* 7 · UPI PIN */
.card-fields{ display:flex; gap:10px; }
/* an explicit display beats the UA rule for [hidden], so the two PIN paths
   would both render at once without this */
.card-fields[hidden]{ display:none; }
.cf{ flex:1; display:flex; flex-direction:column; gap:7px; }
.cf-sm{ flex:0 0 108px; }
.cf label{ font-size:11.5px; font-weight:700; color:var(--text-faint); letter-spacing:.04em; }
.cf input{
  width:100%; padding:14px 14px; border-radius:var(--radius-sm);
  background:var(--surface-alt); border:1.5px solid transparent;
  font-size:16px; font-weight:700; letter-spacing:0.1em; color:var(--ink);
  outline:none; font-variant-numeric:tabular-nums;
}
.cf input::placeholder{ color:var(--text-faint); }
.cf input:focus{ border-color:var(--blue); }
/* A field that is wrong says so at its own edge as well as in words, so the
   sentence below does not have to name which of two boxes it means. */
.cf input.bad, .cf input.bad:focus{ border-color:var(--alert); }
.ob-error{
  display:flex; align-items:flex-start; gap:8px; margin-top:14px;
  font-size:12px; line-height:1.45; font-weight:600; color:var(--alert);
}
.ob-error svg{ flex-shrink:0; margin-top:1px; }
.ob-error[hidden]{ display:none; }
/* the note is the calm state and the error replaces it - two blocks of small
   print stacked under a field is how a form starts looking like a form */
.ob-step.has-error .ob-note{ display:none; }

/* 8 · done */
.done-art{
  display:flex; align-items:center; justify-content:flex-start;
  margin:10px 0 28px;
}
.done-art img{ filter:drop-shadow(0 16px 26px rgba(0,0,0,0.45)); }

.ob-foot{ padding:14px 24px 30px; display:flex; flex-direction:column; gap:10px; flex-shrink:0; }
.ob-alt{ text-align:center; font-size:13px; }
.ob-alt[hidden]{ display:none; }

/* ---- devices ---------------------------------------------------------
   A row here is not a list row with a chevron: there is nothing further in,
   and the only thing you can do is take it away. So the action sits on the
   row, in words, and the row you are reading this on says so instead. */
.dv-lede{ font-size:13px; line-height:1.55; color:var(--text-soft); margin:2px 20px 18px; }
.dv-list{ display:flex; flex-direction:column; gap:10px; padding:0 20px; }
.dv{ display:flex; gap:12px; padding:14px; border-radius:var(--radius-md); background:var(--surface); box-shadow:var(--raise); }
.dv-ic{ width:38px; height:38px; border-radius:var(--radius-icon); flex-shrink:0; display:flex; align-items:center; justify-content:center; background:var(--surface-alt); color:var(--text-soft); }
.dv-txt{ flex:1; min-width:0; display:flex; flex-direction:column; gap:3px; }
.dv-name{ font-size:14px; font-weight:700; color:var(--ink); }
.dv-meta{ font-size:12px; color:var(--text-faint); }
.dv-this{ align-self:flex-start; margin-top:4px; font-size:10.5px; font-weight:700; letter-spacing:.04em; text-transform:uppercase; color:var(--blue-bright); background:var(--blue-tint); padding:3px 8px; border-radius:var(--radius-full); }
/* Removing a device is destructive and cannot be undone from here, so it is
   never the loud thing on the row - it is a quiet word that asks again. */
.dv-act{ align-self:center; flex-shrink:0; font-size:12.5px; font-weight:700; color:var(--alert); padding:8px; cursor:pointer; }
.dv-act:disabled{ color:var(--text-faint); cursor:default; }
.dv.gone{ opacity:.55; }
.dv.gone .dv-name{ color:var(--text-faint); }
/* a copy/download is not a removal, so it does not borrow the alert red -
   red stays reserved for the one destructive action on this list */
.dv-act.blue{ color:var(--blue-bright); }

/* ---- account sub-pages ------------------------------------------------
   Ten rows used to open one shared preview sheet and nothing further. Each
   now opens a real page, so the list below is content rather than another
   description of content still to come. */

/* rewards: a scratch card stays a card until it is tapped, the way GPay's
   and PhonePe's own cashback cards work - a reveal is an action, not a
   number printed where anyone glancing at the phone can read it */
.scratch-row{ display:flex; gap:10px; padding:0 20px; }
.scratch-card{
  flex:1; aspect-ratio:1/1.05; border-radius:var(--radius-md);
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:8px;
  background:var(--surface); box-shadow:var(--raise); color:var(--text-faint);
  font-size:11.5px; font-weight:600; cursor:pointer; transition:transform .12s ease;
}
.scratch-card:active{ transform:scale(0.96); }
.scratch-card.revealed{
  background:var(--green-surface); color:var(--green); cursor:default; box-shadow:var(--raise);
}
.scratch-card b{ font-size:20px; font-weight:700; color:var(--green); }
.scratch-card.revealed span{ color:var(--text-soft); font-weight:500; }

/* refer & earn */
.refer-code-box{
  margin:18px 20px 0; padding:16px; border-radius:var(--radius-md);
  background:var(--surface); box-shadow:var(--raise);
  display:flex; align-items:center; gap:10px;
}
.refer-code-label{ font-size:11px; font-weight:600; color:var(--text-faint); }
.refer-code{ flex:1; font-size:16px; font-weight:700; letter-spacing:.06em; color:var(--ink); }
.refer-copy{
  font-size:12.5px; font-weight:700; color:var(--blue-bright);
  background:var(--blue-tint); border:1px solid rgba(51,85,218,0.3);
  padding:8px 14px; border-radius:var(--radius-sm); cursor:pointer;
}
.refer-stats{ display:flex; gap:10px; margin:16px 20px 0; }
.refer-stats > div{
  flex:1; padding:14px; border-radius:var(--radius-md); background:var(--surface);
  box-shadow:var(--raise); text-align:center;
}
.refer-stats b{ display:block; font-size:19px; font-weight:700; color:var(--ink); }
.refer-stats span{ display:block; font-size:11px; color:var(--text-faint); margin-top:2px; }

/* get help: an accordion, because four answers printed open at once is a
   wall of text nobody scans looking for the one line they need */
.faq-list{ display:flex; flex-direction:column; gap:8px; padding:0 20px; }
.faq-item{ border-radius:var(--radius-md); background:var(--surface); box-shadow:var(--raise); overflow:hidden; }
.faq-q{
  width:100%; display:flex; align-items:center; justify-content:space-between; gap:12px;
  padding:14px; text-align:left; font-size:13px; font-weight:600; color:var(--ink); cursor:pointer;
}
.faq-q svg{ flex-shrink:0; stroke:var(--text-faint); transition:transform .18s ease; }
.faq-item.open .faq-q svg{ transform:rotate(180deg); }
.faq-a{ max-height:0; overflow:hidden; transition:max-height .22s ease; }
.faq-a p{ margin:0 14px 14px; font-size:12.5px; line-height:1.55; color:var(--text-soft); }
.faq-item.open .faq-a{ max-height:160px; }
