/* ============================================================================
 * DHANAM CUSTOMER-APP THEME  (Figma "Customer Facing Dhanam App" re-skin)
 * ----------------------------------------------------------------------------
 * Loaded LAST in index.html, so its cascade wins over styles.css +
 * dhanam-theme.css. Everything here is either:
 *   (a) a --dh-* design token,
 *   (b) a .dh-* component class (inert until used by a converted screen), or
 *   (c) a rule scoped to `body.dh-customer` (added on customer login only).
 * Staff / prospect screens are NEVER affected (they don't get .dh-customer).
 *
 * Owner: Claude Code. Ink + antique-gold palette, Poppins app-wide for body
 * AND headings (2026-08-06, Prem — was Inter body + Playfair Display headings).
 * Lifted verbatim from the approved POC (_skin_poc/).
 * PR 1 of the screen-by-screen re-skin: foundation only — no screen markup
 * is converted yet; later PRs rewrite each cust*Screen to use .dh-* parts.
 * ==========================================================================*/

:root {
  /* Palette — exact values from the Figma theme.css */
  --dh-bg:        #FFFFFF;
  --dh-ink:       #0A0A0A;
  --dh-soft:      #3A3A3A;
  --dh-muted:     #8A8A8A;
  --dh-muted2:    #6B6B6B;
  --dh-faint:     #C4C4C4;
  --dh-faint2:    #AAAAAA;
  --dh-gold:      #C9962A;
  --dh-gold-light:#E8C96A;
  --dh-gold-dim:  rgba(201,150,42,0.10);
  --dh-gold-dim2: rgba(201,150,42,0.12);
  --dh-gold-border:rgba(201,150,42,0.25);
  /* THE hero-card surface. One ramp for every card in the app (2026-08-08,
     Prem: "don't use this kind of bg-colour, check all the places and update to
     the new colour") — the near-black espresso ink below is retired and
     --dh-cardgrad now points at the gold ramp, so all three consumers
     (.dh-darkcard · .dh-card-dark · the banner-error fallback) move together.

     Retired value, kept for one-line revert:
       radial-gradient(115% 95% at 88% 0%, rgba(201,150,42,0.26) 0%, rgba(201,150,42,0) 58%),
       linear-gradient(148deg, #241B0E 0%, #16110A 52%, #0B0906 100%);

     ⚠ CONTRAST — measured, not assumed. The ramp runs #5e4002 (top) → #c9962a
     (bottom), and white reads 9.5:1 on the top stop but only 2.66:1 on the
     bottom one. An earlier note here claimed "5.70 rising to 9.1 at the bottom";
     that had the ends backwards. Big text is fine; small print low on a tall
     card is NOT at the 4.5:1 body minimum. Mitigated below by forcing the
     on-card tokens to solid white (the best available in a white-on-gold
     scheme). To make small text pass outright, darken the closing stop —
     #8A6305 puts white at 5.6:1 — which is a one-token change here. */
  --dh-goldcard: linear-gradient(155deg, #5e4002 0%, #c9962a 100%);
  /* Distance from the screen edge to the support-FAB group. A token because the
     ≥720px block moves the whole group in to the app's 520px column, and the
     open arc adds its own per-button offset on top of it (_applyFabCorner). */
  --dh-fab-gutter: 16px;
  --dh-cardgrad: var(--dh-goldcard);
  /* The header band's cream — the colour that continues the topbar down the
     page (.dh-homeband). Tokenised 2026-08-10 because a second consumer
     appeared: the profile camera button rings itself in the band colour so it
     doesn't read as a hole punched in the header. Two literals would drift. */
  --dh-band: #f8f0e0;
  --dh-oncard-muted: rgba(255,255,255,0.88);
  --dh-error:     #D94040;
  --dh-error-dim: rgba(217,64,64,0.08);
  --dh-success:   #1A7F4B;
  --dh-success-dim:rgba(26,127,75,0.08);
  --dh-warn:      #C05C00;
  --dh-warn-dim:  rgba(192,92,0,0.08);
  /* Solid warning/notice surface. Replaces the Tailwind amber-100 literal
     #fef3c7 that had spread to ~60 inline style strings across the SPA
     (2026-08-11, Prem: "that fef3c7 bg colour remove in all the place, make to
     theme colour"). --dh-warn-dim is 8%-alpha and too weak to carry a badge or
     a panel on a tinted card, so this is an OPAQUE token, hue-shifted off the
     gold ramp (#C9962A) instead of amber-100's yellow so notices read as part
     of the ink+gold theme.
     ⚠ CONTRAST — measured. The existing notice ink stays legible on it:
     #92400e reads 5.94:1 and #78350f 8.0:1 (both clear the 4.5:1 body
     minimum; amber-100 gave 6.37:1, so this is a ~0.4 step down, not a cliff).
     Keep any new on-warn text at #92400e or darker.
     --dh-warn-line is the matching border/left-accent; it replaces the
     #fde68a / #f59e0b / #fcd34d / #fbbf24 / #b45309 spread that sat alongside
     those backgrounds. Deliberately NOT var()-with-fallback: several call
     sites pack the colour into comma- or colon-split strings
     ('#fef3c7,#92400e'.split(',')), which a fallback comma would break. */
  --dh-warn-bg:   #F6EAD1;
  --dh-warn-line: #D9B36A;
  --dh-surface:   #F6F6F6;
  --dh-surface2:  #F8F8F8;
  /* Sheet chrome — the tinted bar behind a bottom sheet's grip + header. Warm
     rather than the neutral greys above, so it reads as part of the ink+gold
     theme instead of a grey browser dialog. */
  --dh-sheet-hdr: #FBF7EF;
  --dh-border:    rgba(0,0,0,0.08);
  --dh-border2:   rgba(0,0,0,0.07);
  /* Display face for headings, greetings and hero amounts.
   * APP FONT = POPPINS app-wide (2026-08-06, Prem) — body AND headings are now
   * one family, replacing the old Inter-body / Playfair-Display-headings pair.
   * --dh-display is the accurate name; --dh-serif is kept as an alias purely so
   * the ~20 existing `var(--dh-serif)` call sites (and the inline
   * `var(--dh-serif, ...)` fallbacks in index.html) keep working untouched.
   * NOTE the fallback chain is now SANS, not serif: if Poppins fails to load,
   * headings must degrade to the same system sans the body text uses, not to
   * Georgia — otherwise a font-load failure would leave the app half-serif. */
  --dh-display:   'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --dh-serif:     var(--dh-display);

  /* Cross-device parity: font/spacing scales for Android WebView text rendering differences.
     iOS WKWebView (iPhone) is the design reference (scale = 1.0).
     Android WebView renders text wider and with more line-height by default.
     These variables are overridden in html.platform-android to compress layout. */
  --dh-font-scale: 1;
  --dh-card-pad-scale: 1;
  --dh-line-height-tight: 1.35;
  --dh-line-height-normal: 1.45;
  --dh-letter-spacing-tight: 0;
}

/* ---------------------------------------------------------------------------
 * ANDROID PLATFORM PARITY — WebView text rendering compensation.
 * Android WebView renders fonts with different metrics than iOS WKWebView.
 * This parity layer tightens text rendering on Android to match iPhone layout. */
html.platform-android {
  --dh-font-scale: 0.96;
  --dh-card-pad-scale: 0.95;
  --dh-line-height-tight: 1.30;
  --dh-line-height-normal: 1.40;
  --dh-letter-spacing-tight: -0.02em;
  -webkit-text-size-adjust: 100% !important;
  text-size-adjust: 100% !important;
}

html.platform-android body {
  letter-spacing: -0.3px;
  word-spacing: -0.5px;
}

/* ---------------------------------------------------------------------------
 * LOGIN SCREEN — Figma "Customer Facing Dhanam App" white+gold.
 * NOT scoped to body.dh-customer (the login shows pre-login, before that class
 * is set), so these rules are targeted at #loginScreen specifically.
 * ------------------------------------------------------------------------- */
#loginScreen #loginLogo img { width: 150px; max-width: 62%; height: auto; }
#loginScreen #loginPanelTitle {
  font-family: var(--dh-serif);
  font-weight: 700; letter-spacing: -0.01em; color: var(--dh-ink);
}
/* phone-first OTP form: gold primary button to match the landing */
#loginScreen #loginSubmitBtn {
  background: var(--dh-gold); color: var(--dh-ink); border: none; border-radius: 14px;
  font-weight: 700; box-shadow: 0 4px 16px rgba(201,150,42,.30);
}
#loginScreen #loginSubmitBtn:hover { background: var(--dh-gold); filter: brightness(0.97); }
/* signup lives on the landing now — hide the redundant in-form "Sign Up" button */
#loginScreen #loginSignupBtn { display: none !important; }
/* rounder inputs to match the design */
#loginScreen .login-input-wrap { border-radius: 13px; }

/* No sideways scroll on any login step.
 * The decorative gold glow (.login-container::before, styles.css) is
 * `width: min(900px, 110%)` and centred, so on a phone it deliberately hangs
 * ~5% past each edge. #loginScreen sets `overflow-y: auto`, and per spec a
 * non-`visible` value on one axis forces the other to `auto` as well — so that
 * intentional overhang became a real horizontal SCROLLBAR rather than being
 * clipped. Pinning overflow-x keeps the glow bleeding off-screen as designed
 * while making the axis unscrollable. Vertical scrolling is untouched. */
#loginScreen { overflow-x: hidden; }

/* ---------------------------------------------------------------------------
 * SIGN-IN VIEW (#loginPanel) — back arrow → lock badge → heading → filled
 * fields → gold CTA → "OR" alternate sign-in → sign-up link, plus the OTP
 * step (#loginPanel.dhlg-otp): centred heading + 6 segmented code boxes.
 * Everything is scoped to #loginPanel / .dhlg-*, so the landing view and the
 * prospect/enquiry/appointment panels that share .login-box are untouched.
 * ------------------------------------------------------------------------- */
/* The sign-in view carries its own heading, so the shared lotus + <h1> above it
   are hidden while it's open. Toggled by _dhlgEnterSignIn(); always cleared by
   _hideAllPreLoginPanels() so every other panel gets them back. */
#loginScreen.dhlg-form #loginLogo,
#loginScreen.dhlg-form #loginPanelTitle { display: none; }

.dhlg-back {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; margin: 0 0 26px -8px;
  border-radius: 50%;
  color: var(--dh-ink, #0A0A0A);
  text-decoration: none; flex-shrink: 0;
}
.dhlg-back:hover { background: rgba(0,0,0,0.05); }
/* On phones/tablets the form is vertically centred in the viewport, so an
   in-flow back arrow floats halfway down the screen. Pin it to the top-left of
   the screen instead. Fixed (not absolute) because the nearest positioned
   ancestor is .login-split, which is only as tall as its content. The safe-area
   inset covers iOS notches; on Android the native shell already pads the
   WebView by the system-bar insets, so env() there is 0 and this is a no-op. */
@media (max-width: 768px) {
  #loginScreen.dhlg-form .dhlg-back {
    position: fixed;
    top: calc(14px + env(safe-area-inset-top, 0px));
    left: 14px;
    z-index: 5;
    margin: 0;
  }
}

.dhlg-badge {
  width: 44px; height: 44px; border-radius: 14px;
  background: var(--dh-gold, #C9962A); color: #fff;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 6px 18px rgba(201,150,42,0.32);
  margin-bottom: 20px;
}
.dhlg-welcome {
  font-family: var(--dh-serif);
  font-size: 19px; font-weight: 700; letter-spacing: -0.01em;
  color: var(--dh-ink, #0A0A0A);
  margin: 0 0 5px;
}
.dhlg-hello {
  font-size: 13.5px; color: var(--dh-muted, #8A8A8A); margin: 0 0 22px;
}

/* The OTP field's label has nowhere to float to (the code boxes are the field
   and carry their own heading), so it stays visually hidden — still announced
   by screen readers, just not painted. */
#loginPanel #otpGroup > label[for="otpCode"] {
  position: absolute !important;
  width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Identity + password use the shared .dh-float floating-label component (see
   the FLOATING LABELS block further down — same behaviour as the quick-signup
   and modal fields, so there's one implementation, not two). The component
   doesn't assume a leading icon, so the only overrides here are the 32px shift
   that clears it, plus the filled/bordered look. The border is what stops the
   field disappearing into the cream page background now that the card fill is
   gone below 768px. */
#loginPanel .login-input-icon {
  display: flex; left: 16px;
  color: var(--dh-faint2, #AAAAAA);
  transition: color .16s ease;
}
#loginPanel .dh-float:focus-within .login-input-icon { color: var(--dh-gold, #C9962A); }
#loginPanel .dh-float { --dh-float-h: 60px; }
#loginPanel .dh-float > label { left: 43px; max-width: calc(100% - 62px); }
#loginPanel .login-input-wrap input {
  box-sizing: border-box;
  height: 60px;
  background: #fff;
  border: 1.5px solid rgba(10,10,10,0.16);
  border-radius: 14px;
  /* No top/bottom padding: the floated caption sits ON the border rather than
     inside the box, so the value gets the full height and browsers centre a
     single-line input's text in it. */
  padding: 0 14px 0 47px;
}
#loginPanel .login-input-wrap input:focus {
  border-color: var(--dh-gold, #C9962A);
  box-shadow: 0 0 0 4px rgba(201,150,42,0.14);
}

/* Notched (outlined) float: instead of lifting inside the box, the caption
   straddles the top border and interrupts it.
   · height:auto so the box hugs the text — the base rule makes the label
     full-field-height for the resting centre, which would put a chip-sized
     background behind the whole field.
   · translateY(-50%) of that hugged height centres it exactly on the border.
   · The chip is transparent on top and white from just above the border down,
     so it masks the border line without painting a white block over the page
     behind it. Solid white would show as a rectangle wherever the page's
     cream→white gradient hasn't reached white yet. */
#loginPanel .dh-float > input:focus ~ label,
#loginPanel .dh-float > input:not(:placeholder-shown) ~ label {
  height: auto;
  top: 0;
  left: 38px;
  transform: translateY(-50%);
  padding: 0 6px;
  background: linear-gradient(180deg, #fdfaf2 0%, #ffffff 55%);
}
/* Inline validation message, directly under the field it's about. */
#loginPanel .login-error {
  margin: 0 0 14px;
  padding: 11px 14px;
  border-radius: 12px;
  font-size: 13px;
  line-height: 1.45;
}
#loginPanel #loginSubmitBtn {
  padding: 16px 0; font-size: 15px; border-radius: 14px; margin-top: 4px;
}
#loginPanel #loginSubmitBtn::after { content: '  \2192'; }

/* "OR" rule + alternate sign-in. #loginAltRow's display is driven by
   _dhlgSyncBio() (inline none until the device is enrolled). */
.dhlg-or {
  display: flex; align-items: center; gap: 12px;
  margin: 20px 0 14px;
  font-size: 11.5px; font-weight: 600; letter-spacing: 0.08em;
  color: var(--dh-faint, #C4C4C4);
}
.dhlg-or::before, .dhlg-or::after {
  content: ''; flex: 1 1 0; height: 1px; background: var(--dh-border, rgba(0,0,0,0.08));
}
.dhlg-altbtn {
  width: 100%; display: flex; align-items: center; justify-content: center; gap: 9px;
  padding: 15px 0; border-radius: 14px; cursor: pointer;
  background: #fff; border: 1.5px solid var(--dh-border, rgba(0,0,0,0.08));
  color: var(--dh-ink, #0A0A0A); font-family: inherit;
  font-size: 14.5px; font-weight: 600;
}
.dhlg-altbtn:hover { border-color: var(--dh-gold-border, rgba(201,150,42,0.25)); }

.dhlg-footlink {
  text-align: center; margin-top: 20px;
  font-size: 13.5px; color: var(--dh-muted, #8A8A8A);
}
.dhlg-footlink a {
  color: var(--gold-dark, #8A6305); font-weight: 700; text-decoration: none;
}

/* ── OTP step ─────────────────────────────────────────────────────────────
   Centred heading, no badge, no identity field, no alternate sign-in — just
   the code boxes and the CTA. */
#loginPanel.dhlg-otp .dhlg-badge,
#loginPanel.dhlg-otp #identityGroup,
#loginPanel.dhlg-otp #loginAltRow,
#loginPanel.dhlg-otp #loginSignupFooter { display: none !important; }
#loginPanel.dhlg-otp .dhlg-welcome {
  font-size: 25px; text-align: center; margin-top: 30px;
}
#loginPanel.dhlg-otp .dhlg-hello { text-align: center; margin-bottom: 26px; }
#loginPanel.dhlg-otp #loginSubmitBtn { margin-top: 26px; }

/* Six boxes because our OTP is six digits (server-generated; the DLT template
   and the delivery fallbacks are all 6). They are DISPLAY ONLY — #otpCode is
   overlaid transparently on top and remains the single real input. */
#otpGroup { position: relative; }
/* The overlaid OTP field has no icon slot — the boxes are the field. */
#otpGroup .login-input-icon { display: none; }
.dhlg-otpboxes { display: flex; gap: 8px; }
.dhlg-otpbox {
  box-sizing: border-box;
  flex: 1 1 0; min-width: 0; height: 58px; border-radius: 16px;
  background: #fff;
  border: 1.5px solid rgba(10,10,10,0.16);
  display: flex; align-items: center; justify-content: center;
  font-size: 21px; font-weight: 700; color: var(--dh-ink, #0A0A0A);
  font-variant-numeric: tabular-nums lining-nums;
}
.dhlg-otpbox.is-next {
  background: #fff;
  border-color: var(--dh-gold, #C9962A);
  box-shadow: 0 0 0 4px rgba(201,150,42,0.14);
}
#otpGroup .login-input-wrap { position: absolute; top: 0; left: 0; right: 0; height: 58px; }
#otpGroup .login-input-wrap input {
  height: 58px; padding: 0; text-align: center;
  background: transparent !important; border-color: transparent !important;
  box-shadow: none !important;
  color: transparent; caret-color: transparent;
}
#otpGroup .login-input-wrap input::placeholder { color: transparent; }
/* Fallback: the value isn't a plain 6-digit code (the internal support
   override is a password), so the boxes can't represent it — show an ordinary
   visible field rather than hiding what is being typed. */
#otpGroup.dhlg-otp-raw .dhlg-otpboxes { display: none; }
#otpGroup.dhlg-otp-raw .login-input-wrap { position: static; height: auto; }
#otpGroup.dhlg-otp-raw .login-input-wrap input {
  height: auto; padding: 16px; text-align: left;
  background: #fff !important;
  border-color: rgba(10,10,10,0.16) !important;
  color: var(--dh-ink, #0A0A0A); caret-color: var(--dh-ink, #0A0A0A);
}
#otpGroup.dhlg-otp-raw .login-input-wrap input::placeholder { color: var(--dh-faint, #C4C4C4); }

/* Mobile/tablet: no floating card. The showcase panel is already hidden below
   768px, so the "card" was a white rounded box with a drop shadow wrapping the
   full-height form — drop the shadow, the radius and the white fill so the form
   sits directly on the page's cream→white gradient. Desktop (>=769px), where
   the split card genuinely IS a card next to the showcase, is untouched. */
@media (max-width: 768px) {
  #loginScreen .login-split {
    box-shadow: none;
    border-radius: 0;
    background: transparent;
  }
  #loginScreen .login-form-panel {
    background: transparent;
    border-radius: 0;
  }
}

/* Narrow phones: six 58px boxes + gaps don't fit under ~340px of content
   width, so tighten both rather than let them overflow the screen. */
@media (max-width: 380px) {
  .dhlg-otpboxes { gap: 6px; }
  .dhlg-otpbox { height: 52px; border-radius: 13px; font-size: 19px; }
  #otpGroup .login-input-wrap,
  #otpGroup .login-input-wrap input { height: 52px; }
  #loginPanel.dhlg-otp .dhlg-welcome { font-size: 22px; }
}

/* ---------------------------------------------------------------------------
 * FLOW CHROME — apply/signup wizard shell (Dhanam theme). The wizard opens for
 * logged-in customers; the .dh-wizmodal class is added to its .modal.
 * ------------------------------------------------------------------------- */
.dh-wizmodal { border-radius: 20px; }
.dh-wizmodal .modal-header h2 {
  font-family: var(--dh-serif);
  font-weight: 700; color: var(--dh-ink); font-size: 18px; letter-spacing: -0.01em;
}
/* Generic step bodies: dh-ify the form fields across EVERY wizard step at once
   (identity / address / nominee / kyc inputs / review …) without touching each
   renderer. Custom inline-styled boxes (product banner, KYC consent boxes) get
   their own per-renderer passes. */
.dh-wizmodal #wizContent h3 {
  font-family: var(--dh-serif); font-weight: 700; color: var(--dh-ink);
  font-size: 16px; margin: 0 0 14px; letter-spacing: -0.01em;
}
.dh-wizmodal .form-group label { color: var(--dh-soft); font-weight: 600; font-size: 13px; }
.dh-wizmodal #wizContent input,
.dh-wizmodal #wizContent select,
.dh-wizmodal #wizContent textarea {
  border: 1.5px solid var(--dh-border); border-radius: 13px; background: var(--dh-bg);
  color: var(--dh-ink); font-size: 16px; padding: 16px 14px 2px; box-sizing: border-box;
}
.dh-wizmodal #wizContent input:focus,
.dh-wizmodal #wizContent select:focus,
.dh-wizmodal #wizContent textarea:focus {
  outline: none; border-color: var(--dh-gold); box-shadow: 0 0 0 3px var(--dh-gold-dim);
}
/* generic in-body buttons (Verify Aadhaar etc.) → gold, unless already a dh-btn */
.dh-wizmodal #wizContent .btn-primary:not(.dh-btn) {
  background: var(--dh-gold); color: var(--dh-ink); border: none; border-radius: 13px;
  font-weight: 700; box-shadow: 0 4px 14px rgba(201,150,42,.28);
}

/* Offline-payment + similar dynamically-built flow modals (cheque / cash …).
   Tag the modal's inner box with .dh-flowmodal; this dh-ifies its inline-styled
   inputs + buttons via !important (the file inputs keep a dashed upload look,
   and the load-bearing capture="environment" attribute is untouched). */
.dh-flowmodal h2,
.dh-flowmodal h3 { font-family: var(--dh-serif); font-weight: 700; color: var(--dh-ink); letter-spacing: -0.01em; }
/* Card + chrome polish so a legacy-markup .modal tagged .dh-flowmodal reads as
   the customer theme (rounded card, gold header rule, dh-radius footer). */
.dh-flowmodal { border-radius: 18px 18px 0px 0px !important; }
.dh-flowmodal .modal-header { border-bottom: 1px solid var(--dh-border) !important; }
.dh-flowmodal .modal-footer { border-top: 1px solid var(--dh-border) !important; }
.dh-flowmodal input:not([type=file]),
.dh-flowmodal select,
.dh-flowmodal textarea {
  border: 1.5px solid var(--dh-border) !important; border-radius: 13px !important;
  background: var(--dh-bg) !important; color: var(--dh-ink) !important;
}
.dh-flowmodal input:not([type=file]):focus,
.dh-flowmodal select:focus,
.dh-flowmodal textarea:focus {
  outline: none !important; border-color: var(--dh-gold) !important;
  box-shadow: 0 0 0 3px var(--dh-gold-dim) !important;
}
.dh-flowmodal input[type=file] {
  border: 1.5px dashed var(--dh-border) !important; border-radius: 12px !important;
  background: var(--dh-surface) !important;
}
.dh-flowmodal .btn-primary {
  background: var(--dh-gold); color: var(--dh-ink); border: none; border-radius: 13px;
  font-weight: 700; box-shadow: 0 4px 14px rgba(201,150,42,.28);
}
.dh-flowmodal .btn-secondary { border-radius: 13px; }

/* ---------------------------------------------------------------------------
 * (c) SCOPED BASE — customer experience only.
 * Conservative: white surface + Playfair on the existing heading hooks so the
 * foundation reads as an upgrade even before screens are individually rebuilt.
 * ------------------------------------------------------------------------- */
body.dh-customer #appContainer { background: var(--dh-bg); }

body.dh-customer .welcome-title {
  font-family: var(--dh-serif);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--dh-ink);
}
body.dh-customer .welcome-sub { color: var(--dh-muted); }

/* Section titles inside customer screens pick up the serif display face. */
body.dh-customer .cust-screen .section-title,
body.dh-customer [id^="cust"] .section-title {
  font-family: var(--dh-serif);
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* Gold becomes the single accent on customer screens. */
body.dh-customer { --accent: var(--dh-gold); }

/* ===========================================================================
 * (b) COMPONENT LIBRARY — .dh-* classes (namespaced; inert until used).
 * These are the building blocks the per-screen PRs assemble into the Figma
 * layouts. Ported verbatim from the approved _skin_poc.
 * =========================================================================*/

/* — typography helpers — */
.dh-serif { font-family: var(--dh-serif); }
.dh-row   { display: flex; align-items: center; }
.dh-between { justify-content: space-between; }

/* — buttons — */
.dh-btn {
  width: 100%; padding: 16px 0; border-radius: 8px; border: none;
  font-size: 15px; font-weight: 700; font-family: inherit;
  display: flex; align-items: center; justify-content: center; gap: 8px; cursor: pointer;
}
.dh-btn-gold    { background: var(--dh-gold); color: var(--dh-ink); box-shadow: 0 4px 16px rgba(201,150,42,.30); }
.dh-btn-outline { background: var(--dh-bg); border: 1.5px solid var(--dh-border); color: var(--dh-ink); font-weight: 600; }
.dh-btn-dark    { background: var(--dh-ink); color: #fff; }
/* Disabled state — was invisible (button looked clickable but wasn't),
   which made the PL apply screen feel broken ("Confirm & continue does
   nothing" — Suren report 2026-06-21). Now the customer sees the dim
   state immediately. */
.dh-btn:disabled,
.dh-btn[disabled] { opacity: .45; box-shadow: none; cursor: not-allowed; filter: grayscale(.25); }

/* — cards — */
.dh-card     { background: var(--dh-bg); border: 1.5px solid var(--dh-border2); border-radius: 14px; box-shadow: 0 5px 8px rgb(0 0 0 / 8%); }
.dh-darkcard { background: var(--dh-cardgrad); border-radius: 12px; padding: 24px 22px; color: #fff; }
.dh-darkcard .dh-cap { font-size: 13px; color: var(--dh-oncard-muted); }
.dh-darkcard .dh-big { font-family: var(--dh-serif); font-size: 34px; font-weight: 700; color: #fff; margin: 4px 0; }
/* ON-CARD TEXT TOKENS, RE-POINTED INSIDE THE CARD (2026-08-08).
 *
 * This is the load-bearing half of the gold recolour. Roughly twenty INLINE
 * styles in index.html paint text inside these cards with var(--dh-faint),
 * var(--dh-muted) or var(--dh-gold-light) — values chosen when the card was
 * near-black. On gold they break, two different ways:
 *
 *   --dh-faint      #C4C4C4  → ~1.4:1 on the ramp's lower stop. Near-invisible.
 *   --dh-gold-light #E8C96A  → gold on gold, ~1.4:1. The exact failure the
 *                              portfolio card was audited to remove.
 *
 * An inline `color` beats any class rule, so those could not be fixed by adding
 * selectors — only by editing twenty call sites, or by redefining the tokens
 * for the subtree. The second is what custom properties are for: re-point them
 * on the card and every inline var() inside resolves to the on-gold value, with
 * no JS change and nothing to keep in sync.
 *
 * Scoped to the card, so the same tokens keep their normal values everywhere
 * else on the page. Only text tokens are re-pointed: --dh-gold-dim2 and friends
 * are decorative fills and stay as they are.
 *
 * Cream rather than pure white for the two muted roles, so the type hierarchy
 * (heading vs sub) survives without dropping into the alpha values that caused
 * the original problem. */
.dh-darkcard,
.dh-card-dark {
  --dh-faint:      rgba(255,255,255,0.88);
  --dh-muted:      rgba(255,255,255,0.82);
  --dh-muted2:     rgba(255,255,255,0.82);
  --dh-gold-light: #FFF3D6;
}
/* Sub-line under a hero headline. Exists because the colour it needs cannot be
 * written inline — an inline `color` would outrank the token re-pointing above
 * and pin the near-black-era grey onto gold. Any new hero sub should use this
 * class and set no colour of its own.
 *
 * (The short-lived .dh-darkcard-gold modifier is gone: gold is the default for
 * every card now, so it had nothing left to switch.) */
.dh-darkcard-sub { color: var(--dh-faint); }

/* — portfolio hero card (Home) — */
/* Gold card, deepened so its text passes (2026-08-08). The lighter ramp it
 * replaces — rgb(178,123,7) → rgb(187,132,15)/53% — could not carry any of the
 * text on it: white was 3.67:1 (13px stats need 4.5), and the gold accents the
 * stats were tinted with came out at 1.38:1 and 2.27:1, i.e. gold on gold.
 * The 53% alpha made the lower half worse again by letting the white sheet
 * through. Now opaque, and white measures 5.70:1 at the top stop rising to
 * 9.1:1 at the bottom. Stat colours are set below, in CSS, not per-stat in JS. */
.dh-pcard  { background: var(--dh-goldcard); border-radius: 12px; padding: 22px 20px 18px; }
.dh-pcap   { color: #ffffff; font-size: 10px; letter-spacing: 1px; text-transform: uppercase; }
.dh-pval   { color: #fff; font-size: 34px; font-weight: 700; letter-spacing: -1.5px; line-height: 1.1; margin: 4px 0 8px; }
/* Pill: tinted DOWN, not up. A near-white 14% wash lightened it to within a
 * shade of the card, leaving its gold-light text at 2.68:1 — an 11px label
 * needs 4.5. Darkening gives the pill its own ground, and #F5E6BE (a lighter
 * gold, so the accent survives) reads 6.54:1 on it. */
.dh-ppill  { display: inline-flex; align-items: center; gap: 4px; background: rgba(0,0,0,0.22); border: 1px solid rgba(232,201,106,0.40); border-radius: 20px; padding: 3px 10px; font-size: 11px; color: #F5E6BE; font-weight: 600; }
.dh-pstats { display: flex; border-top: 1px solid rgba(255,255,255,.07); padding-top: 14px; margin-top: 16px; }
.dh-pstat  { flex: 1; }
.dh-pstat + .dh-pstat { padding-left: 14px; border-left: 1px solid rgba(255,255,255,.07); margin-left: 14px; }
.dh-pstat .l { color: #ffffff; font-size: 9px; text-transform: uppercase; letter-spacing: .5px; }
/* Colour lives HERE, not in the per-stat JS argument. It has to sit next to
 * the card background it is read against — that is the pairing that decides
 * legibility, and splitting the two across files is what let gold-on-gold ship. */
.dh-pstat .v { font-size: 13px; font-weight: 600; margin-top: 2px; color: #fff; }

/* — loan-card stats on a LIGHT card —
   .dh-pstats was built for the dark portfolio card: its label colour
   (--dh-oncard-muted, a near-white) and hairline borders are white-on-dark, and
   it lays six items out as equal flex columns. Reused on the white My Loans card
   that gave ~40px per column with 9px near-white uppercase labels — illegible,
   and the values wrapped mid-date. This is the light-card equivalent: a grid
   that wraps honestly instead of crushing, with ink-on-white contrast. */
.dh-lstats { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px 16px; margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--dh-border, #eee); }
@media (min-width: 400px) { .dh-lstats { grid-template-columns: repeat(3, minmax(0,1fr)); } }
.dh-lstat .l { color: var(--dh-muted, #6b7280); font-size: 10px; text-transform: uppercase; letter-spacing: .6px; line-height: 1.3; }
.dh-lstat .v { font-size: 15px; font-weight: 700; margin-top: 3px; color: var(--dh-ink, #0A0A0A); line-height: 1.25; }
.dh-lstat .v.is-bad { color: #b00020; }

/* Document links on the loan card: chips, not default-browser blue underlines. */
.dh-docrow { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--dh-border, #eee); }
.dh-docchip { display: inline-flex; align-items: center; gap: 6px; padding: 7px 11px; border: 1px solid var(--dh-border2, #e3e3e6); border-radius: 999px; background: var(--dh-surface, #fff); color: var(--dh-ink, #0A0A0A); font-size: 12px; font-weight: 600; text-decoration: none; cursor: pointer; line-height: 1; }
.dh-docchip:active { transform: scale(.97); }
.dh-docchip svg { flex: none; opacity: .75; }

/* Generic swipe row: horizontal scrolling with the scrollbar hidden, same
   convention as the two named carousels below. `scrollbar-width` covers
   Firefox and the WebKit pseudo-element covers Chrome/Safari + both WebViews;
   Android WebView paints an overlay bar during a fling without it. Scrolling
   itself is untouched — this only hides the indicator.

   NOTE for callers: overflow-y is hidden (a tall slide must not become its own
   vertical scroller), and a box-shadow contributes NOTHING to scrollable
   overflow — so if the slides are shadowed, give the row a padding-bottom
   large enough for the shadow or it gets sliced off at the card's edge. */
.dh-hscroll {
  display: flex;
  gap: 10px;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.dh-hscroll::-webkit-scrollbar { display: none; width: 0; height: 0; }

/* — portfolio carousel (Home): 4 swipeable dark cards, one card per view — */
.dh-pcarousel { display: flex; gap: 12px; overflow-x: auto; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; scrollbar-width: none; margin: 6px 0 10px; }
.dh-pcarousel::-webkit-scrollbar { display: none; }
.dh-pcard-slide { flex: 0 0 85%; min-width: 80%; box-sizing: border-box; scroll-snap-align: center; cursor: pointer; }
.dh-pcard-slide:active { transform: scale(0.992); }
/* …but NOT when the press is on the eye toggle (2026-08-08, Prem: "clicking the
 * eye makes the whole card feel like a button").
 *
 * :active matches ANCESTORS of the pressed element, so pressing the eye put the
 * entire card into its pressed state and it visibly shrank — reading as "you
 * tapped the card" when you tapped a 30px control inside it. The JS was already
 * correct (the eye's handler calls event.stopPropagation(), so navigation never
 * fired); this was purely the visual feedback lying about what was hit.
 *
 * :has() beats the rule above on specificity (0,0,3,0 vs 0,0,2,0) — it takes the
 * specificity of its argument — so no !important is needed. Where :has() is
 * unsupported the card simply keeps shrinking as it does today: the press still
 * does the right thing, it just looks the way it used to. */
.dh-pcard-slide:has(.dh-eye:active) { transform: none; }
.dh-pcard-soon { opacity: .6; cursor: default; }
/* Same reason as the pill: gold-light sat at 3.53:1 directly on the card. */
.dh-pcard-cta { display: inline-flex; align-items: center; gap: 4px; margin-top: 12px; font-size: 13px; font-weight: 600; color: #F5E6BE; }
.dh-pdots { display: flex; gap: 6px; justify-content: center; }
.dh-pdots i { width: 6px; height: 6px; border-radius: 50%; background: var(--dh-faint); transition: all .2s; }
.dh-pdots i.dh-active { width: 18px; border-radius: 3px; background: var(--dh-gold); }

/* Announcement slides — side-scroll carousel (root/HO published). */
/* align-items: flex-start so each slide sizes to its OWN content. A two-line
   announcement renders in a small box; a long one grows. JS resizes the
   carousel container to the active slide's height on swipe so the page
   doesn't leave whitespace below short slides. */
/* overflow-y: hidden is LOAD-BEARING — CSS spec auto-derives `overflow-y: auto`
   when overflow-x is set to a scrolling value. Without the explicit hidden, a
   tall slide produces a vertical scroll INSIDE the carousel (customers can
   scroll within a single announcement card, which they shouldn't). */
.dh-anncarousel { display: flex; align-items: flex-start; gap: 12px; overflow-x: auto; overflow-y: hidden; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; scrollbar-width: none; margin: 6px 0 10px; transition: height .22s ease; }
.dh-anncarousel::-webkit-scrollbar { display: none; }
.dh-annslide { flex: 0 0 100%; min-width: 100%; box-sizing: border-box; scroll-snap-align: center; align-self: flex-start;
  background: var(--dh-card, #fff); border: 1px solid var(--dh-border, rgba(0,0,0,.08)); border-radius: 18px; overflow: hidden;
  box-shadow: 0 1px 3px rgba(0,0,0,.05);
  display: flex; flex-direction: column; min-height: 120px; position: relative; }
.dh-annmedia { width: 100%; aspect-ratio: 16 / 9; background: var(--dh-surface, #F3EFE6); overflow: hidden; flex: 0 0 auto; }
.dh-anntext { padding: 14px 16px 16px; flex: 1 1 auto; min-height: 0; overflow: hidden; }

/* Optional "highlight" mode — admin toggles per announcement. Pulsing gold
   border + warm gradient background + lifted shadow. Animation defaults to
   ~2s pulse; respects prefers-reduced-motion so a tired phone or screen-
   reader user gets the bright look without the motion. */
.dh-annslide--highlight {
  background: linear-gradient(135deg, #FFF8E7 0%, #FFEFC2 100%);
  border-color: var(--dh-gold, #C9962A);
  box-shadow: 0 4px 16px rgba(201, 150, 42, 0.25);
  animation: dh-annPulse 2.4s ease-in-out infinite;
}
.dh-annslide--highlight .dh-anntitle { color: #6B4A0E; }
.dh-annslide--highlight .dh-annbody  { color: #6B4A0E; opacity: 0.85; }
.dh-annslide--highlight::before {
  content: ""; position: absolute; inset: -2px; border-radius: 18px; pointer-events: none;
  border: 2px solid rgba(201, 150, 42, 0.45);
  animation: dh-annPulseBorder 2.4s ease-in-out infinite;
}
@keyframes dh-annPulse {
  0%, 100% { box-shadow: 0 4px 16px rgba(201, 150, 42, 0.25); }
  50%      { box-shadow: 0 6px 22px rgba(201, 150, 42, 0.55); }
}
@keyframes dh-annPulseBorder {
  0%, 100% { opacity: 0.45; transform: scale(1); }
  50%      { opacity: 0.85; transform: scale(1.005); }
}
@media (prefers-reduced-motion: reduce) {
  .dh-annslide--highlight,
  .dh-annslide--highlight::before { animation: none; }
}
/* Text-only slide: centre the text inside the box; padding scales with content. */
.dh-annslide--textonly .dh-anntext {
  display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 22px;
}
/* Rich-text body — let the announcement's own inline styling (font-size /
   font-family / bold / italic / underline) render. white-space: pre-line
   keeps legacy plain-text bodies wrapping at their \n. */
.dh-annbody b, .dh-annbody strong { font-weight: 700; }
.dh-annbody i, .dh-annbody em { font-style: italic; }
.dh-annbody u { text-decoration: underline; }
.dh-annbody span { display: inline; }
.dh-annslide--textonly .dh-anntag, .dh-annslide--textonly .dh-anntitle,
.dh-annslide--textonly .dh-annbody, .dh-annslide--textonly .dh-annlink { text-align: center; }
.dh-anntag { display: inline-block; font-size: 10px; font-weight: 700; letter-spacing: .06em; margin-bottom: 6px; }
.dh-anntitle { font-family: var(--dh-serif, 'Poppins', serif); font-weight: 700; font-size: 17px; line-height: 1.25; color: var(--dh-ink, #0A0A0A); letter-spacing: -.01em; }
.dh-annbody { font-size: 13px; color: var(--dh-muted2, #6B6B6B); line-height: 1.55; margin-top: 6px; white-space: pre-line; }
.dh-annlink { display: inline-flex; align-items: center; gap: 4px; margin-top: 12px; font-size: 13px; font-weight: 700; color: var(--dh-gold, #C9962A); text-decoration: none; }

/* Flashing-text palettes — deep base pulses to a bright glow and back, ~2s
   per cycle. Used as <span class="ann-flash ann-flash-<palette>"> on the
   accent words inside an announcement body. Defined here for the customer
   carousel; the staff editor injects identical keyframes into its modal so
   the preview matches. `prefers-reduced-motion` users see a static colour. */
.ann-flash { animation: ann-flash-cycle 2s ease-in-out infinite; font-weight: 700; }
.ann-flash-gold  { --c1:#A07816; --c2:#FFD96B; color: var(--c1); }
.ann-flash-ocean { --c1:#0E7490; --c2:#5EE9F4; color: var(--c1); }
.ann-flash-rose  { --c1:#8E2745; --c2:#FF8AAB; color: var(--c1); }
.ann-flash-mint  { --c1:#1F6E54; --c2:#7DF1C2; color: var(--c1); }
.ann-flash-royal { --c1:#3F35A8; --c2:#B5A8FF; color: var(--c1); }
@keyframes ann-flash-cycle {
    0%, 100% { color: var(--c1); text-shadow: none; }
    50%      { color: var(--c2); text-shadow: 0 0 8px var(--c2); }
}
@media (prefers-reduced-motion: reduce) {
    .ann-flash { animation: none; }
}

/* "Chat now" floating button — customer only, anchored above the tab bar.
   The right/bottom here are only the pre-JS resting place: _applyFabCorner
   writes inline anchors for whichever corner the customer parked the group in.
   They mirror slot 1 of the arc (90° at R 68 — straight up from the toggle) so
   the stylesheet alone never disagrees with the JS. */
#custChatFab { display: none; }
body.dh-customer #custChatFab,
body.dh-prospect #custChatFab {
  position: fixed; right: var(--dh-fab-gutter, 16px); z-index: 1200;
  bottom: calc(68px + env(safe-area-inset-bottom, 0px) + 16px + 68px);
  display: inline-flex; align-items: center; justify-content: center;
  width: 48px; height: 48px; padding: 0;
  border: none; border-radius: 50%; background: var(--dh-gold, #C9962A); color: var(--dh-ink, #0A0A0A);
  cursor: grab; touch-action: none;
  box-shadow: 0 8px 22px rgba(201,150,42,.40), 0 2px 6px rgba(0,0,0,.14);
  transition: transform .12s ease, box-shadow .12s ease,
              left .18s ease, right .18s ease, top .18s ease, bottom .18s ease;
}
body.dh-customer #custChatFab:active,
body.dh-prospect #custChatFab:active { transform: scale(.96); }
body.dh-customer #custChatFab.dh-fab-dragging,
body.dh-prospect #custChatFab.dh-fab-dragging {
  cursor: grabbing; transform: scale(1.08);
  box-shadow: 0 14px 32px rgba(201,150,42,.50), 0 4px 10px rgba(0,0,0,.20);
  transition: none;
}
body.dh-customer #custChatFab:disabled,
body.dh-prospect #custChatFab:disabled { opacity: .6; cursor: default; }
#custChatFab .dh-fabicon { display: inline-flex; }
#custChatFab .dh-fablabel,
#custCallFab .dh-fablabel { display: none; }

/* — portfolio card: gold sparkline + eye (hide/show values) — */
.dh-pcard { position: relative; }
.dh-spark { margin: 14px 0 2px; height: 40px; }
.dh-eye { position: absolute; top: 18px; right: 18px; width: 30px; height: 30px; border-radius: 9px; background: rgba(255,255,255,.06); border: none; display: flex; align-items: center; justify-content: center; color: #ffffff; cursor: pointer; z-index: 2; padding: 0; }
/* The eye owns its own press feedback. With the card's shrink suppressed above,
 * this is the ONLY thing that confirms the tap registered — so it scales too,
 * not just a background wash. `touch-action: manipulation` drops the 300ms
 * double-tap delay so the toggle responds immediately; the eye is a fast, often
 * repeated tap (hide, glance, show). */
.dh-eye { touch-action: manipulation; transition: background .12s ease, transform .12s ease; }
.dh-eye:active { background: rgba(255,255,255,.20); transform: scale(0.90); }
/* Reduced motion keeps the confirmation, drops the movement — the background
 * wash alone still says "pressed", which is the part that carries meaning. */
@media (prefers-reduced-motion: reduce) {
  .dh-eye { transition: none; }
  .dh-eye:active { transform: none; }
}
/* Values hidden → an x-mask, not a blur (2026-08-08, Prem). A blur still leaks
 * the shape and rough magnitude of a number — a ₹3,00,000 blurs wider than a
 * ₹500. A fixed mask reveals nothing, not even the digit count.
 *
 * The real text stays in the DOM and is only made transparent, so each element
 * keeps its box and nothing reflows when toggling. `!important` on the colour
 * is required because _pcard writes the stat colour as an INLINE style, which
 * would otherwise win over any class rule.
 *
 * The mask is an absolutely-positioned ::after so it overlays the now-invisible
 * value rather than adding to it. Note this hides the value visually, not from
 * the DOM — same threat model as the blur it replaces: shoulder-surfing, not
 * a security control.
 *
 * Scoped to `.dh-pcard.dh-vhide` — the mask belongs to ONE card. Each card
 * carries its own eye and its own state, so revealing the Investments card
 * leaves Lockers, Gold and Loans masked. (Was `body.dh-values-hidden`, which
 * flipped the whole carousel from any single eye.) Cards start masked; the
 * customer opens the ones they want to see. */
.dh-pcard.dh-vhide .dh-pval,
.dh-pcard.dh-vhide .dh-pstat .v,
.dh-pcard.dh-vhide .dh-ppill {
  position: relative;
  color: transparent !important;
  user-select: none;
  pointer-events: none;
}
.dh-pcard.dh-vhide .dh-pval::after,
.dh-pcard.dh-vhide .dh-pstat .v::after,
.dh-pcard.dh-vhide .dh-ppill::after {
  content: 'xxxx';
  position: absolute; top: 0; bottom: 0;
  display: flex; align-items: center;
  letter-spacing: .08em;
  white-space: nowrap;
}
.dh-pcard.dh-vhide .dh-pval::after     { content: 'xxxx xxxx'; left: 0; color: #fff; }
.dh-pcard.dh-vhide .dh-pstat .v::after { left: 0; color: rgba(255,255,255,.88); }
/* Inset to the pill's own 10px side padding so the mask sits inside it, and
 * centred — the pill keeps the width of the sentence it is hiding
 * ("+₹18,450 interest earned"), so a left-aligned mask would sit in a wide
 * pill with dead space trailing it. */
.dh-pcard.dh-vhide .dh-ppill::after    { left: 10px; right: 10px; justify-content: center; color: var(--dh-gold-light); }

/* — product hub: clean 2-up "open a new product" cards (de-hardcoded) — */
.dh-prodhubcard { padding: 16px; cursor: default; display: flex; flex-direction: column; background: rgba(201,150,42,0.05); border-color: var(--dh-gold-border); }
.dh-prodhubcard-soon { opacity: .62; cursor: default; }
/* Top row of each product card: icon on the left, an optional badge ("Open
   now") pinned to the top-right and vertically aligned with the icon. */
.dh-prodhub-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: calc(12px * var(--dh-card-pad-scale)); }
/* Title on a SINGLE line so all four cards stay equal height and the tag line
   ("Up to 13% p.a." vs "Vault-grade · insured") lines up across cards. Sized to
   fit the longest title ("Safe Deposit Locker") on a phone; ellipsis is a
   defensive guard for any hypothetical longer product name. */
.dh-prodhub-title { display: block; font-size: 13px; font-weight: 600; color: var(--dh-ink); line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dh-prodhub-tag { font-size: 11px; color: var(--dh-muted); line-height: 1.45; }
.dh-prodhub-cta { display: inline-flex; align-items: center; gap: 4px; margin-top: 12px; font-size: 13px; font-weight: 700; color: var(--dh-gold); cursor: pointer; }
.dh-prodhub-more { margin-top: 8px; align-self: flex-start; background: none; border: none; padding: 0; font-family: inherit; font-size: 12px; font-weight: 600; color: var(--dh-muted); cursor: pointer; text-decoration: underline; }

/* — service tile grid (Home) — */
.dh-tiles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.dh-tile  { background: var(--dh-bg); border-radius: 18px; padding: 14px 8px; border: 1.5px solid var(--dh-border2); box-shadow: 0 2px 8px rgba(0,0,0,.04); display: flex; flex-direction: column; align-items: center; gap: 9px; text-align: center; }
.dh-tile.dh-dis { opacity: .5; }
/* Chip background — a white base with a warm glow bleeding in from the
 * bottom-right corner (2026-08-10, Prem, ref card), replacing the flat 10% gold
 * wash. The flat tint made every chip a uniform beige square; the gradient
 * gives it a light direction, so a grid of chips has depth instead of reading
 * as a row of identical swatches.
 *
 * Layer order matters: the gradient is listed FIRST so it paints ON TOP of the
 * white beneath it. Reversed, the opaque white would hide the glow entirely.
 *
 * The final stop is `rgba(255,255,255,0)`, not `transparent` — in some engines
 * `transparent` interpolates through transparent-BLACK, which greys the middle
 * of the ramp instead of fading cleanly to the white base.
 *
 * The gold border stays: with a near-white fill the chip needs an edge to read
 * as a chip on the white sheet it sits on. */
.dh-tchip {
  width: 42px; height: 42px; border-radius: 14px;
  background:
    radial-gradient(130% 130% at 100% 100%,
      rgba(201, 150, 42, .26) 0%,
      rgba(201, 150, 42, .10) 42%,
      rgba(255, 255, 255, 0) 78%),
    var(--dh-bg, #fff);
  border: 1px solid rgba(201, 150, 42, .18);
  box-shadow: 0 2px 6px rgba(201, 150, 42, .10);
  display: flex; align-items: center; justify-content: center;
  color: #634404; font-size: 19px;
}
/* Disabled resets the gradient to a flat grey — the shorthand `background`
 * replaces the whole layer stack, which is what we want here. box-shadow has to
 * be cleared separately though: it is a different property, so a "coming soon"
 * chip would otherwise keep the gold glow the live ones have. */
.dh-tile.dh-dis .dh-tchip { background: var(--dh-surface); border-color: var(--dh-border); color: var(--dh-faint); box-shadow: none; }
.dh-tlabel { font-size: 12px; font-weight: 600; color: var(--dh-ink); line-height: 1.2; }
.dh-tile.dh-dis .dh-tlabel { color: var(--dh-faint); }
.dh-tsub  { font-size: 10px; color: var(--dh-muted); margin-top: 2px; }

/* — product tile, v2 (2026-08-06) — the Home/Invest catalog tile.
 *
 * `.dh-tile-prod` is the transparent variant: the SECTION card is the box, so
 * each product is just chip + label (no per-product border). It replaces a pile
 * of inline styles that used to be built in _custProdTile.
 *
 * Two things were wrong with v1 and are fixed here:
 *  1. Rows went ragged — "Safe Deposit Locker" wraps to two lines while "NCD"
 *     does not, so tiles in a row ended up different heights. The label now
 *     RESERVES two lines (min-height + clamp), so a row lines up whatever the
 *     copy does.
 *  2. Coming-soon tiles were dimmed with a blanket opacity:.5, which dragged
 *     the grey "Soon" pill down to roughly 2:1 against the card — illegible.
 *     Dimming is now done with explicit muted tokens per element, so the pill
 *     keeps its contrast while the tile still reads as inactive.
 *
 * No description line under the label: icon + name only, by product decision.
 */
/* FLAT TILES (2026-08-08, Prem: "remove the card, the box-shadow and the
 * border — make it simple").
 *
 * No box at any level. The section wrapper is not a card either (see
 * _custCatSection), so a product is just its icon chip + label sitting on the
 * white sheet, and the only structure on the screen is the section heading and
 * the grid rhythm itself.
 *
 * The chip keeps its own gold-tinted fill — that is the tile's colour and its
 * tap target; removing that too would leave nothing to aim at. Press feedback
 * is therefore the gold wash + scale, since there is no shadow left to lift.
 */
.dh-tiles { gap: 10px; }
.dh-tile-prod {
  position: relative; cursor: pointer; font-family: inherit;
  background: none; border: none; box-shadow: none;
  padding: 24px 6px 12px; gap: 7px; border-radius: 16px;
  -webkit-tap-highlight-color: transparent;
  transition: background .15s ease, transform .12s ease;
}
.dh-tile-prod:active { transform: scale(.97); background: rgba(201,150,42,.07); }
/* No badge (status 'plain') → reclaim the space the badge would have used. */
.dh-tile-prod.dh-tile-nobadge { padding-top: 12px; }
/* Scoped to .dh-tile-prod: .dh-tchip is shared with the testimonial avatars. */
.dh-tile-prod .dh-tchip { position: relative; width: 48px; height: 48px; border-radius: 15px; }

/* LIVE product — a gold ring around the icon chip that fades IN, drifts UP and
 * fades out, on a loop. It marks the products the customer can open right now.
 *
 * A ::before ring rather than an animated box-shadow: box-shadow spread cannot
 * be moved independently of the chip, and animating it would repaint the chip
 * itself every frame. The ring is inset by -4px so its line sits entirely
 * OUTSIDE the chip's edge — it therefore needs no z-index, even though a
 * pseudo-element paints above its host's background.
 *
 * Radius is the chip's 15px + the 4px offset, so the ring stays concentric.
 * The static box-shadow halo that used to live here is gone: two rings on one
 * chip just read as mud. */
.dh-tile-prod.dh-tile-live .dh-tchip::before {
  content: ''; position: absolute; inset: -4px;
  border-radius: 19px; border: 1.5px solid rgba(201,150,42,.55);
  pointer-events: none; opacity: 0;
  animation: dh-chipring 2.8s ease-in-out infinite;
}
/* Spread only — the ring stays centred on the icon and grows outward. No
 * translate: it must not drift upward off the chip. */
@keyframes dh-chipring {
  0%   { opacity: 0;   transform: scale(.94); }
  35%  { opacity: .90; transform: scale(1);   }
  70%  { opacity: .35; transform: scale(1.06); }
  100% { opacity: 0;   transform: scale(1.10); }
}
/* The chip's top edge butts right up against the status pill, and the ring is
 * inset 4px and grows past that again, so it still reaches into the pill's box.
 * Both are positioned with z-index:auto and the chip comes LATER in the DOM, so
 * by default the ring would paint a gold line straight across the pill. Lifting
 * the pill one layer makes the ring pass cleanly BEHIND it instead. */
.dh-tile-prod .dh-tbadge { z-index: 1; }

/* Decorative only — the motion carries no information, so it simply goes. */
@media (prefers-reduced-motion: reduce) {
  .dh-tile-prod.dh-tile-live .dh-tchip::before { animation: none; opacity: 0; }
}
.dh-tile-prod .dh-tlabel {
  font-size: 12.5px; line-height: 1.25; min-height: 2.5em;
  display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2;
  -webkit-box-orient: vertical; overflow: hidden;display: flex;align-items: center;
}

/* Status pill. Centred over the chip — it reads as a ribbon on the tile. */
.dh-tbadge {
  position: absolute; top: 6px; left: 50%; transform: translateX(-50%);
  white-space: nowrap; font-size: 9px; font-weight: 400; letter-spacing: .4px;
  text-transform: uppercase; padding: 3px 9px 2px; border-radius: 999px;
  line-height: 1.4;
}
.dh-tbadge-live   { background-image: linear-gradient(310deg, #976805, #e9b348); color: #fff; box-shadow: 0 2px 6px rgba(201,150,42,.30); }
.dh-tbadge-branch { background: var(--dh-gold-dim); color: var(--dh-gold); border: 1px solid var(--dh-gold-border); }
.dh-tbadge-soon   { background: var(--dh-surface); color: var(--dh-muted); border: 1px solid var(--dh-border2); }

/* Inactive: per-element muting instead of a blanket opacity — see (3). */
.dh-tile-prod.dh-dis { opacity: 1; }
.dh-tile-prod.dh-dis .dh-tchip { background: var(--dh-surface); border-color: var(--dh-border); color: var(--dh-muted); box-shadow: none; }
.dh-tile-prod.dh-dis .dh-tlabel { color: var(--dh-muted); }

/* — product ROW list (2026-08-07) — the "Invest & Grow" layout.
 *
 * An alternative to the 3-up .dh-tiles grid, opted into per catalog category
 * via `layout:'rows'`. The grid is icon + name only; a row has space for a
 * one-line description, which is what an investment product needs — "Digital
 * Gold" alone doesn't tell a first-time customer what they'd be buying.
 *
 * Structural difference from the grid: with rows there is NO section card.
 * Each product is its own white card and the header sits on the page
 * background — a card-inside-a-card reads as a mistake at this width.
 *
 * The chip is a CIRCLE here (the grid uses a squircle) so the two layouts stay
 * visually distinct when both appear on Home.
 */
.dh-prows { display: flex; flex-direction: column; gap: 10px; }
.dh-prow {
  display: flex; align-items: center; gap: 13px; width: 100%;
  background: var(--dh-bg); border: 1.5px solid var(--dh-border2);
  border-radius: 16px; padding: 14px 14px; text-align: left;
  font-family: inherit; cursor: pointer;
  box-shadow: 0 2px 8px rgba(0,0,0,.04);
  -webkit-tap-highlight-color: transparent;
  transition: background .15s ease, transform .12s ease, box-shadow .15s ease;
}
.dh-prow:active { transform: scale(.985); background: rgba(201,150,42,.05); }
.dh-prow-chip {
  position: relative; flex: 0 0 auto; width: 44px; height: 44px;
  border-radius: 50%; background: var(--dh-gold-dim);
  border: 1px solid var(--dh-gold-border); color: var(--dh-gold);
  display: flex; align-items: center; justify-content: center;
}
/* min-width:0 is what lets the description's ellipsis work: without it a flex
 * item refuses to shrink below its content's intrinsic width, so the text
 * would push the chevron off the card instead of truncating. */
.dh-prow-body { display: block; flex: 1 1 auto; min-width: 0; }
.dh-prow-top  { display: flex; align-items: center; gap: 7px; }
.dh-prow-name {
  font-size: 14.5px; font-weight: 700; color: var(--dh-ink); line-height: 1.25;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dh-prow-desc {
  display: block;
  margin-top: 3px; font-size: 12.5px; color: var(--dh-muted); line-height: 1.35;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The status pill: same palette as .dh-tbadge-*, but the grid's version is
 * absolutely positioned over the chip, so a row needs its own inline box. */
.dh-prow-badge {
  flex: 0 0 auto; white-space: nowrap; font-size: 9px; font-weight: 400;
  letter-spacing: .4px; text-transform: uppercase;
  padding: 3px 8px 2px; border-radius: 999px; line-height: 1.4;
}
.dh-prow-chev { flex: 0 0 auto; color: var(--dh-faint); display: flex; }

/* ═══ "Open a new product" hub — ONE grouped module ═══════════════════════
 *
 * A single card with hairline-divided rows, so the block has one silhouette
 * instead of five separate outlined boxes reading as a settings menu. Visual
 * weight goes into the leading chip and the value line.
 *
 * Three things carry the "finance app" feel, each doing a job:
 *   · a GRADIENT chip (--dh-goldcard) rather than a flat tint — the only
 *     saturated element in the row, so it anchors the eye;
 *   · a VALUE line in gold under the product name, where the live NCD coupon
 *     lives — the number is the reason to tap;
 *   · a circular chevron, so the row reads as openable at a glance.
 * ------------------------------------------------------------------------- */
.dh-nphead { margin: 2px 4px 10px; }
.dh-nphead-t {
  font-family: var(--dh-serif); font-size: 16px; font-weight: 600;
  color: var(--dh-ink); line-height: 1.2;
}
.dh-nphead-s { font-size: 12.5px; color: var(--dh-muted); margin-top: 2px; }
/* One block of vertical rhythm for a titled section. Customer pages that stack
 * "heading + content" use this instead of a per-section inline margin — the
 * Invest page had 22px on two of its three sections and none on the third,
 * which is the drift a shared class prevents. */
.dh-sec { margin-bottom: 22px; }
.dh-sec:last-child { margin-bottom: 0; }
.dh-nphub {
  background: var(--dh-bg);
  border: 1.5px solid var(--dh-border2);
  border-radius: 20px;
  /* Clips the child rows' corners AND the footer's cream fill to the radius. */
  overflow: hidden;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .05);
}
.dh-nprow {
  display: flex; align-items: center; gap: 13px; width: 100%;
  padding: 15px 16px; text-align: left;
  background: none; border: none; font-family: inherit;
  cursor: pointer; -webkit-tap-highlight-color: transparent;
  transition: background .15s ease;
}
/* Hairline BETWEEN rows only — no border on the first, and none doubled against
 * the card edge, which is what a per-row border would produce. */
.dh-nprow + .dh-nprow { border-top: 1px solid var(--dh-border2); }
.dh-nprow:active { background: rgba(201, 150, 42, .06); }
.dh-npchip {
  flex: 0 0 auto; width: 46px; height: 46px; border-radius: 16px;
  background: var(--dh-goldcard); color: #fff;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 3px 10px rgba(201, 150, 42, .28);
}
.dh-npbody { display: block; flex: 1 1 auto; min-width: 0; }
.dh-nptop  { display: flex; align-items: center; gap: 7px; }
.dh-npname {
  font-size: 15px; font-weight: 700; color: var(--dh-ink); line-height: 1.25;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The value line. Gold + bold when it carries a real figure (the live NCD
 * coupon), muted when it is only a descriptor — same slot either way, so every
 * row is exactly three lines tall and the list stays even. */
.dh-npval {
  display: block; margin-top: 3px; font-size: 13px; line-height: 1.35;
  font-weight: 700; color: var(--gold-dark, #8A6305);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dh-npval.dh-npval-soft { font-weight: 500; color: var(--dh-muted); font-size: 12.5px; }
.dh-npmore {
  display: inline-block; margin-top: 6px;
  background: none; border: none; padding: 0; font-family: inherit;
  font-size: 11.5px; font-weight: 600; color: var(--dh-muted);
  cursor: pointer; text-decoration: underline; text-underline-offset: 2px;
  -webkit-tap-highlight-color: transparent;
}
.dh-npmore:active { color: var(--dh-gold); }
/* Pressing "Know more" must not light up the whole row — :active matches
 * ancestors. Same technique as .dh-pcard-slide:has(.dh-eye:active). */
.dh-nprow:has(.dh-npmore:active) { background: none; }
.dh-npgo {
  flex: 0 0 auto; width: 30px; height: 30px; border-radius: 50%;
  background: var(--dh-gold-dim); color: var(--dh-gold);
  display: flex; align-items: center; justify-content: center;
}
/* Inactive product (Loans before LOANS_ENABLED): muted per element rather than
 * a blanket opacity, so the "Soon" pill keeps its own contrast. No chevron —
 * an arrow promises somewhere to go. The row stays tappable for "Know more". */
.dh-nprow.dh-dis { cursor: default; }
.dh-nprow.dh-dis .dh-npchip { background: var(--dh-surface); color: var(--dh-faint); box-shadow: none; }
.dh-nprow.dh-dis .dh-npname { color: var(--dh-muted); }
.dh-nprow.dh-dis:active { background: none; }
/* Branch visit closes the module on the warm band the app's sheets use, so it
 * belongs to the card but is plainly not a product. */
.dh-npfoot { background: var(--dh-sheet-hdr, #FBF7EF); }
.dh-npfoot .dh-npchip {
  background: var(--dh-gold-dim); color: var(--dh-gold);
  border: 1px solid var(--dh-gold-border); box-shadow: none;
}

/* Inactive: muted per element (not a blanket opacity) so the grey pill keeps
 * its contrast — same reasoning as the grid tile above. No chevron either:
 * there is nothing to open yet, and an arrow promises there is. The row stays
 * TAPPABLE though — it opens the notify-me sheet, same as the grid tile. */
.dh-prow.dh-dis { box-shadow: none; background: var(--dh-surface); }
.dh-prow.dh-dis:active { transform: scale(.985); background: rgba(0,0,0,.03); }
.dh-prow.dh-dis .dh-prow-chip { background: var(--dh-bg); border-color: var(--dh-border); color: var(--dh-faint); }
/* .dh-tbadge-soon is grey-on-grey and the inactive CARD is grey too, so the
 * pill would vanish into it. Put it back on white here. */
.dh-prow.dh-dis .dh-prow-badge { background: var(--dh-bg); }
.dh-prow.dh-dis .dh-prow-name { color: var(--dh-muted); }
.dh-prow.dh-dis .dh-prow-desc { color: var(--dh-faint); }

/* — pills / badges — */
.dh-pill      { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 600; color: var(--dh-muted); background: var(--dh-bg); border: 1px solid var(--dh-border); border-radius: 20px; padding: 3px 9px; }
.dh-pill-gold { color: var(--dh-gold); background: var(--dh-gold-dim); border-color: var(--dh-gold-border); }
.dh-pill-ok   { color: var(--dh-success); background: var(--dh-success-dim); border-color: rgba(26,127,75,.2); }
.dh-pill-warn { color: var(--dh-warn); background: var(--dh-warn-dim); border-color: rgba(192,92,0,.2); }
.dh-badge     { font-size: 9px; font-weight: 700; color: var(--dh-ink); background: var(--dh-gold); border-radius: 20px; padding: 2px 8px; letter-spacing: .3px; }

/* — list row (activity / NCD holdings) — */
.dh-arow  { display: flex; align-items: center; gap: 12px; background: var(--dh-bg); border-radius: 16px; padding: 13px 14px; border: 1.5px solid var(--dh-border2); box-shadow: 0 1px 4px rgba(0,0,0,.04); text-align: left; }
.dh-arow.dh-due { background: var(--dh-error-dim); border-color: rgba(217,64,64,.2); }
.dh-achip { width: 38px; height: 38px; border-radius: 12px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 17px; }
.dh-listcard { width: 100%; text-align: left; background: var(--dh-surface2); border: 1.5px solid rgba(0,0,0,.09); border-radius: 16px; padding: 18px; }

/* — alert (due banner) — */
.dh-alert { padding: 14px 16px; border-radius: 16px; background: var(--dh-error-dim); border: 1.5px solid rgba(217,64,64,.25); display: flex; align-items: center; gap: 12px; text-align: left; }
.dh-alert .t { font-size: 13px; font-weight: 700; color: var(--dh-error); }
.dh-alert .s { font-size: 12px; color: #A02020; margin-top: 1px; }

/* — screen header (back · title · action) — */
.dh-shead { display: flex; align-items: center; gap: 6px; padding: 14px 16px; border-bottom: 1px solid var(--dh-border); }
.dh-shead .dh-back { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; color: var(--dh-muted); font-size: 20px; }
.dh-shead .dh-title { font-size: 16px; font-weight: 600; color: var(--dh-ink); }
.dh-shead .dh-sp { flex: 1; }

/* — numpad + PIN boxes (OTP / MPIN) — */
.dh-numpad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.dh-key { height: 54px; border-radius: 14px; background: var(--dh-bg); border: 1.5px solid var(--dh-border); font-size: 20px; font-weight: 500; color: var(--dh-ink); display: flex; align-items: center; justify-content: center; font-family: inherit; box-shadow: 0 1px 3px rgba(0,0,0,.05); }
.dh-key.dh-del { background: var(--dh-surface); }
.dh-pinbox { width: 46px; height: 54px; border-radius: 14px; background: var(--dh-surface); border: 1.5px solid var(--dh-border); display: flex; align-items: center; justify-content: center; }
.dh-pinbox.dh-on { background: var(--dh-ink); border-color: var(--dh-gold); box-shadow: 0 0 0 3px var(--dh-gold-dim); }
.dh-pinbox .dh-fill { width: 9px; height: 9px; border-radius: 50%; background: var(--dh-gold); }

/* — product cards (Products hub) — */
.dh-prodcard { background: var(--dh-bg); border-radius: 20px; overflow: hidden; border: 1.5px solid var(--dh-border2); box-shadow: 0 1px 4px rgba(0,0,0,.05); }
.dh-prodcard.dh-open { border-color: var(--dh-gold); box-shadow: 0 4px 20px rgba(201,150,42,.12); }
.dh-prodicon { width: 46px; height: 46px; border-radius: 15px; background: var(--dh-gold-dim); border: 1px solid rgba(201,150,42,.18); display: flex; align-items: center; justify-content: center; color: var(--dh-gold); font-size: 20px; flex-shrink: 0; }
.dh-statsrow { display: flex; border-top: 1px solid var(--dh-border2); }
.dh-statcell { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 10px 4px; }
.dh-statcell + .dh-statcell { border-left: 1px solid var(--dh-border2); }
.dh-statcell .l { font-size: 9px; color: var(--dh-faint); text-transform: uppercase; letter-spacing: .4px; }
.dh-statcell .v { font-size: 12px; font-weight: 700; color: var(--dh-gold); margin-top: 2px; }

/* — payment chooser tiles (checkout) — */
.dh-paytile { width: 100%; text-align: left; display: flex; align-items: center; gap: 13px; padding: 15px 16px; border-radius: 16px; border: 1.5px solid var(--dh-border); background: var(--dh-bg); }
.dh-paytile.dh-primary { border-color: var(--dh-gold); background: var(--dh-gold-dim); }
.dh-ptchip { width: 42px; height: 42px; border-radius: 13px; display: flex; align-items: center; justify-content: center; font-size: 19px; flex-shrink: 0; }

/* Illustration slot in styledConfirm (opts.art) — e.g. the "coming soon"
   sheet. Capped rather than full-bleed so a square illustration doesn't push
   the buttons below the fold on a short phone; the block removes itself if the
   asset 404s (see the img's onerror). */
/* Sits after the body copy, which already carries a 20px bottom margin — so no
   top margin here, only the gap down to the action buttons. */
.dh-cfart { margin: 0 0 18px; text-align: center; }

/* The box RESERVES the illustration's space while it downloads.
 *
 * Without it the dialog opened with the art collapsed to nothing (an <img> with
 * height:auto is zero-tall until its file arrives), then snapped taller and
 * pushed the buttons down the moment it did — which on a slow connection is a
 * second or two after the sheet is already on screen and being read.
 * coming-soon.gif is ~209KB, so this is the common case, not the edge one.
 *
 * The 1:1 ratio applies ONLY while loading. "is-ready" drops it so the loaded
 * image keeps its own proportions — the reserve is a floor for the wait, not a
 * crop imposed on the artwork. */
.dh-cfart-box { position: relative; width: 100%; max-width: 210px; margin: 0 auto; }
.dh-cfart:not(.is-ready) .dh-cfart-box { aspect-ratio: 1 / 1; }
.dh-cfart-skel { position: absolute; inset: 0; border-radius: 14px; }
.dh-cfart.is-ready .dh-cfart-skel { display: none; }

.dh-cfart img {
  display: block; margin: 0 auto;
  width: 100%; height: auto;
  /* Cross-fade in over the skeleton rather than cutting, so a fast (cached)
     load is not a flash of movement. */
  opacity: 0; transition: opacity .28s ease;
}
.dh-cfart.is-ready img { opacity: 1; }

@media (max-height: 700px) {
  .dh-cfart-box { max-width: 150px; }
}
@media (prefers-reduced-motion: reduce) {
  .dh-cfart img { transition: none; }
}

/* The topbar is already outside the scroller (.topbar is a sibling of
 * .content-area, so it never scrolled) — but it is a NON-POSITIONED block, and
 * a viewport-fixed backdrop like .dh-lnintro::before would paint over its
 * background. Giving it a paint layer keeps the header exactly as it is today:
 * position:relative with no offsets moves nothing, and no colour is changed. */
body.dh-customer .topbar { position: relative; z-index: 1; }

/* ---------------------------------------------------------------------------
 * PERSONAL LOAN — apply intro (_loanRenderApplyCta). Page-level title + sub,
 * then a white card carrying a floating icon badge, the benefit rows, a chip
 * row and the gold CTA.
 * ------------------------------------------------------------------------- */
/* The gradient is VIEWPORT-FIXED, not part of the scrolling flow — the card
 * slides over it and it never scrolls away. Three things make that work:
 *
 * 1. `z-index: 0` on the wrapper gives it a stacking context, so the backdrop
 *    below can use z-index:-1 and land BEHIND the card but still ABOVE
 *    `body.dh-customer #appContainer`'s opaque background. A plain negative
 *    z-index without this wrapper would be painted over by that background —
 *    block backgrounds paint after negative-z children in the same context.
 * 2. The backdrop is `position: fixed`, which is only viewport-anchored
 *    because no ancestor (.content-area / .main-content / #appContainer /
 *    body.dh-customer) sets transform, filter or will-change. Adding one to
 *    any of those later would make this scroll again.
 * 3. `body.dh-customer .topbar` is lifted to z-index 1 (further down) so the
 *    fixed backdrop cannot creep over the header.
 *
 * The wrapper still carries no background of its own, and because it lives
 * inside #custLoanContent every later step's innerHTML replaces it — the
 * backdrop leaves with the step, no cleanup code required. */
/* Fixed warm backdrop — put .dh-goldbg on a screen's TOP-LEVEL wrapper. The
 * gradient is anchored to the viewport, so cards scroll over it and it never
 * scrolls away. Three things make that work:
 *
 * 1. `z-index: 0` gives the wrapper a stacking context, so the backdrop's
 *    z-index:-1 lands BEHIND the screen's cards but still ABOVE
 *    `body.dh-customer #appContainer`'s opaque background. A bare negative
 *    z-index without this wrapper gets painted over by that background —
 *    block backgrounds paint after negative-z children in the same context.
 * 2. `position: fixed` is only viewport-anchored because no ancestor
 *    (.content-area / .main-content / #appContainer / body.dh-customer) sets
 *    transform, filter or will-change. Adding one to any of those would make
 *    this scroll with the content again.
 * 3. `body.dh-customer .topbar` is lifted to z-index 1 (above) so the fixed
 *    backdrop cannot creep over the header.
 *
 * Self-scoping: the wrapper lives inside the screen's content element, so any
 * re-render that replaces innerHTML takes the backdrop with it. */
.dh-goldbg {
  position: relative;
  z-index: 0;
}
.dh-goldbg::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(180deg,
              #FBF2E1 0%,
              #FDF8EF 46%,
              var(--dh-bg, #FFFFFF) 100%);
}

/* ===========================================================================
 * HOME — header band + rounded white sheet (2026-08-08, Prem — ref mock).
 *
 * The topbar's cream continues DOWN behind the greeting, and the rest of Home
 * sits in a white panel whose top corners are rounded and pulled up over that
 * band. Topbar + greeting therefore read as one header block, exactly as in
 * the reference.
 *
 * Bleeding to the edges means cancelling .content-area's padding, so these
 * mirror it. The values look scattered in styles.css but collapse to very few:
 *
 *   sides  >768px : 32px
 *   sides ≤768px : max(env(safe-area-inset-left/right), 8px) — an @supports
 *                   block LATER in styles.css overrides the 16/12/8px from the
 *                   768/480/360 breakpoints, so all phones land on this one
 *                   expression. It is copied verbatim rather than flattened to
 *                   8px, otherwise a notched device in landscape would push
 *                   content under the notch.
 *   top           : 32 / 20 / 14 / 10 at >768 / 768 / 480 / 360. Home zeroes
 *                   .content-area's padding-top and the band re-applies this
 *                   value itself (see below), so the gutter above the greeting
 *                   is unchanged from before the band existed.
 *
 * NOT `margin-inline: calc(50% - 50vw)` (the usual full-bleed trick): from
 * 720px up, styles.css caps .content-area at a 520px CENTRED column, so 100vw
 * is no longer the box to fill — measured at a 1184px viewport it overflowed
 * the scroller by 352px. Cancelling the padding is right in both layouts: it
 * spans the phone's full width AND exactly the 520px column on a tablet.
 *
 * KEEP IN SYNC with `.content-area` in styles.css (Emergent-owned).
 * ======================================================================== */
body.dh-customer { --dh-capad-l: 32px; --dh-capad-r: 32px; --dh-capad-t: 32px; --dh-sheet-r: 28px; }
@media (max-width: 768px) {
  body.dh-customer {
    --dh-capad-l: max(env(safe-area-inset-left, 0px), 8px);
    --dh-capad-r: max(env(safe-area-inset-right, 0px), 8px);
    --dh-capad-t: 20px;
  }
}
@media (max-width: 480px) { body.dh-customer { --dh-capad-t: 14px; } }
@media (max-width: 360px) { body.dh-customer { --dh-capad-t: 10px; } }

/* HOW THE EDGE-TO-EDGE IS DONE: .content-area's own padding is removed on
 * Home, and the band + sheet re-apply it as their own. No negative margins.
 *
 * Negative margins were the obvious approach and are WRONG here.
 * .content-area is a scroll container, and anything pulled past its content
 * origin — above it, or to the left of it — lands in the "before start"
 * overflow region, which browsers CLIP and cannot scroll to. That silently ate
 * a strip of the band's background: `--dh-capad-t` worth under the topbar, and
 * `--dh-capad-l` worth down the left edge.
 *
 * Two things made it hard to see. getBoundingClientRect() reported a perfect
 * full-bleed box throughout, because it returns the UNCLIPPED layout box — so
 * every measurement passed while the pixels were wrong. And the RIGHT edge
 * genuinely was fine, since overflow past the END edge paints normally, which
 * made a systematic bug look like a left-only quirk.
 *
 * Home is `body.dh-customer:not(.dh-away)` — .dh-away is already maintained by
 * _custTrackNav for every customer navigation, so this needs no new JS and
 * cannot get out of step with the section that is actually showing. Everything
 * Home renders lives inside the band or the sheet, so nothing is left without
 * a gutter when the container's padding goes.
 *
 * Fallback 0px on every var(): they are set on body.dh-customer, so outside a
 * customer session the declaration would otherwise be invalid and drop. */
body.dh-customer:not(.dh-away) .content-area {
  padding-top: 0;
  padding-left: 0;
  padding-right: 0;
}
/* The band+sheet format is no longer Home-only — the Personal Loan intro uses
 * it too. Rather than add a second section name to the rule above (and a third
 * next time), this keys off the thing that actually needs the padding gone: an
 * ACTIVE screen containing a band. `.screen.active` matters — every screen's
 * markup stays in the DOM after navigation, so a bare :has(.dh-homeband) would
 * match from any page once Home had rendered once.
 *
 * Kept as its OWN rule, not merged into the selector list above: a selector
 * list containing an unsupported :has() is invalid in full, which would take
 * Home down with it on an old engine. Split, the worst case is that this rule
 * alone is ignored and a banded screen renders inset instead of edge-to-edge —
 * plainer, never broken. */
body.dh-customer .content-area:has(.screen.active .dh-homeband) {
  padding-top: 0;
  padding-left: 0;
  padding-right: 0;
}
.dh-homeband, .dh-homesheet {
  padding-left:   12px;
  padding-right: 12px;
}
/* The band is PINNED and the sheet scrolls over it (2026-08-08, Prem).
 *
 * `sticky`, not `fixed`: fixed would take the band out of flow, so the sheet
 * would jump up under the topbar and the band would need its height hardcoded
 * somewhere to compensate. Sticky keeps it in flow — it simply stops moving at
 * top:0 of the scrollport — so the layout is identical at rest and the only
 * change is what happens once you scroll.
 *
 * top:0 is the top of .content-area's scrollport, which on Home sits directly
 * under the topbar (its padding-top is removed above), so the band pins flush
 * to the header with no gap to tune.
 *
 * Paint order needs no z-index: the sheet is positioned too and comes LATER in
 * the DOM, so among positioned siblings with z-index:auto it already paints on
 * top. That is what lets it slide up and cover the pinned band.
 */
/* Sticky needs a scrollable ancestor chain that actually scrolls, and one link
 * was quietly breaking it: dhanam-theme.css sets `.screen { overflow-x: hidden }`.
 * Per spec, `hidden` on one axis computes the other axis's `visible` to `auto`,
 * so every .screen is a scroll CONTAINER — and it is the nearest one to the
 * band. .screen has no bounded height so it never scrolls, which meant the band
 * pinned to a scrollport that never moved: sticky applied, and did nothing.
 * Measured as `band rect.top = -130` at scrollTop 200 — scrolling away freely.
 *
 * Restoring `overflow: visible` on the banded screens hands sticky back to
 * .content-area, the element that really scrolls. Nothing is lost: horizontal
 * overflow is still clipped by .content-area's own overflow-x:hidden one level
 * up. Targeted at the specific screens rather than .screen so no other screen's
 * clipping behaviour changes. (dhanam-theme.css is Emergent-owned — overridden
 * here, not edited, and dhanam-app.css loads last so this wins.)
 *
 * BOTH banded screens must be listed. #custLoanScreen was missing (2026-08-08):
 * _loanRenderApplyCta renders the same .dh-homeband + .dh-homesheet pair into
 * #custLoanContent, so it inherited the exact bug this rule exists to fix — its
 * band scrolled away with the sheet instead of pinning. Any NEW screen that
 * adopts the band+sheet format has to be added here too; the :has() rule below
 * covers it automatically on engines that support :has(), and this list is the
 * guarantee for the ones that don't. */
body.dh-customer #custHomeScreen,
body.dh-customer #custLoanScreen,
body.dh-customer #custInvestScreen,
body.dh-customer #custDashboardScreen,
body.dh-customer #custLockersScreen,
body.dh-customer #custProfileScreen { overflow: visible; }
/* Future-proofing for any screen that adopts the format without being listed
 * above. Its own rule, not merged into the list: an unsupported :has() makes a
 * whole selector list invalid, which would take the two known screens down with
 * it. Split, the worst case is this rule alone being ignored. */
body.dh-customer .screen:has(.dh-homeband) { overflow: visible; }
.dh-homeband {
  position: sticky;
  top: 0;
  padding-top: var(--dh-capad-t, 0px);
  /* Reserves the card's lift plus a fixed 20px gap, so the greeting can never
   * be covered however far --dh-hold-lift is pushed. See .dh-homehold. */
  padding-bottom: calc(var(--dh-hold-lift, 46px) + 20px);
  background: var(--dh-band, #f8f0e0);
  min-height: 16vh;
}
/* Flat variant — a band whose sheet does NOT straddle the boundary.
 *
 * The base band's bottom padding exists solely to reserve the space Home's
 * holdings carousel rises into (see .dh-homehold). A screen without that lift
 * — the Dashboard, whose first sheet child is a conditional banner — would
 * render 66px of empty cream instead, so it opts out and keeps only a normal
 * gutter under the greeting. The #f8f0e0, the sticky pin and the top padding
 * are inherited, so the two screens still read as the same header block.
 *
 * 34px, not 18: the sheet's -28px lift eats most of whatever is set here, so a
 * plain gutter value would leave the white lip almost touching the strapline.
 * 34 - 28 = 6px, plus the greeting block's own 18px bottom margin = ~24px of
 * clear cream under the text.
 *
 * min-height is CANCELLED (2026-08-08, Prem: "there's unwanted space at the
 * top"). The base band reserves 16vh because Home's holdings carousel rises
 * into it — real content occupies that height. Nothing rises into a flat band,
 * so on the Dashboard the two-line page head came to ~106px and 16vh (≈136px
 * on a 850px screen, more on a taller one) padded it out with 30-40px of empty
 * cream before the sheet even started. Hugging the content removes exactly that
 * dead strip and changes nothing else — the padding above still sets the
 * breathing room, it is just no longer stretched past it. */
.dh-homeband-flat { padding-bottom: 56px; min-height: 0; }

.dh-bandcta {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  /* .cust-dash-welcome already carries 18px below the strapline. */
  margin-top: 4px;
}
.dh-bandcta .dh-btn {
  width: 100%;
  padding: 15px 10px;
  border-radius: 14px;
  font-size: 14.5px;
  line-height: 1.2;
  gap: 7px;
}
.dh-bandcta .dh-btn-outline { border-color: var(--dh-gold-border, rgba(201,150,42,.28)); }
.dh-bandnote { margin-top: 10px; text-align: center; }
@media (max-width: 340px) {
  .dh-bandcta { grid-template-columns: 1fr; }
}
/* The sheet. -24px lifts it over the band; the matching top padding gives the
 * content back the space the lift took, so nothing shifts upward.
 *
 * No box-shadow: the rounded corners against the cream are the entire effect,
 * and a shadow on a full-bleed panel only ever shows at those two corners,
 * where it reads as a smudge.
 *
 * Everything below the sheet is already #ffffff (body.dh-customer
 * #appContainer), so the panel does not need to stretch to the bottom of the
 * scroller — it just draws the rounded lip and the page carries on in the
 * same colour. */
/* The lift and the radius are ONE number on purpose. The corner arc is
 * transparent outside the curve, so whatever sits behind the sheet's top
 * `radius` pixels shows through there. The lift is how much band is behind it.
 * Lift 24 with radius 28 left the last 4px of each arc backed by the page's
 * white instead of the cream — a small white nick at both top corners.
 * Tying them together means that can't drift back. */
.dh-homesheet {
  position: relative;
  margin-top: calc(-1 * var(--dh-sheet-r, 28px));
  background: #ffffff;
  border-radius: var(--dh-sheet-r, 28px) var(--dh-sheet-r, 28px) 0 0;
  /* Shorthand LAST would wipe the top value above and the var-driven sides
   * from the grouped rule, so the longhands come after it deliberately. */
  padding: 14px;
  padding-top: calc(var(--dh-sheet-r, 28px) + 2px);
  padding-left: 14px;
  padding-right: 14px;
}
/* Tighter top on a sheet that follows a FLAT band (2026-08-08, Prem — the gap
 * between the sheet's top edge and "Open a new product").
 *
 * The base 30px (radius + 2) is not a gutter, it is compensation: the sheet is
 * pulled up by -28px so its rounded lip overlaps the cream, and the padding
 * gives the content back the space that lift took. Home NEEDS it left alone —
 * .dh-homehold measures its -46px straddle from this content box, so trimming
 * the padding would drag the holdings carousel further into the band.
 *
 * A flat band has nothing straddling, so the compensation buys nothing there
 * and just prints ~30px of blank white under the lip. 16px keeps the first
 * element clear of the 28px corner arc — at y=16 the arc has come in to x≈4,
 * and the label starts at x≈12 — while removing half the dead height.
 *
 * Adjacent-sibling, so it can only ever apply where a flat band precedes it. */
.dh-homeband-flat + .dh-homesheet,
/* .dh-profband is the Profile header's band. It is not `-flat` (it has its own
   padding-bottom for the avatar) but it straddles nothing either, so its sheet
   needs the same trim — without it Profile kept the full 30px and showed the
   same gap this rule exists to remove. */
.dh-profband + .dh-homesheet { padding-top: 22px; }
/* …and the first section label in that sheet drops its own top margin, which
 * would otherwise stack on the padding to re-create the gap. */
.dh-homeband-flat + .dh-homesheet > .dh-prodhub-section:first-child,
.dh-homeband-flat + .dh-homesheet > .dh-stepcap:first-child,
.dh-homeband-flat + .dh-homesheet > .dh-nphead:first-child,
.dh-profband + .dh-homesheet > .dh-grphd:first-child,
.dh-profband + .dh-homesheet > .dh-nphead:first-child { margin-top: 0; }

/* Holdings carousel, lifted so the card STRADDLES the band/sheet boundary —
 * half on the cream, half on the white, as in the reference.
 *
 * This lift is safe where the earlier ones were not: it is mid-content, not at
 * .content-area's scroll origin, so nothing lands in the clipped "before
 * start" region. It rises into the band's own 34px bottom padding, which is
 * empty by design, so the greeting keeps its clearance.
 *
 * No z-index needed: the sheet is positioned and comes after the band in the
 * DOM, so it and its children already paint above the pinned band. */
/* --dh-hold-lift is the ONE number to tune. The band's bottom padding is
 * derived from it, so the cream always reserves exactly the space the card
 * rises into and the greeting keeps a constant 20px of clearance no matter
 * how far the card is lifted. Raising the lift alone used to eat the name.
 *
 * The previous version combined margin-top:-66px with top:-25px — 91px of
 * total travel. `top` on a relative box moves the paint without moving the
 * layout, so the band had no idea the card was up there, and the card covered
 * "Test". Layout-affecting margin only, now. */
.dh-homehold {
  position: relative;
  margin-top: calc(-1 * var(--dh-hold-lift, 46px));
  /* margin-bottom: 20px; */
  top: -24px;
}

.dh-lnintro {
  position: relative;
  z-index: 0;
  margin: -8px -8px 0;
  padding: 14px 10px 30px;
}

.dh-lnhero { padding: 6px 2px 0; }
.dh-lnhero h2 {
  font-family: var(--dh-serif);
  font-size: 30px; font-weight: 700; letter-spacing: -0.02em; line-height: 1.15;
  color: var(--dh-ink); margin: 0 0 10px;
}
.dh-lnhero p {
  font-size: 14.5px; line-height: 1.5; color: var(--dh-muted2, #6B6B6B);
  margin: 0; max-width: 34ch;
}

.dh-lncard {
  position: relative;
  background: #FFFFFF;
  /* border: 1.5px solid var(--dh-border2, rgba(0,0,0,0.07)); */
  /* Slightly deeper top corners — the sheet-over-gradient edge. */
  border-radius: 26px 26px 22px 22px;
  /* Top padding leaves room for the badge that overhangs the card edge. */
  padding: 44px 16px 22px;
  margin-top: 46px;
  /* Elevation: a soft lift above the card plus a wider ambient shadow below. */
  /* box-shadow: 0 -2px 14px rgba(120, 90, 25, .05),
              0 12px 30px rgba(60, 45, 12, .09); */
}
/* Inside the sheet the card no longer needs the 46px that separated it from the
 * hero — the sheet's own top padding does that now. Enough is kept for the
 * badge, which overhangs the card's top edge by half its 62px height. */
.dh-homesheet > .dh-lncard { margin-top: 36px; }

/* Badge straddles the card's top edge, so it has to be OPAQUE: the card's top
   border runs horizontally right through its middle, and a translucent fill
   (the --dh-gold-dim2 token is 12% gold) let that line show straight through.
   #F9F2E5 is that same tint pre-composited over white, so the colour is
   unchanged — it just no longer transmits what's behind it.
   No white ring either: it was there to break the card's border, but the badge's
   top half sits over the cream gradient, where a white ring reads as a halo.
   Being opaque already hides the border segment it covers. */
.dh-lnbadge {
  position: absolute; top: 0; left: 50%;
  transform: translate(-50%, -50%);
  width: 62px; height: 62px; border-radius: 20px;
  display: flex; align-items: center; justify-content: center;
  background: #F9F2E5;
  color: var(--dh-gold, #C9962A);
}

.dh-lnfeats { display: flex; flex-direction: column; gap: 18px; }
.dh-lnfeat  { display: flex; align-items: center; gap: 13px; }
.dh-lnck {
  flex: none; width: 26px; height: 26px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--dh-gold-dim2, rgba(201,150,42,0.12));
  color: var(--gold-dark, #8A6305);
  margin-top: 1px;
}
.dh-lnfeat-t { min-width: 0; }
.dh-lnfeat-t b {
  display: block; font-size: 15.5px; font-weight: 700;
  color: var(--dh-ink); line-height: 1.3;
}
.dh-lnfeat-t i {
  display: block; font-style: normal; margin-top: 3px;
  font-size: 13px; line-height: 1.45; color: var(--dh-muted, #8A8A8A);
}

.dh-lnrule { height: 1px; background: var(--dh-border, rgba(0,0,0,.08)); margin: 22px 0; }

/* Chips wrap 2-up then 1 — each grows to fill the row, matching the design. */
.dh-lnchips { display: flex; flex-wrap: wrap; gap: 10px; }
.dh-lnchips span {
  flex: 1 1 40%; min-width: 0; text-align: center;
  padding: 12px 10px; border-radius: 12px;
  background: var(--dh-gold-dim, rgba(201,150,42,0.10));
  border: 1px solid var(--dh-gold-border, rgba(201,150,42,0.25));
  font-size: 13px; font-weight: 600; color: var(--gold-dark, #8A6305);
}

.dh-lncta  { width: 100%; margin-top: 22px; font-size: 16px; padding: 16px; border-radius: 14px; }
.dh-lnfine { margin-top: 12px; text-align: center; }

@media (max-width: 380px) {
  .dh-lnhero h2 { font-size: 26px; }
  .dh-lnchips span { flex-basis: 100%; }
}

/* ---------------------------------------------------------------------------
 * MY LOCKERS — booking timeline. A gold rail down the left with one dot per
 * locker, then a card per booking: status pill + annual rent, locker number,
 * lease range, branch, primary action, secondary links.
 * ------------------------------------------------------------------------- */
.dh-lktl { position: relative; padding-left: 22px; }
.dh-lktl::before {
  content: ''; position: absolute; left: 4px; top: 18px; bottom: 10px;
  width: 2px; border-radius: 2px; background: var(--dh-gold-border, rgba(201,150,42,0.25));
}
.dh-lkitem { position: relative; margin-bottom: 14px; }
/* The dot sits ON the rail; the white ring punches it out of the line. */
.dh-lkitem::before {
  content: ''; position: absolute; left: -22px; top: 20px;
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--dh-gold, #C9962A); box-shadow: 0 0 0 3px var(--dh-bg, #fff);
}
.dh-lkitem.is-due::before  { background: var(--dh-warn, #C05C00); }
.dh-lkitem.is-past::before { background: var(--dh-faint, #C4C4C4); }

.dh-lkcard {
  background: var(--dh-bg, #fff);
  border: 1.5px solid rgba(10,10,10,0.12);
  border-radius: 16px;
  padding: 14px 16px;
}
.dh-lkitem.is-past .dh-lkcard { background: var(--dh-surface2, #F8F8F8); }

.dh-lktop { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.dh-lkpill {
  display: inline-flex; align-items: center; gap: 5px; flex: none;
  padding: 4px 10px; border-radius: 20px;
  background: var(--dh-gold-dim2, rgba(201,150,42,0.12));
  border: 1px solid var(--dh-gold-border, rgba(201,150,42,0.25));
  font-size: 11px; font-weight: 700; color: var(--gold-dark, #8A6305);
}
.dh-lkpill.is-due  { background: var(--dh-warn-dim, rgba(192,92,0,.08)); border-color: rgba(192,92,0,.28); color: var(--dh-warn, #C05C00); }
.dh-lkpill.is-done { background: var(--dh-surface, #F6F6F6); border-color: var(--dh-border, rgba(0,0,0,.08)); color: var(--dh-muted2, #6B6B6B); }

.dh-lkprice { text-align: right; flex: none; }
.dh-lkprice b    { display: block; font-size: 15px; font-weight: 700; color: var(--dh-ink); line-height: 1.2; }
.dh-lkprice span { display: block; font-size: 10.5px; color: var(--dh-muted); margin-top: 1px; }

.dh-lkname { margin-top: 11px; font-size: 15px; font-weight: 700; color: var(--dh-ink); }
.dh-lkname i { font-style: normal; color: var(--dh-gold, #C9962A); }

.dh-lkmeta { display: flex; align-items: center; gap: 7px; margin-top: 7px; font-size: 12.5px; color: var(--dh-muted); }
.dh-lkmeta svg { color: var(--dh-gold, #C9962A); flex: none; }
/* Long branch names truncate rather than push the card wide. */
.dh-lkmeta span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.dh-lkdiv { height: 1px; background: var(--dh-border, rgba(0,0,0,.08)); margin-top: 13px; }
.dh-lkact { margin-top: 13px; }
.dh-lkbtn {
  width: 100%; padding: 12px 14px; border-radius: 12px; cursor: pointer;
  background: var(--dh-gold-dim2, rgba(201,150,42,0.12));
  border: 1px solid var(--dh-gold-border, rgba(201,150,42,0.25));
  color: var(--gold-dark, #8A6305);
  font-family: inherit; font-size: 14px; font-weight: 700;
}
.dh-lkbtn:active { opacity: .6; }

.dh-lklinks { display: flex; flex-wrap: wrap; gap: 18px; margin-top: 12px; }
.dh-lklink {
  display: inline-flex; align-items: center; gap: 6px;
  background: none; border: none; padding: 0; cursor: pointer;
  font-family: inherit; font-size: 12.5px; font-weight: 600;
  color: var(--gold-dark, #8A6305);
}
.dh-lklink svg { flex: none; }
.dh-lklink:active { opacity: .6; }

.dh-lkhead {
  margin: 22px 0 10px; font-size: 11px; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--dh-muted);
}

/* — bottom tab nav (customer mobile shell) — */
.dh-bottomnav { display: flex; align-items: center; justify-content: space-around; height: 68px; background: var(--dh-bg); border-top: 1px solid var(--dh-border2); box-shadow: 0 -4px 16px rgba(0,0,0,.05); }
.dh-navbtn { display: flex; flex-direction: column; align-items: center; gap: 3px; padding: 6px 10px; position: relative; background: none; border: none; cursor: pointer; flex: 1; min-width: 0; }
.dh-navbtn .dh-nicon { width: 36px; height: 36px; border-radius: 12px; display: flex; align-items: center; justify-content: center; color: var(--dh-muted); transition: background .18s, color .18s; }
.dh-navbtn.dh-active .dh-nicon { background: var(--dh-ink); color: var(--dh-gold); }
.dh-navbtn .dh-nlabel { font-size: 10px; color: var(--dh-muted); white-space: nowrap; }
.dh-navbtn.dh-active .dh-nlabel { color: var(--dh-ink); font-weight: 700; }
.dh-navbtn .dh-navtab { position: absolute; top: 0; left: 50%; transform: translateX(-50%); width: 20px; height: 2px; border-radius: 2px; background: var(--dh-gold); }

/* ===========================================================================
 * CUSTOMER MOBILE SHELL — bottom nav replaces the sidebar (PR: customer shell)
 * Everything below only applies under body.dh-customer, so staff keep the
 * sidebar + hamburger untouched.
 * =========================================================================*/

/* Hide the sidebar + hamburger for customers — navigation is the bottom nav. */
body.dh-customer .sidebar { display: none !important; }
body.dh-customer .sidebar-overlay { display: none !important; }
body.dh-customer .hamburger-btn { display: none !important; }
/* main-content takes the full width once the sidebar is gone. */
body.dh-customer .main-content { margin-left: 0 !important; width: 100% !important; }
/* leave room for the fixed bottom nav so the last content isn't hidden. */
body.dh-customer .content-area { padding-bottom: calc(90px + env(safe-area-inset-bottom, 0px)); }

/* My Payments (re-skinned): the legacy desktop table is replaced by the dh
   stacked cards (#custPaymentsCards) — hide the table for customers. */
body.dh-customer #custPaymentsTableWrap { display: none !important; }

/* Fixed bottom nav (created once in JS, CSS-gated to customers).
 *
 * FLOATING PILL (2026-08-06) — the nav no longer spans edge-to-edge as a
 * docked bar. It is a detached, fully-rounded card that floats above the
 * content with a soft shadow, icons only (no labels), an active gold dot
 * under the current tab, and the middle tab raised into a gold circle that
 * breaks the pill's top edge. `overflow: visible` is load-bearing — the
 * raised centre button is drawn outside the pill's own box.
 */
#custBottomNav { display: none; }
body.dh-customer #custBottomNav {
  display: flex; align-items: center; justify-content: space-around;
  position: fixed; left: 0; right: 0; z-index: 1200;
  /* Full-bleed bar: anchored to the very bottom edge, with the home-indicator
     inset carried as PADDING rather than as an offset. Offsetting it (the old
     `bottom: env(...)`) floated the bar above the indicator and left a strip of
     scrolling content showing beneath it. Height absorbs the same inset so the
     row of icons still gets its full 62px — box-sizing is border-box globally,
     so without that the padding would eat into the tap targets.
     Net occupied height is unchanged, so .content-area's 96px clearance still
     holds. */
  bottom: 0;
  height: calc(62px + env(safe-area-inset-bottom, 0px));
  padding: 0 0 env(safe-area-inset-bottom, 0px);
  background: var(--dh-bg);
  /* Side borders would land on the screen edges now — top rule only. */
  border: none;
  border-top: 1px solid var(--dh-border2);
  border-top-right-radius: 20px;
  border-top-left-radius: 20px;
  box-shadow: 0 8px 28px rgba(10,10,10,.14), 0 2px 6px rgba(10,10,10,.06);
  overflow: visible;
  font-family: var(--font, 'Poppins', sans-serif);
}

/* — topbar: brand mark on Home, Back everywhere else —
 * .dh-away is set by _custTrackNav for any section that isn't Home. Both the
 * .dh-customer AND .dh-away scopes are required, so a staff session can never
 * inherit this even if the class lingers on <body>. */
#custTopBack { display: none; }
body.dh-customer.dh-away #custTopHome,
body.dh-customer.dh-away #topbarTitle { display: none; }
body.dh-customer.dh-away #custTopBack {
  display: inline-flex; align-items: center; gap: 3px;
  padding: 6px 10px 6px 2px; margin: 0;
  background: none; border: none; cursor: pointer;
  font-family: inherit; font-size: 15px; font-weight: 600;
  color: var(--dh-ink); line-height: 1;
  /* The label is the PAGE NAME now (switchSection writes it), not the fixed
     word "Back", so it can be as long as "My Bank Statements". min-width:0 lets
     the button shrink inside .topbar-left instead of shouldering the bell off
     the right edge; the span ellipses rather than wrapping the topbar to two
     lines. */
  min-width: 0; max-width: 100%;
}
body.dh-customer.dh-away #custTopBack > span {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
body.dh-customer.dh-away #custTopBack svg { color: var(--dh-gold); flex-shrink: 0;width: 24px;height: 24px; }
body.dh-customer.dh-away #custTopBack:active { opacity: .55; }

/* One Back per screen. Seven customer screens (Appointments, Payments,
 * Transfers, Support, Applications, Documents, Refer & Earn) carry their own
 * in-screen `.cust-backbtn` in the static markup — they predate the topbar
 * control above. Since #custTopBack shows on EVERY non-Home section, those
 * screens rendered two stacked "‹ Back" controls.
 *
 * The topbar one is also the better of the two: it walks the real breadcrumb
 * (_custNavStack), so it returns where the customer actually came from, while
 * .cust-backbtn's _custProfileBack() always jumps to My Profile — wrong when
 * the screen was reached from a dashboard banner or a push notification.
 *
 * Hidden rather than deleted, and gated on the SAME `.dh-away` condition that
 * reveals #custTopBack — so if the shell's control isn't showing for any
 * reason, the in-screen button is still there and still works. No markup
 * removed, no handler dropped.
 *
 * `!important` is REQUIRED, not defensive: each of those buttons carries
 * `display:flex` in its own inline `style` attribute, and an inline style beats
 * any class rule that isn't !important. Without it this declaration is inert. */
body.dh-customer.dh-away .cust-backbtn { display: none !important; }

/* Icon-only tabs inside the pill. */
body.dh-customer #custBottomNav .dh-navbtn {
  flex: 1; min-width: 0; height: 100%;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0; padding: 0; position: relative;
  background: none; border: none; cursor: pointer;
}
/* Tab names sit under the icons — EXCEPT on the active tab, which hides its own.
 * The active icon lifts 16px out of the bar into the raised gold circle, so its
 * name would either collide with that circle or sit orphaned under it; the
 * circle is already the selection affordance (see .dh-navtab below), which makes
 * the name redundant exactly where it would be in the way.
 *
 * Positioned ABSOLUTELY on purpose: it keeps the label out of the button's flex
 * flow, so every icon box stays precisely where it was. That is what preserves
 * the "nothing reflows as the selection moves" invariant the icon sizing above
 * depends on — in normal flow, a 54px icon plus a label would not fit the 62px
 * bar and would push the icons around as labels appeared and disappeared. */
body.dh-customer #custBottomNav .dh-nlabel {
  display: block;
  position: absolute; left: 0; right: 0; bottom: 12px;
  padding: 0 2px;
  font-size: 10px; line-height: 1.1; font-weight: 600;
  color: var(--dh-muted); text-align: center;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  pointer-events: none;               /* the button owns the tap, not the text */
  transition: opacity .18s ease;
}
/* Hidden via opacity rather than display, so it fades with the circle's lift
   instead of snapping, and never affects layout either way. */
body.dh-customer #custBottomNav .dh-navbtn.dh-active .dh-nlabel {
  opacity: 0; visibility: hidden;
}
/* THE RAISED CIRCLE FOLLOWS THE ACTIVE TAB (2026-08-06) — it is not pinned to
 * the middle tab. Every icon therefore sits in an identically-sized 54px round
 * box that is transparent when inactive; only `.dh-active` fills it gold and
 * lifts it above the pill's top edge. Because the box never changes size and
 * every tab keeps `flex: 1`, nothing reflows as the selection moves — the
 * circle just animates in place, and the other four icons never shift.
 * `box-sizing: border-box` is global, so 54px is the OUTER size including the
 * 3px ring; the columns are ~57px on a 360px phone, so it always fits. */
body.dh-customer #custBottomNav .dh-nicon {
  width: 54px; height: 54px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: transparent; color: var(--dh-muted);
  border: 6px solid transparent;
  transition: background .2s ease, color .2s ease, transform .2s ease,
              box-shadow .2s ease, border-color .2s ease;
}
body.dh-customer #custBottomNav .dh-navbtn.dh-active .dh-nicon {
  background: var(--dh-gold); color: #fff;
  border-color: var(--dh-bg);
  box-shadow: 0 6px 16px rgba(201,150,42,.42);
  transform: translateY(-16px);
}
/* Centre the icon+label PAIR, not the icon alone. The icon box is centred in the
 * bar, so once a label is pinned below it the pair reads bottom-heavy — a big
 * void above the icon and the text crowding the bottom edge. Lifting the icon
 * 7px balances the two.
 *
 * Scoped to :not(.dh-active) deliberately: the active tab has no label, and its
 * raised gold circle is positioned from the icon's own centre. Lifting the base
 * would drag the circle up with it and change an already-settled look. Keeping
 * the lift off the active tab leaves that geometry byte-for-byte identical, and
 * the transform transition makes the 7px→16px hand-off animate as one motion. */
body.dh-customer #custBottomNav .dh-navbtn:not(.dh-active) .dh-nicon {
  transform: translateY(-7px);
}
body.dh-customer #custBottomNav .dh-navbtn:active .dh-nicon { transform: scale(.9); }
/* Press feedback has to re-state the lift — transform replaces, never stacks. */
body.dh-customer #custBottomNav .dh-navbtn:not(.dh-active):active .dh-nicon {
  transform: translateY(-7px) scale(.9);
}
body.dh-customer #custBottomNav .dh-navbtn.dh-active:active .dh-nicon {
  transform: translateY(-16px) scale(.93);
}

/* The active tab's raised gold circle IS the selection affordance, so the old
   marker dot would be redundant. Kept in the markup (JS toggles it) but never
   painted — see _setCustTabActive. */
body.dh-customer #custBottomNav .dh-navtab { display: none !important; }

/* The active circle is drawn ABOVE the button's own box (translateY), so the
   part of it that overhangs the pill would not be tappable. This transparent
   pseudo element extends the hit area upward to cover the whole circle — a
   pseudo element's pointer events are attributed to its host element.
   Only the ACTIVE tab gets it (it is the only raised one) and it is exactly as
   wide as the circle, so it can never bleed sideways into a neighbouring tab's
   tap target. */
body.dh-customer #custBottomNav .dh-navbtn.dh-active::before {
  content: ''; position: absolute; left: 50%; top: -20px;
  width: 54px; height: 58px; margin-left: -27px; border-radius: 27px;
}

/* Customer brand mark in the topbar (tap → Home/dashboard). */
#custTopHome { display: none; }
body.dh-customer #custTopHome {
  display: inline-flex; align-items: center; cursor: pointer;
  background: none; border: none; padding: 4px; margin-right: 4px;
  font-family: inherit;
}
#custTopHome .dh-brandmark {
  width: 34px; height: 34px; border-radius: 11px; background: var(--dh-gold);
  display: flex; align-items: center; justify-content: center;
  font-size: 16px; font-weight: 700; color: var(--dh-ink);
  font-family: var(--dh-serif); flex-shrink: 0;
}
#custTopHome:active .dh-brandmark { transform: scale(0.94); }
#custTopHome .dh-brandlogo {
  width: 42px; height: 42px; object-fit: contain; flex-shrink: 0;
}
#custTopHome:active .dh-brandlogo { transform: scale(0.94); }

/* Customer name chip in the topbar (tap → My Profile). */
#custTopChip { display: none; }
body.dh-customer #custTopChip {
  display: inline-flex; align-items: center; gap: 9px; cursor: pointer;
  background: none; border: none; padding: 4px; font-family: inherit;
}
#custTopChip .dh-avatar {
  width: 34px; height: 34px; border-radius: 11px; background: #fff;
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 700; color: var(--dh-ink); font-family: var(--dh-serif);
  flex-shrink: 0;border: 1px solid transparent;
}
#custTopChip .dh-cname { font-size: 13px; font-weight: 600; color: var(--dh-ink); max-width: 120px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@media (max-width: 330px) { #custTopChip .dh-cname { display: none; } }

/* ===========================================================================
 * CUSTOMER TOPBAR — polish pass (2026-08-06)
 *
 * The bar carries the brand mark, the section title, the bell and the profile
 * chip. Four things were off:
 *   · the logo (42px) and the avatar (34px) sat at visibly different weights
 *     on the two ends of the same bar;
 *   · the bell was a bare 22px glyph with 6px of padding — a 34px target,
 *     under the 44px guideline, with nothing to press against;
 *   · the count badge sat flush on the bell glyph with no separation;
 *   · the title used the staff sans while every heading inside the app
 *     ("Invest & grow") uses the display face.
 * Staff screens are untouched — everything here is body.dh-customer scoped.
 * ======================================================================== */

/* styles.css pins .topbar-title to 13px !important below 400px, i.e. on most
 * phones, so !important is the only way to reach it. */
body.dh-customer .topbar-title {
  font-family: var(--dh-serif) !important;
  font-size: 17px !important;
  font-weight: 700;
  color: var(--dh-ink);
  letter-spacing: -.01em;
}
@media (max-width: 360px) {
  body.dh-customer .topbar-title { font-size: 15px !important; }
}

/* Two 40px targets side by side; the base drops to gap:6px on phones, which
 * is too much once each control has its own padded box. */
body.dh-customer .topbar-right { gap: 2px; }

/* Bell — a real icon button with a press surface. */
body.dh-customer .dh-topicon {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; padding: 0;
  background: none; border: none; border-radius: 13px;
  color: var(--dh-ink); cursor: pointer; font-family: inherit;
  -webkit-tap-highlight-color: transparent;
  transition: background .15s ease, transform .12s ease;
}
body.dh-customer .dh-topicon:active { background: var(--dh-gold-dim); transform: scale(.94); }

/* Count badge. Ringed in the topbar's own white so it reads as a separate
 * object rather than merging into the bell. `display` is deliberately NOT set
 * here — _custSetNotifBadge owns it inline. */
body.dh-customer .dh-notifbadge {
  position: absolute; top: 4px; right: 4px;
  min-width: 16px; height: 16px; padding: 0 4px; box-sizing: border-box;
  border-radius: 999px;
  background: var(--dh-error); color: #fff;
  font-size: 10px; font-weight: 700; line-height: 16px; text-align: center;
  box-shadow: 0 0 0 2px #fff;
}

/* ═══ Notifications as a PAGE, sliding in from the right (2026-08-10) ══════
 *
 * Was a dropdown anchored under the bell — a 380px card that had to be measured
 * and clamped against the viewport on every open and every resize, and that
 * covered the screen it floated over. A list you scroll and tap into is a
 * destination, so it is now a full-screen page with the platform push
 * transition: in from the right, out to the right.
 *
 * translateX, not left/right offsets: transform is compositor-driven, so the
 * slide stays smooth in a WebView where animating layout properties judders.
 * The page keeps display:none between opens so it is out of the a11y tree and
 * cannot swallow taps while closed — .dh-open drives the motion, JS flips
 * display around it. */
.dh-notifpage {
  position: fixed; inset: 0; z-index: 1400;
  display: none; flex-direction: column;
  background: var(--dh-bg, #fff);
  transform: translateX(100%);
  transition: transform .28s cubic-bezier(.22, 1, .36, 1);
  will-change: transform;
}
.dh-notifpage.dh-open { transform: translateX(0); }
/* Header sits on the same warm band as the app's topbar so the page reads as
 * part of the shell, and carries the status-bar inset itself — the page is
 * fixed to the viewport, so it is outside .topbar's own safe-area padding. */
.dh-notifpage-hd {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 10px;
  padding: max(env(safe-area-inset-top), 12px) 14px 12px;
  /* background: var(--dh-sheet-hdr, #FBF7EF); */
  border-bottom: 1px solid var(--dh-border2);
}
.dh-notifpage-hd .dh-notifpage-back {
  flex: 0 0 auto; width: 34px; height: 34px; margin-left: -4px;
  display: flex; align-items: center; justify-content: center;
  background: none; border: none; padding: 0; cursor: pointer;
  color: var(--dh-gold); border-radius: 50%;
  -webkit-tap-highlight-color: transparent;
}
.dh-notifpage-hd .dh-notifpage-back:active { background: var(--dh-gold-dim); }
.dh-notifpage-hd .dh-notifpage-t {
  font-family: var(--dh-serif); font-size: 18px; font-weight: 700;
  color: var(--dh-ink);
}
/* The list is the only scroller; the header stays put. Bottom inset clears the
 * home indicator / gesture bar — the page covers the bottom nav, so it cannot
 * borrow that padding. */
.dh-notifpage-body {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
  padding: 8px 8px calc(16px + env(safe-area-inset-bottom, 0px));
}
@media (prefers-reduced-motion: reduce) {
  .dh-notifpage { transition: none; }
}

/* ── IN-PAGE PUSH TRANSITION (2026-08-11, Prem) ─────────────────────────────
 * My Profile's four detail rows (Identity & KYC / Payment methods / Personal
 * details / Security) open by swapping #custProfileContent's innerHTML — same
 * screen, same scroll container — so the new page simply blinked into place
 * with nothing to say it was a level deeper.
 *
 * This gives that swap the platform push, and it is deliberately the SAME
 * motion as .dh-notifpage above — .28s on cubic-bezier(.22,1,.36,1), in from
 * the right — so "going deeper" looks identical whether the destination is a
 * full-screen page or an in-place one.
 *
 * Forward and back are not the same animation. On the way back the previous
 * screen is not arriving from off-stage on the right; it is the screen you
 * left, coming back from where it was parked. Reusing the forward slide would
 * read as "one level deeper" for a gesture that means the opposite, so back
 * enters from the LEFT, and from a shorter distance (32%) at the platform's
 * usual parallax ratio.
 *
 * translateX + opacity ONLY: both are compositor-driven, so this stays smooth
 * in a WebView where animating layout properties judders. Nothing here moves a
 * box, so no reflow and no effect on the scroll height.
 *
 * No `will-change`. It would pin a permanent compositing layer on the profile
 * content, and text drawn into one loses subpixel antialiasing — the exact
 * blur that had to be undone on the modal card (see the note at
 * §"Android WebView modal-open flash fix"). A running animation gets its own
 * layer anyway, for the 280ms it needs one.
 *
 * `both` holds the final frame so there is no flash at the end — which also
 * means the element keeps a transform once the animation finishes, and an
 * ancestor with a transform becomes the containing block for any
 * position:fixed descendant. So _dhPush() strips the class on animationend;
 * these classes are never left on an element at rest. */
@keyframes dhPushFwd {
  from { transform: translateX(100%); opacity: .55; }
  to   { transform: translateX(0);    opacity: 1; }
}
@keyframes dhPushBack {
  from { transform: translateX(-32%); opacity: .55; }
  to   { transform: translateX(0);    opacity: 1; }
}
/* Transform-free variant for a page that carries .dh-homeband. That band is
 * `position: sticky`, and a transform on any ancestor re-parents it so it drops
 * out of its pin while the animation runs — see the ⚠ note on _dhPush() in
 * index.html, which is what chooses between these three. Opacity creates a
 * stacking context but NOT a containing block, so the pin survives. */
@keyframes dhPushFade {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.dh-pushfwd  { animation: dhPushFwd  .28s cubic-bezier(.22, 1, .36, 1) both; }
.dh-pushback { animation: dhPushBack .28s cubic-bezier(.22, 1, .36, 1) both; }
.dh-pushfade { animation: dhPushFade .20s ease both; }
/* The slide carries no information the destination doesn't already state. */
@media (prefers-reduced-motion: reduce) {
  .dh-pushfwd, .dh-pushback, .dh-pushfade { animation: none; }
}

/* Brand mark and avatar, matched in weight. NOTE: the logo's px size is ALSO
 * set inline in _ensureCustShell (deliberate — a cached stylesheet must not be
 * able to blow the mark up), so both places have to move together. */
#custTopHome .dh-brandlogo { width: 38px; height: 38px; }
#custTopHome .dh-brandmark { width: 38px; height: 38px; border-radius: 12px; }
#custTopChip .dh-avatar {
  width: 36px; height: 36px; border-radius: 24px;
  box-shadow: 0 2px 8px 2px rgb(89 81 65 / 28%);
}
body.dh-customer #custTopChip {
  gap: 8px; padding: 4px 4px 4px 8px; border-radius: 14px;
  -webkit-tap-highlight-color: transparent;
  transition: background .15s ease, transform .12s ease;
}
body.dh-customer #custTopChip:active { background: var(--dh-gold-dim); transform: scale(.97); }

/* ===========================================================================
 * styledConfirm as a BOTTOM SHEET (2026-08-06)
 *
 * The "Coming soon" / "book a branch visit" dialogs (_custSoonSheet /
 * _custBranchSheet) and every other customer confirm go through the shared
 * styledConfirm() helper, which rendered a centred desktop-style card. On a
 * phone that is the wrong idiom — the rest of the customer app already speaks
 * in bottom sheets (#custMoreSheet), so these now slide up from the bottom
 * edge to match.
 *
 * `.dh-cfsheet` is added by styledConfirm ONLY in a customer session, so the
 * staff hard-delete confirms that share the helper are untouched.
 *
 * !important is unavoidable here, not laziness: styledConfirm writes its
 * layout as INLINE styles on the elements (position/display/align-items on the
 * overlay, width/border-radius/padding on the card), and an inline style beats
 * any stylesheet selector. Only the properties that must flip are forced.
 * ========================================================================= */
/* ── Kill the inherited drag-handle pseudo-element ────────────────────────────
 * At ≤480px styles.css adds a bottom-sheet grip to .modal:
 *     .modal::before { content:''; display:block; width:36px; height:4px;
 *                      margin:8px auto 0; }
 * That is correct where .modal IS the card. But styledConfirm puts .modal on
 * the OVERLAY, which is display:flex — so the grip becomes a FLEX ITEM sitting
 * beside the card. It claims 36px of the row and the card shrinks to the rest
 * (480 − 36 = 444px), hard against the right edge with a 36px gap on the left.
 * That is why the sheet looked broken at exactly ≤480px and correct at 481px.
 * `content: none` removes the box entirely rather than just hiding it, so it
 * stops being a flex item at all.
 * Applied to .dh-cfroot (every styledConfirm, both variants) because the
 * CENTRED staff dialog inherits the same stray grip on a narrow window. */
.dh-cfroot::before { content: none !important; }

.dh-cfsheet {
  align-items: flex-end !important;
  justify-content: center !important;
  padding: 0 !important;
  background: rgba(10, 10, 10, .45) !important;
  animation: dhCfScrimIn .2s ease backwards;
  /* ── Neutralise .modal ────────────────────────────────────────────────────
   * styledConfirm puts class="modal" on the OVERLAY and "modal-content" on the
   * card — the inverse of the app's own convention, where .modal IS the card.
   * So the scrim inherits styles.css's card rules. Left alone they break the
   * sheet outright: `max-height: 90vh` + `align-items: flex-end` would park the
   * sheet 10vh ABOVE the bottom edge, `width: calc(100vw - 48px)` would leave a
   * strip of screen unscrimmed, and the 3px gold `border-top` would draw a line
   * across the top of the dimmed backdrop. (.modal-content itself is styled
   * nowhere — it is a pure hook, so the card is safe to own here.) */
  width: 100% !important;
  max-width: 100% !important;
  max-height: 100% !important;
  overflow: hidden !important;
  border: none !important;
  border-radius: 0 !important;
  box-shadow: none !important;
}
.dh-cfsheet > .modal-content {
  width: 100% !important;
  /* Full-bleed on a phone. On wider viewports the sheet must line up with the
     customer content column, NOT float at some width of its own — the column is
     capped at 480px from 540px and 520px from 720px (see the breakpoints far
     below), so the sheet matches those exactly. A flat 520px here made the
     sheet 40px wider than the column between 540 and 719px, so it visibly stuck
     out on both sides instead of reading as part of the app. */
  max-width: 100% !important;
  border-radius: 22px 22px 0 0 !important;
  /* Clear the iOS home indicator / Android gesture bar. */
  padding: 18px 20px calc(18px + env(safe-area-inset-bottom, 0px)) !important;
  max-height: 88vh !important;
  box-shadow: 0 -10px 40px rgba(0, 0, 0, .22) !important;
  animation: dhCfSheetIn .26s cubic-bezier(.22, 1, .36, 1) backwards;
}
/* Grip — the affordance for swipe-to-dismiss, and one of the two drag handles.
   `touch-action: none` is what makes the gesture ours: without it the browser
   claims the vertical pan and the pointermove handler never gets to move the
   sheet. Same on the header row below. */
.dh-cfsheet .dh-cfsheet-grip {
  width: 38px; height: 4px; border-radius: 2px;
  background: var(--dh-gold);
  margin: -6px auto 12px; flex-shrink: 0;
  touch-action: none;
  /* Painted ABOVE the header's bled-up cream band below — the header comes
     later in the DOM, so without its own paint layer the grip would disappear
     behind that background. */
  position: relative; z-index: 1;
}
/* Header row: title left, × right, on the same warm band the app's other
 * bottom sheets use (body.dh-customer .modal-overlay > .modal > .modal-header
 * → --dh-sheet-hdr). styledConfirm writes its own markup rather than
 * .modal-overlay > .modal, so it never inherited that rule and its header sat
 * on plain white while every other sheet's was cream.
 *
 * The card carries `padding: 18px 20px …`, so the band is bled back out to the
 * card edges with negative margins and the padding re-added inside. The top
 * offset is 28px = 18px card padding + the grip's own box (-6 margin + 4 tall
 * + 12 below); it must reach the very top of the card, and the top corners are
 * rounded to match the card's 22px so the band can't square off its corners. */
.dh-cfsheet .dh-cfsheet-hd {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 12px;
  background: var(--dh-sheet-hdr, #FBF7EF);
  margin: -28px -20px 14px;
  padding: 28px 20px 13px;
  border-radius: 22px 22px 0 0;
  touch-action: none;
}
/* …but the × inside it is a button, not part of the drag surface. */
.dh-cfsheet .dh-cfsheet-hd .dh-cfsheet-x { touch-action: manipulation; }
/* Keep a drag/scroll at the sheet's edge from chaining to the page behind it. */
.dh-cfsheet > .modal-content { overscroll-behavior: contain; }
.dh-cfsheet .dh-cfsheet-x {
  flex-shrink: 0; width: 32px; height: 32px; margin: -4px -4px 0 0;
  display: flex; align-items: center; justify-content: center;
  border: none; border-radius: 50%; background: #ffffff;
  color: var(--dh-muted, #8A8A8A); cursor: pointer; font-family: inherit;
}
.dh-cfsheet .dh-cfsheet-x:active { background: var(--dh-border, rgba(0,0,0,.08)); }
/* Full-width stacked actions. DOM order is [cancel][confirm] and the column
   keeps it that way — secondary on top, primary as the bottom-most, easiest
   thumb target. */
.dh-cfsheet .dh-cfsheet-actions {
  display: flex !important;
  flex-direction: column !important;
  align-items: stretch !important;
  justify-content: flex-start !important;
  gap: 10px !important;
  margin-top: 4px;
}
.dh-cfsheet .dh-cfsheet-actions .btn {
  width: 100%; min-width: 0 !important;
  padding: 14px 16px; border-radius: 14px;
  font-size: 15px; font-weight: 700;
}
/* Primary action in gold — same treatment as .dh-btn-gold, so the sheet's CTA
 * matches every other primary button in the customer app.
 *
 * The DANGER variant is untouched on purpose: styledConfirm writes
 * `background:#dc2626;border-color:#dc2626` as an INLINE style for those, which
 * beats this rule — so destructive confirms stay red rather than turning gold.
 * Scoped to .dh-cfsheet (customer sessions only), so the staff dialogs that
 * share the same helper keep the default primary. */
.dh-cfsheet .dh-cfsheet-actions .btn-primary {
  background: var(--dh-gold, #C9962A);
  border-color: var(--dh-gold, #C9962A);
  color: var(--dh-ink, #0A0A0A);
  box-shadow: 0 4px 16px rgba(201, 150, 42, .30);
}
.dh-cfsheet .dh-cfsheet-actions .btn-primary:active { filter: brightness(.96); }
/* The DISMISSAL is deliberately not a CSS animation — styledConfirm drives it
   with an inline transition instead, because a swipe leaves the card at an
   arbitrary offset and the exit has to continue from wherever the finger let
   go. A keyframe would restart from translateY(0) and snap. */

/* fill-mode is `backwards`, deliberately — NOT `both`, and not omitted.
 *   · omitted → the sheet paints one frame at its RESTING position before the
 *     animation's first frame, which reads as a flash/jump on open.
 *   · both → the finished animation keeps asserting `transform: translateY(0)`
 *     forever. A permanent transform on the card makes it a containing block
 *     for any position:fixed descendant and keeps it on its own compositing
 *     layer, and — because animations outrank inline styles — it also fights
 *     every inline transform the drag and the close write.
 *   · backwards → the `from` state applies before the animation starts (no
 *     flash) and nothing is held after it ends (the natural state is already
 *     translateY(0), so it looks identical). */
@keyframes dhCfSheetIn { from { transform: translateY(100%); } to { transform: translateY(0); } }
@keyframes dhCfScrimIn { from { opacity: 0; } to { opacity: 1; } }

/* Reduced motion: no entry animation. styledConfirm reads the same media query
   in JS and removes the sheet instantly instead of transitioning it out, so the
   two halves stay in step. Swipe-to-dismiss still works — the drag itself is a
   direct response to the finger, not decorative motion. */
@media (prefers-reduced-motion: reduce) {
  .dh-cfsheet,
  .dh-cfsheet > .modal-content { animation: none !important; }
}

/* ===========================================================================
 * EVERY CUSTOMER MODAL IS A BOTTOM SHEET (2026-08-06)
 *
 * Covers the two remaining modal families so the whole customer app is
 * consistent with styledConfirm above and the existing #custMoreSheet:
 *   · the generic overlays  .modal-overlay > .modal   (openModal/closeModal)
 *   · the alert/confirm/prompt popup  #appPopup > div (appAlert & friends)
 *
 * styles.css already did this below 480px. Here it applies at EVERY width and
 * only for body.dh-customer, so staff keep the centred dialog they were
 * designed for. The close animation is driven from JS (_dhSheetClose) because
 * .modal-overlay toggles `display`, which no CSS transition can animate.
 * ========================================================================= */
body.dh-customer .modal-overlay { align-items: flex-end; }
body.dh-customer .modal-overlay > .modal,
body.dh-customer #appPopup > div {
  width: 100%;
  max-width: 100%;
  margin: 0;
  border-radius: 22px 22px 0 0;
  /* The 3px gold rule reads as the top edge of a CARD; on a sheet that is
     flush with the screen edge it just looks like a stray line. */
  border-top: none;
  box-shadow: 0 -10px 40px rgba(0, 0, 0, .22);
  animation: dhCfSheetIn .28s cubic-bezier(.22, 1, .36, 1) backwards;
  /* Deliberately no max-height: the wizards (#wizModal, #signupWizardModal…)
     set their own 95dvh + flex column in styles.css and must keep it. */
}
/* The inline-styled overlays need !important to flip, since their layout is
   written on the element. #appPopup is the alert/confirm/prompt popup; the rest
   are the older ad-hoc modals whose card is a plain `> div` rather than .modal
   (styles.css already sheeted these below 480px — this extends it to every
   width so the whole customer app is consistent). */
body.dh-customer #appPopup,
body.dh-customer #feedbackModal,
body.dh-customer #newLockerReqModal,
body.dh-customer #forceCustPwdModal {
  align-items: flex-end !important;
  padding: 0 !important;
}
body.dh-customer #appPopup > div,
body.dh-customer #feedbackModal > div,
body.dh-customer #newLockerReqModal > div,
body.dh-customer #forceCustPwdModal > div {
  width: 100% !important;
  max-width: 100% !important;
  margin: 0 !important;
  border-radius: 22px 22px 0 0 !important;
  animation: dhCfSheetIn .28s cubic-bezier(.22, 1, .36, 1) backwards !important;
  overscroll-behavior: contain;
}
/* ── Sheet chrome: one tinted bar, gold grip ─────────────────────────────────
 * The grip strip and the header used to be two separate slabs of plain white
 * (#fff on .modal, and --card-bg on .modal-header), so the top of every sheet
 * was an undifferentiated white block with a pale grey line floating in it.
 *
 * The strip is tinted with a hard-stop GRADIENT rather than by setting
 * .modal's background-color: the grip lives in the first ~12px (4px tall with
 * an 8px top margin) and the header is tinted separately, so 14px covers the
 * strip exactly while leaving the rest of the card white. Colouring the whole
 * card instead would tint the body of any modal that has no .modal-body
 * wrapper — and several build their content free-form. */
body.dh-customer .modal-overlay > .modal {
  background-image: linear-gradient(var(--dh-sheet-hdr) 14px, transparent 14px);
}
body.dh-customer .modal-overlay > .modal > .modal-header,
body.dh-customer #appPopup > div > #appPopupHeader {
  background: var(--dh-sheet-hdr);
}
/* The drag grip itself — gold, so the handle reads as an affordance rather than
   a hairline. (styles.css draws it as .modal::before at ≤480px.) */
body.dh-customer .modal-overlay > .modal::before {
  background: var(--dh-gold);
  width: 38px;
}

/* ── Signup sheet ("Create your account") — same top strip, pre-login ────────
 * openQuickSignup runs BEFORE there is a session, so <body> carries neither
 * .dh-customer nor .dh-prospect and none of the sheet chrome above reaches it.
 * At ≤480px styles.css turns it into a bottom sheet but keeps `.modal`'s
 * `border-top: 3px solid var(--gold)` AND draws the ::before grip in grey — so
 * the top 14px of the sheet showed a gold rule and a grey pill, two accent
 * colours stacked (Prem, 2026-08-11). Same treatment the customer sheets get:
 * the rule goes (it reads as the top edge of a CARD, which a sheet flush with
 * the screen edge is not) and the grip carries the single gold accent.
 *
 * Scoped to this modal by id rather than to `.modal-overlay > .modal`, which
 * would also restyle every staff dialog. The ::before has no `content` above
 * 480px, so this is inert on the centred-dialog layout — nothing to guard. */
#quickSignupModal > .modal { border-top: none;border-bottom-right-radius: 0px;border-bottom-left-radius: 0px; }
#quickSignupModal > .modal::before {
  background: var(--dh-gold);
  width: 38px;
}
/* …and the strip the grip sits in. `.modal` is #FFFFFF (styles.css) while
   `.modal-header` is var(--bg) = #FAFAF7 (dhanam-theme.css), so the top of the
   sheet was two near-whites meeting in a visible seam ~14px down — the second
   half of the same report. Hard-stop gradient rather than recolouring `.modal`,
   for the reason given on the customer rule above: the tint must stop at the
   header and leave the card body white. var(--bg) is the SAME token the header
   reads, so the two cannot drift apart again. */
#quickSignupModal > .modal {
  background-image: linear-gradient(var(--bg) 14px, transparent 14px);
}

/* THE affirmative-action CTA on a customer surface — gold on ink, matching
   .dh-btn-primary. Add this class next to .btn-primary on any customer button
   that commits the customer to something ("Book Visit", "Invest now",
   "Start saving", "Rent a locker", "Apply now").

   It exists because the generic .btn-primary is INK here: dhanam-theme.css sets
   --primary: #0A0A0A app-wide, which is right for staff screens. Before this,
   each customer modal reached for its own hand-picked hex (#0f172a slate,
   #d97706 amber, #4a7ce0 blue, #2d7a6a green) and the product sheets ended up
   four different colours. One class, one colour — and staff .btn-primary is
   untouched because staff markup never carries the class.

   `.btn.dh-cta-gold` (0,2,0) outranks `.btn-primary` (0,1,0), so no !important. */
.btn.dh-cta-gold {
  background: var(--dh-gold);
  color: var(--dh-ink);
  box-shadow: 0 4px 16px rgba(201,150,42,0.30);
}
.btn.dh-cta-gold:hover {
  background: var(--dh-gold);
  box-shadow: 0 4px 16px rgba(201,150,42,0.30);
  filter: brightness(0.97);
}
.btn.dh-cta-gold:disabled { opacity: 0.6; filter: none; }

/* Same rule for the product-hub tiles' CTA. styles.css:2962 gives
   .hero-cta-primary only a border reset and a shadow — every tile used to hand
   it a background inline (#4a7ce0 blue / #fbbf24 amber / #2d7a6a green), which
   is exactly the four-colours-per-screen problem. The colour lives here now, so
   the tiles carry no inline background at all. */
.hero-cta.hero-cta-primary {
  background: var(--dh-gold);
  color: var(--dh-ink);
}
/* The tile CTAs carry a trailing arrow ICON now instead of a "→" character, so
   the button has to become a flex row for it to sit on the text's centre line
   rather than its baseline.
   styles.css:2942 gives .hero-cta `overflow:hidden; text-overflow:ellipsis`,
   which stops working the moment the button is a flex container — so the label
   is wrapped in a <span> that carries the truncation instead, and the icon is
   flex:none. Under pressure on a narrow tile the LABEL ellipsises and the arrow
   survives, which is the right way round: a half-clipped arrow reads as a
   rendering fault, a truncated label reads as a truncated label. */
.hero-cta {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
}
.hero-cta > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hero-cta > svg  { flex: 0 0 auto; }

/* ONE surface for the four product tiles (NCD · Locker · Gold · Loans), in both
   places they are built: the static prospect-screen grid in index.html and the
   JS-built "Avail a Product" chooser. Each tile used to inline its own gradient
   — navy+blue, charcoal+brass, brown+amber, forest green — so a 2×2 grid read
   as four unrelated apps. Identity now comes from the icon and the title.

   The bottom-up scrim is load-bearing, not decoration: --dh-cardgrad runs
   #5e4002 → #c9962a, and white measures 9.5:1 on the top stop but only 2.66:1
   on the bottom one (see the token's own comment above). These tiles put the
   title, tag and CTA in the bottom half — exactly the light end. The scrim
   takes that half back to roughly 8:1 while leaving the gold visible up top. */
.hero-tile.dh-prodtile {
  background:
    linear-gradient(to top, rgba(10,10,10,0.55) 0%, rgba(10,10,10,0.12) 58%, rgba(10,10,10,0) 100%),
    var(--dh-cardgrad);
  color: #fff;
  border: 1px solid var(--dh-gold-border);
  box-shadow: 0 12px 30px rgba(90,62,10,0.35);
}
.hero-tile.dh-prodtile .hero-tile-icon-wrap { color: #fff; }
.hero-tile.dh-prodtile .hero-tile-compact-tag { color: var(--dh-oncard-muted); }
/* The prospect tiles' "Learn More →" is a pill, not a .hero-cta button. */
.hero-tile.dh-prodtile .dh-prodtile-pill {
  display: inline-block;
  align-self: flex-start;
  background: var(--dh-gold);
  color: var(--dh-ink);
  font-size: 12px;
  font-weight: 700;
  padding: 6px 16px;
  border-radius: 14px;
}

/* Clear the iOS home indicator / Android gesture bar on the sheet's own edge. */
body.dh-customer .modal-overlay > .modal > .modal-footer,
body.dh-customer #appPopup > div > #appPopupFooter {
  padding-bottom: max(16px, calc(16px + env(safe-area-inset-bottom, 0px)));
}
/* The header is the drag handle (see _dhSheetDrag): the browser must not claim
   the VERTICAL pan, or the sheet never moves.
   `pan-x`, not `none` — some headers hold a horizontally-scrolling strip (the
   allotment form's 6-tab strip), and `none` would take that scrolling away too.
   pan-x leaves horizontal panning with the browser and gives us the vertical
   gesture, which is all the drag needs. Controls inside stay tappable either
   way; touch-action only governs panning and zooming. */
body.dh-customer .modal-overlay > .modal > .modal-header,
body.dh-customer #appPopup > div > #appPopupHeader { touch-action: pan-x; }
body.dh-customer .modal-overlay > .modal > .modal-header button,
body.dh-customer .modal-overlay > .modal > .modal-header a { touch-action: manipulation; }
/* Don't let a drag at the sheet's edge chain into the page behind it. */
body.dh-customer .modal-overlay > .modal,
body.dh-customer #appPopup > div { overscroll-behavior: contain; }

/* Small label/value tables in customer sheets must not be forced 640px wide.
 * styles.css:1635 sets a BARE `table { font-size: 12px; min-width: 640px }`
 * inside its `@media (max-width: 480px)` block. That's aimed at staff data
 * grids, which live in a horizontally-scrolling `.table-container` and genuinely
 * need the width — but a bare `table` selector also catches the little
 * two-column tables in customer sheets (Locker Auto-Pay's rent/cap/frequency/
 * method rows, and others like it). At 640px they overflow a ~380px phone, so
 * the sheet scrolled sideways with every value clipped mid-word.
 *
 * `min-width` always beats `width`, so dhanam-theme.css's
 * `.modal-body table { table-layout: fixed; width: 100% }` could never take
 * effect. Clearing min-width is the missing piece that lets it hold, and the
 * columns then wrap inside the sheet as intended.
 *
 * Scoped to body.dh-customer, so staff tables are untouched; the second rule
 * keeps genuine data grids scrolling should a customer surface ever use one. */
@media (max-width: 480px) {
  body.dh-customer table { min-width: 0; }
  body.dh-customer .table-container table { min-width: 100%; }
  /* Same story 16 lines further down that block: `td { white-space: nowrap }`
     (styles.css:1651). Also for data grids — but it stops the long values in
     these label/value tables ("(includes 20% buffer for future revisions)")
     from EVER wrapping, so the cell spills past the sheet no matter how wide
     the table is allowed to be. Clearing min-width alone couldn't fix that.
     `.dh-kv td:last-child` is more specific and keeps its deliberate nowrap,
     which is what stops a ₹ amount breaking across lines. */
  body.dh-customer td { white-space: normal; }
  body.dh-customer .table-container td { white-space: nowrap; }
}

/* ═══════════════════════════════════════════════════════════════════════════
 * appAlert / appConfirm / appPrompt sheet — visual pass (2026-08-06)
 *
 * Everything here is body.dh-customer scoped: #appPopup also backs every staff
 * dialog, and those stay exactly as they are. !important appears where the
 * markup carries the value inline (#appPopup is inline-styled end to end).
 *
 * Fixed:
 *  · The icon rendered as a tiny grey glyph. #appPopupIcon sets font-size:44px,
 *    but window.ICN.* are 14px SVGs with explicit width/height attributes — a
 *    font-size can never scale those. It is now a proper chip with the SVG
 *    sized in CSS (which beats the presentational attributes) and coloured by
 *    popup type, so an error no longer shows a gold "everything's fine" chip.
 *  · Three surfaces in one card — cream header, white body, #fafafa footer with
 *    a seam. Now one warm surface, matching the tinted-sheet language the other
 *    customer sheets already use.
 *  · The buttons were two narrow centred pills. On a bottom sheet they are the
 *    primary target: full-width, 50px tall, gold for the confirm action.
 *  · No grip, even though the header IS the drag handle (see _dhSheetDrag).
 * ═════════════════════════════════════════════════════════════════════════ */

/* One surface — no bands. */
body.dh-customer #appPopup > div { background: var(--dh-sheet-hdr) !important; }

/* Grip: same gold bar the other customer sheets use, so the drag affordance
   reads the same everywhere. */
body.dh-customer #appPopup > div > #appPopupHeader {
  position: relative;
  padding-top: 24px !important;
}
body.dh-customer #appPopup > div > #appPopupHeader::before {
  content: ''; position: absolute; top: 9px; left: 50%; margin-left: -19px;
  width: 38px; height: 4px; border-radius: 2px; background: var(--dh-gold);
}

/* Icon chip. The inner SVG is sized here because ICN.* hard-code width/height
   as HTML attributes — CSS outranks those; the inline margin-right needs the
   !important. */
body.dh-customer #appPopup #appPopupIcon {
  width: 54px; height: 54px; margin: 0 auto 14px !important;
  border-radius: 17px; display: flex; align-items: center; justify-content: center;
  background: var(--dh-gold-dim); color: var(--dh-gold);
}
body.dh-customer #appPopup #appPopupIcon svg {
  width: 26px; height: 26px; stroke-width: 1.9;
  margin: 0 !important; vertical-align: middle !important;
}
/* data-ptype is stamped on the root by _appPopupOpen. */
body.dh-customer #appPopup[data-ptype="error"]   #appPopupIcon { background: rgba(217,64,64,.10);  color: var(--dh-error); }
body.dh-customer #appPopup[data-ptype="warning"] #appPopupIcon { background: rgba(180,83,9,.10);   color: #B45309; }
body.dh-customer #appPopup[data-ptype="success"] #appPopupIcon { background: rgba(13,148,136,.10); color: #0D9488; }

body.dh-customer #appPopup #appPopupTitle {
  font-family: var(--dh-serif);
  font-size: 20px !important; color: var(--dh-ink) !important;
  letter-spacing: -.01em; margin-bottom: 8px !important;
}
body.dh-customer #appPopup #appPopupBody {
  font-size: 14.5px !important; color: var(--dh-muted2) !important;
  padding: 2px 24px 22px !important;
}

/* Actions: full-width, thumb-sized, gold confirm. */
body.dh-customer #appPopup > div > #appPopupFooter {
  background: transparent !important; border-top: none !important;
  gap: 10px !important; flex-wrap: nowrap !important;
  padding-left: 20px !important; padding-right: 20px !important;
}
body.dh-customer #appPopup #appPopupFooter .btn {
  flex: 1 1 0; min-width: 0 !important; min-height: 50px;
  border-radius: 15px; font-size: 15px; font-weight: 700;
}
/* NOT !important on the background: _appPopupOpen writes the destructive red
   and the success teal INLINE, and those must keep winning. */
body.dh-customer #appPopup #appPopupFooter .btn-primary {
  background: var(--dh-gold); border-color: var(--dh-gold); color: var(--dh-ink);
}
body.dh-customer #appPopup #appPopupFooter .btn-secondary {
  background: #fff; border: 1.5px solid var(--dh-border2); color: var(--dh-ink);
}

@media (prefers-reduced-motion: reduce) {
  body.dh-customer .modal-overlay > .modal,
  body.dh-customer #appPopup > div { animation: none !important; }
}

/* "More" bottom sheet. */
#custMoreSheet { position: fixed; inset: 0; z-index: 1300; display: none; }
#custMoreSheet.dh-open { display: block; }
#custMoreSheet .dh-sheet-scrim { position: absolute; inset: 0; background: rgba(10,10,10,.42); }
#custMoreSheet .dh-sheet {
  position: absolute; left: 0; right: 0; bottom: 0;
  background: var(--dh-bg); border-radius: 22px 22px 0 0;
  padding-top: 0px !important;
  padding: 8px 16px calc(20px + env(safe-area-inset-bottom, 0px));
  max-height: 82vh; overflow-y: auto; box-shadow: 0 -10px 40px rgba(0,0,0,.22);
}
#custMoreSheet .dh-sheet-grip { width: 38px; height: 4px; border-radius: 2px; background: var(--dh-gold); margin: 8px auto 14px; }
#custMoreSheet .dh-sheet-top {
  position: sticky;
  top: 0;
  /* Above the scrolling content, which passes beneath it. */
  z-index: 2;
  margin: 0 -16px;
  padding: 8px 16px 0;
  background: var(--dh-bg);
  border-bottom: 1px solid var(--dh-border);
}
/* Inside the pinned block the grip's 14px bottom margin and the heading's 6px
   would print as dead space above the hairline. */
#custMoreSheet .dh-sheet-top .dh-sheet-grip { margin-bottom: 12px; }
#custMoreSheet .dh-sheet-top .dh-sheet-hd { margin-bottom: 12px; }
/* First element after the pinned block clears the hairline. */
#custMoreSheet .dh-sheet-top + * { margin-top: 14px; }

/* Open/close motion + swipe-to-dismiss for the shared sheet container.
 * This one had NO entry animation at all and closed by dropping a class, so it
 * appeared and vanished instantly while every other sheet slid. Scoped to
 * .dh-open so the animation replays on each open (the container toggles
 * display, which restarts it).
 *
 * The grip and the heading are the drag handles — they must yield the vertical
 * gesture to _dhSheetDrag. `pan-x` rather than `none` so any horizontally
 * scrollable content in a heading keeps working. The BODY deliberately has no
 * touch-action override: this card scrolls, and taking its gestures would make
 * long sheets (the profile gap form) unscrollable. */
#custMoreSheet.dh-open .dh-sheet { animation: dhCfSheetIn .28s cubic-bezier(.22, 1, .36, 1) backwards; }
#custMoreSheet.dh-open .dh-sheet-scrim { animation: dhCfScrimIn .2s ease backwards; }
#custMoreSheet .dh-sheet-grip,
#custMoreSheet .dh-sheet-hd { touch-action: pan-x; }
/* The grip is small; give it a taller invisible grab area so a thumb can find
   it without pixel-hunting. */
#custMoreSheet .dh-sheet-grip { position: relative; }
#custMoreSheet .dh-sheet-grip::after {
  /* Kept inside the grip's own 8px/14px margins so it never covers content. */
  content: ''; position: absolute; left: -50px; right: -50px; top: -8px; bottom: -6px;
}
@media (prefers-reduced-motion: reduce) {
  #custMoreSheet.dh-open .dh-sheet,
  #custMoreSheet.dh-open .dh-sheet-scrim { animation: none !important; }
}
#custMoreSheet .dh-sheet-hd { font-family: var(--dh-serif); font-size: 18px; font-weight: 600; color: var(--dh-ink); margin: 0 2px 6px; }
#custMoreSheet .dh-sheet-sub { font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--dh-muted); margin: 16px 2px 8px; }
.dh-sheet-row {
  width: 100%; display: flex; align-items: center; gap: 13px; text-align: left;
  padding: 13px 12px; border-radius: 14px; border: none; background: none; cursor: pointer;
  font-family: inherit; color: var(--dh-ink);
}
.dh-sheet-row:active { background: var(--dh-surface); }
.dh-sheet-row .dh-srchip { width: 38px; height: 38px; border-radius: 12px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; background: var(--dh-gold-dim); color: #634404; border: 1px solid rgba(201,150,42,.18); }
.dh-sheet-row .dh-srlabel { flex: 1; font-size: 14px; font-weight: 600; }
/* Stacked, not inline: "Call Customer Care" + its number were wrapping into
 * one ragged two-line run. Own line under the label, like the _subRow rows. */
.dh-sheet-row .dh-srsub { display: block; margin-top: 2px; font-size: 12px; color: var(--dh-muted); font-weight: 400; }
.dh-sheet-row .dh-srchev { color: var(--dh-faint); flex-shrink: 0; }
.dh-sheet-row.dh-danger .dh-srchip { background: var(--dh-error-dim); color: var(--dh-error); border-color: rgba(217,64,64,.2); }
.dh-sheet-row.dh-danger .dh-srlabel { color: var(--dh-error); }
.dh-sheet-badge { background: var(--dh-error); color: #fff; font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 9999px; margin-left: 6px; }

/* ===========================================================================
 * PERSONAL LOAN JOURNEY (custLoan) — borrower DLA, re-skinned.
 * Goal: make it EASY to get a loan — visible progress, plain language, big
 * gold CTAs, friendly waiting states. Composes the .dh-* parts above.
 * =========================================================================*/
.dh-loanwrap { display: flex; flex-direction: column; gap: 14px;margin-bottom: 20px; }
.dh-loanwrap .dh-card { padding: 20px; }

/* progress stepper — always shows how far along the borrower is */
.dh-steps { display: flex; align-items: center; gap: 6px; margin: 2px 0 4px; }
.dh-steps i { height: 4px; border-radius: 2px; background: var(--dh-border); flex: 1; transition: background .25s; }
.dh-steps i.dh-done { background: var(--dh-gold-light); }
.dh-steps i.dh-on  { background: var(--dh-gold); }
.dh-stepcap { font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: #514e4e; margin: 10px 0 2px; }
/* Group header (icon + caption) for the My Profile menu — _grpHd in
 * index.html. The margins were an INLINE style there, which outranks every
 * class rule, so the first header in a sheet could not have its top margin
 * trimmed and stacked 14px on top of the sheet's own padding. Moved here so it
 * can be overridden. MUST stay after .dh-stepcap: both are single-class
 * selectors, so source order is what makes this margin win. */
.dh-grphd { display: flex; align-items: center; gap: 7px; margin: 14px 4px 8px; }
.dh-grphd-ic { color: #634404; display: inline-flex; align-items: center; }

/* form fields */
.dh-flabel { display: block; font-size: 13px; font-weight: 600; color: var(--dh-ink); margin: 16px 0 7px; }
.dh-flabel:first-child { margin-top: 0; }

/* ── FLOATING-LABEL FIELD (2026-08-08) ────────────────────────────────────────
 * The label starts inside the box as the placeholder would, and rises to a
 * small caption once the field has focus or content. Opt-in per field via the
 * .dh-ffield wrapper, so the .dh-flabel + .dh-input stack above is untouched
 * everywhere it is still used.
 *
 * MARKUP ORDER IS LOAD-BEARING: the input comes FIRST, the label second.
 *   <div class="dh-ffield"><input class="dh-input" placeholder=" "><label
 *    class="dh-fflabel">…</label></div>
 * CSS has no previous-sibling combinator, so the filled state
 * (`:not(:placeholder-shown) + .dh-fflabel`) can only be expressed with the
 * input ahead of the label. Visual order is unaffected — the label is absolute.
 *
 * EVERY input needs a placeholder, even `" "`. Without the attribute
 * :placeholder-shown never matches, the label never rises, and it sits on top
 * of the value the customer typed. Use .dh-ff-fixed for controls that cannot
 * have one — <select>, and type="date", which renders dd/mm/yyyy chrome when
 * empty and so must keep its label raised at all times. */
.dh-ffield { position: relative; margin-top: 14px; }
.dh-ffield > .dh-input { padding-top: 23px; padding-bottom: 7px; }
.dh-ffield > .dh-fflabel {
  position: absolute; left: 15px; top: 15px;
  font-size: 15px; font-weight: 500; color: var(--dh-muted);
  transform-origin: left top;
  transition: transform .15s ease, color .15s ease;
  /* Never intercept the tap — the whole box must stay the input's hit area. */
  pointer-events: none;
  /* A long label must not wrap over the value or spill past the border. */
  max-width: calc(100% - 30px);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The real placeholder is hidden until focus, otherwise it and the resting
 * label would print on top of each other. On focus it becomes the hint (e.g.
 * "e.g. 60000") under the now-raised label. */
.dh-ffield > .dh-input::placeholder { color: transparent; }
.dh-ffield > .dh-input:focus::placeholder { color: var(--dh-faint); }
.dh-ffield:focus-within > .dh-fflabel,
.dh-ffield > .dh-input:not(:placeholder-shown) + .dh-fflabel,
.dh-ffield.dh-ff-fixed > .dh-fflabel {
  transform: translateY(-9px) scale(.75);
  color: var(--gold-dark, #8A6305);
}
/* Row of fields (City / Pincode). flex lives on the WRAPPER now, not the input
 * — the input is no longer the flex child. */
.dh-ffrow { display: flex; gap: 8px; }
.dh-ffrow > .dh-ffield { flex: 1; min-width: 0; }
@media (prefers-reduced-motion: reduce) {
  .dh-ffield > .dh-fflabel { transition: none; }
}
.dh-input { width: 100%; padding: 13px 14px; border: 1.5px solid var(--dh-border); border-radius: 13px; font-size: 16px; font-family: inherit; color: var(--dh-ink); background: var(--dh-bg); box-sizing: border-box; }
.dh-input:focus { outline: none; border-color: var(--dh-gold); box-shadow: 0 0 0 3px var(--dh-gold-dim); }

/* amount slider */
.dh-slider { -webkit-appearance: none; appearance: none; width: 100%; height: 6px; border-radius: 3px; background: var(--dh-border); outline: none; margin: 12px 0 2px; }
.dh-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 26px; height: 26px; border-radius: 50%; background: var(--dh-gold); border: 3px solid #fff; box-shadow: 0 2px 8px rgba(201,150,42,.45); cursor: pointer; }
.dh-slider::-moz-range-thumb { width: 22px; height: 22px; border-radius: 50%; background: var(--dh-gold); border: 3px solid #fff; box-shadow: 0 2px 8px rgba(201,150,42,.45); cursor: pointer; }
.dh-srange { display: flex; justify-content: space-between; font-size: 11px; color: var(--dh-muted); }

/* tenure chips */
.dh-chips { display: flex; gap: 8px; }
.dh-chip { flex: 1; padding: 12px 6px; border-radius: 13px; border: 1.5px solid var(--dh-border); background: var(--dh-bg); font-size: 14px; font-weight: 600; color: var(--dh-soft); cursor: pointer; font-family: inherit; text-align: center; }
.dh-chip.dh-on { border-color: var(--dh-gold); background: var(--dh-gold-dim); color: var(--dh-ink); }

/* key/value breakdown (offer + KFS) */
.dh-kv { width: 100%; min-width: 0; border-collapse: collapse; font-size: 14px; }
.dh-kv td { padding: 9px 0; border-bottom: 1px solid var(--dh-border2); overflow-wrap: anywhere; word-break: break-word; vertical-align: top; }
/* Label wraps and takes the leftover width; the value column shrinks to its
   content and never wraps, so a long label (e.g. "Documentation charge (incl.
   stamp duty)") can never overlap the amount. padding-right keeps a gap. */
.dh-kv td:first-child { width: auto; padding-right: 14px; }
.dh-kv tr:last-child td { border-bottom: none; }
.dh-kv td:first-child { color: var(--dh-muted); }
.dh-kv td:last-child { text-align: right; font-weight: 600; color: var(--dh-ink); white-space: nowrap; }
.dh-kv tr.dh-total td { font-weight: 800; color: var(--dh-ink); font-size: 15px; padding-top: 13px; }
/* Profile info cards reuse .dh-kv for text values (email, address) — these can be
   long and must wrap rather than overflow/clip, unlike the offer/KFS ₹ amounts. */
.dh-kv-info td:last-child { white-space: normal; overflow-wrap: anywhere; word-break: break-word; }

/* KFS / cost breakdown — flex rows that can NEVER overlap. Unlike the table
   (where a long label like "Documentation charge (incl. stamp duty)" could
   collide with the amount on a narrow phone), flex + space-between + gap keeps
   the two apart: the label takes the leftover width and wraps; the value holds
   its width on the right. */
.dh-kvlist { font-size: 14px; }
.dh-kvf { display: flex; justify-content: space-between; align-items: baseline; gap: 14px; padding: 9px 0; border-bottom: 1px solid var(--dh-border2); }
.dh-kvf:last-child { border-bottom: none; }
.dh-kvf > .k { flex: 1 1 auto; min-width: 0; color: var(--dh-muted); overflow-wrap: anywhere; word-break: break-word; }
.dh-kvf > .v { flex: 0 0 auto; text-align: right; font-weight: 600; color: var(--dh-ink); white-space: nowrap; }
.dh-kvf.dh-total { padding-top: 13px; }
.dh-kvf.dh-total > .k, .dh-kvf.dh-total > .v { font-weight: 800; color: var(--dh-ink); }
.dh-kvf.dh-total > .v { font-size: 15px; }

/* Product filter chips (My Applications list, and reusable). */
.dh-fchips { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
.dh-fchip { display: inline-flex; align-items: center; gap: 6px; padding: 7px 13px; border-radius: 999px;
  border: 1px solid var(--dh-border); background: var(--dh-card); color: var(--dh-muted);
  font-size: 13px; font-weight: 600; cursor: pointer; font-family: inherit; transition: background .12s, color .12s, border-color .12s; }
.dh-fchip:hover { border-color: var(--dh-gold); }
.dh-fchip.is-active { background: var(--dh-ink); color: #fff; border-color: var(--dh-ink); }
.dh-fchip-n { font-size: 11px; opacity: .7; }
.dh-fchip.is-active .dh-fchip-n { opacity: .85; }

/* centered status screens (eSign / mandate / bank / disbursal) */
.dh-icirc { width: 64px; height: 64px; border-radius: 20px; margin: 2px auto 14px; display: flex; align-items: center; justify-content: center; }
.dh-icirc-gold { background: var(--dh-gold-dim); color: var(--dh-gold); border: 1px solid var(--dh-gold-border); }
.dh-icirc-ok   { background: var(--dh-success-dim); color: var(--dh-success); border: 1px solid rgba(26,127,75,.2); }

/* misc loan-journey bits */
.dh-fineprint { font-size: 11px; color: var(--dh-muted); line-height: 1.6; }
.dh-consent { display: flex; align-items: flex-start; gap: 9px; font-size: 12.5px; color: var(--dh-soft); cursor: pointer; line-height: 1.5; }
.dh-consent input { width: 19px; height: 19px; margin-top: 1px; flex-shrink: 0; accent-color: var(--dh-gold); }
.dh-trust { display: flex; gap: 7px; flex-wrap: wrap; margin-top: 14px; }
.dh-loanerr  { background: var(--dh-error-dim); border: 1.5px solid rgba(217,64,64,.25); color: #A02020; padding: 11px 13px; border-radius: 13px; font-size: 13px; margin-bottom: 12px; }
.dh-loaninfo { background: var(--dh-gold-dim); border: 1px solid var(--dh-gold-border); color: #7A5D15; padding: 11px 13px; border-radius: 13px; font-size: 13px; margin-bottom: 12px; }
.dh-busy { text-align: center; padding: 30px 0; color: var(--dh-muted); font-size: 13px; }
.dh-schedrow { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 11px 2px; border-bottom: 1px solid var(--dh-border2); font-size: 13px; }
.dh-schedrow:last-child { border-bottom: none; }
.dh-schedscroll { max-height: 340px; overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; }
.dh-doclink { display: flex; align-items: center; gap: 10px; padding: 12px 2px; color: var(--dh-ink); font-weight: 600; font-size: 13.5px; text-decoration: none; border-bottom: 1px solid var(--dh-border2); cursor: pointer; }
.dh-doclink:last-child { border-bottom: none; }
.dh-doclink svg { color: var(--dh-gold); flex-shrink: 0; }

/* My Credit Score — KBA answer options */
.dh-csopts { display: flex; flex-direction: column; gap: 8px; }
.dh-csopt { display: block; width: 100%; text-align: left; padding: 13px 15px; border: 1.5px solid var(--dh-border); border-radius: 12px; background: var(--dh-bg); color: var(--dh-ink); font-size: 14px; font-family: inherit; cursor: pointer; transition: border-color .15s, background .15s; }
.dh-csopt:hover { border-color: var(--dh-gold-border); }
.dh-csopt.dh-on { border-color: var(--dh-gold); background: var(--dh-gold-dim); font-weight: 600; }

/* Profile avatar header (My Profile) */
.dh-avhead { display: flex; align-items: center; gap: 14px; }
.dh-avwrap { position: relative; flex-shrink: 0; }
.dh-av { width: 64px; height: 64px; border-radius: 50%; object-fit: cover; display: block; border: 2px solid var(--dh-gold-border); background: var(--dh-surface); }
.dh-av-ph { display: flex; align-items: center; justify-content: center; font-family: var(--dh-serif); font-weight: 700; font-size: 22px; color: var(--dh-gold); background: var(--dh-gold-dim); }
.dh-avcam { position: absolute; right: -3px; bottom: -3px; width: 26px; height: 26px; border-radius: 50%; background: var(--dh-gold); color: var(--dh-ink); border: 2px solid var(--dh-bg); display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; }
.dh-avmeta { min-width: 0; }
.dh-avname { font-weight: 700; font-size: 16px; color: var(--dh-ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ═══ Profile header — all of it INSIDE the band (2026-08-10) ══════════════
 *
 * Revised from the straddling version: the avatar, name and phone now sit
 * wholly in the cream band, so the header is one uninterrupted block and the
 * white sheet starts clean at the menu. Nothing crosses the boundary any more,
 * which is why .dh-profband cancels the straddle reserve the base band carries.
 *
 * The "Complete your profile" CARD is gone (Prem). Its number did not need a
 * card — it is now the ring drawn AROUND the avatar, which is the same
 * information in the space the photo already occupies. The KYC destination the
 * card used to open is still reachable: the caption under the phone, and the
 * "Identity & KYC" row in the menu below. */
.dh-profband {
  /* No straddle: the base band reserves --dh-hold-lift + 20 for a card rising
     into it, and nothing rises now. 44px leaves ~16px of clear cream under the
     caption once the sheet's -28px lift is subtracted. */
  padding-bottom: 44px;
  min-height: 0;
}
/* 104px box = the 88px avatar plus the ring track and its breathing room. */
.dh-profav {
  position: relative;
  width: 104px; height: 104px;
  margin: 0px auto 0;
}
/* The completion ring, OUTSIDE the photo. An SVG rather than a conic-gradient
 * border: the stroke gives a true round cap and animates from one attribute
 * (stroke-dashoffset), and it degrades to nothing rather than a square smear on
 * an engine without conic-gradient. Rotated -90deg so 0% starts at 12 o'clock. */
.dh-profring {
  position: absolute; inset: 0; width: 104px; height: 104px;
  transform: rotate(-90deg);
  pointer-events: none;
}
/* DIAGONAL barber-pole — the crossed-stripe reference. The stripes come from an
 * SVG <pattern> painted into the stroke (see _renderCustProfile), not from
 * stroke-dasharray: a dasharray can only cut ACROSS a stroke, giving
 * perpendicular ticks, never a 45° candy stripe.
 *
 * Only the COLOURS live here. Tile size, the 45° tilt and the marching
 * animation are attributes on the <pattern> itself, because patternTransform is
 * not animatable from CSS in the WebViews this ships to. Keep the two in step:
 * the animation slides exactly one tile (10) per cycle, and any other distance
 * makes the pattern jump on repeat. */
.dh-profring circle { fill: none; stroke-width: 5; stroke-linecap: round; }
/* The TRACK, warmed (2026-08-10). It was a neutral 9% ink — visible, but a grey
 * ring on a cream band next to a gold arc read as a foreign part. A 20% gold
 * tint keeps it clearly a track while putting it in the same family as the fill
 * it holds. */
.dh-profring .dh-profring-bg { stroke: rgba(201, 150, 42, .20); }
/* Fallback stroke for the completed (100%) ring, which is painted solid rather
 * than striped. Also what shows if the pattern reference ever fails to resolve,
 * so the arc degrades to plain gold rather than vanishing. */
.dh-profring .dh-profring-fg { stroke: var(--dh-gold, #C9962A); }
/* The two halves of the stripe tile. Styled by class so they track the palette
 * — fill="var(--dh-gold)" would not work as an SVG presentation attribute. */
.dh-profstripe-a { fill: var(--dh-gold, #C9962A); }
.dh-profstripe-b { fill: var(--dh-gold-light, #E8C96A); }
/* The sweep lives on the MASK circle, since that is what now encodes the
 * percentage. .9s, not .6: a longer ease lets a small percentage still read as
 * "filling up" rather than a flick, and the delay lets the avatar's pop land
 * first so the two do not compete. */
.dh-profring .dh-profring-mc {
  transition: stroke-dashoffset .9s cubic-bezier(.22, 1, .36, 1) .18s;
}

/* ── Header entrance ───────────────────────────────────────────────────────
 * Staggered by 60ms so the eye is led down the header — avatar, name, phone,
 * then the call to action — instead of everything arriving at once.
 * `both` holds the start frame, so nothing flashes at full opacity before its
 * delay elapses. */
@keyframes dhProfPop {
  from { opacity: 0; transform: scale(.90); }
  to   { opacity: 1; transform: scale(1); }
}
@keyframes dhProfRise {
  from { opacity: 0; transform: translateY(7px); }
  to   { opacity: 1; transform: translateY(0); }
}
.dh-profav      { animation: dhProfPop  .44s cubic-bezier(.22, 1, .36, 1) both; }
.dh-profname    { animation: dhProfRise .40s ease .08s both; }
.dh-profphone   { animation: dhProfRise .40s ease .14s both; }
.dh-profpctwrap { animation: dhProfRise .40s ease .20s both; }
/* The camera button rides the avatar's pop, then fades in on its own once the
 * circle has settled — it is a control, not part of the portrait. */
.dh-profav .dh-avcam { animation: dhProfPop .30s ease .34s both; }

@media (prefers-reduced-motion: reduce) {
  .dh-profav, .dh-profname, .dh-profphone,
  .dh-profpctwrap, .dh-profav .dh-avcam { animation: none; }
  /* The marching stripes are handled in JS, not here: SMIL animations ignore
     this media query entirely, so _renderCustProfile omits the
     <animateTransform> for these users. The diagonal stripes remain — they just
     hold still. */
  /* No sweep either — _profAnimateHeader still writes the real offset, so the
     ring simply appears at its correct value instead of filling. */
  .dh-profring .dh-profring-mc { transition: none; }
}
/* The photo, inset inside the ring. The cream band-coloured border is a GAP,
 * not a decoration: without it the photo's edge touches the ring and the two
 * read as one thick bezel, which is what made the arc hard to find. */
.dh-profav .dh-av,
.dh-profav .dh-av-ph {
  position: absolute; top: 10px; left: 10px;
  width: 84px; height: 84px; border-radius: 50%;
  border: 3px solid var(--dh-band, #f8f0e0);
  box-shadow: 0 4px 14px rgba(0, 0, 0, .12);
  box-sizing: border-box;
}
/* Initials fall back to the gold card ramp — the same surface the portfolio
 * card uses, so a customer with no photo still gets a branded avatar instead of
 * a pale tint that disappears against the cream. */
.dh-profav .dh-av-ph {
  background: var(--dh-goldcard); color: #fff; font-size: 30px;
}
/* Moved OUT of the photo to its bottom-right corner. Inside, a gold button on
 * the gold-ramp avatar was gold-on-gold — the icon was barely findable. On the
 * cream band, ringed in that same cream, it reads as a distinct control. */
.dh-profav .dh-avcam {
  right: -2px; bottom: 2px; width: 32px; height: 32px;
  background: var(--dh-gold, #C9962A); color: var(--dh-ink, #0A0A0A);
  border: 3px solid var(--dh-band, #f8f0e0);
  box-shadow: 0 3px 10px rgba(0, 0, 0, .22);
}
.dh-profav .dh-avcam:active { filter: brightness(.94); }
/* Replaces the completion card: one tappable chip that names the ring and still
 * opens Identity & KYC. A PILL, not an underlined link — underline is the
 * app's style for a quiet inline aside, and this is the header's only call to
 * action. Hidden at 100%: a finished profile has nothing to chase, and the ring
 * is a closed gold circle by then. */
.dh-profpct {
  display: inline-flex; align-items: center; gap: 5px;
  margin: 10px auto 0;
  padding: 6px 13px; border-radius: 999px;
  background: var(--dh-gold-dim, rgba(201,150,42,.10));
  border: 1px solid var(--dh-gold-border, rgba(201,150,42,.25));
  font-family: inherit; font-size: 12.5px; font-weight: 700;
  color: var(--gold-dark, #8A6305); cursor: pointer;
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}
.dh-profpct:active { background: var(--dh-gold-dim2, rgba(201,150,42,.18)); }
/* The pill is inline-flex, so `margin: auto` cannot centre it — it needs a
 * block-level line box to sit in. */
.dh-profpctwrap { text-align: center; }
.dh-profname {
  text-align: center; margin-top: 13px;
  font-family: var(--dh-serif); font-size: 20px; font-weight: 700;
  color: var(--dh-ink); line-height: 1.25;
  /* Two lines then ellipsis. A long name (or the test accounts, whose name
     carries the phone number) previously stretched the header indefinitely. */
  display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden; overflow-wrap: anywhere;
  padding: 0 20px;
}
.dh-profphone {
  text-align: center; margin-top: 3px;
  font-size: 13px; color: var(--dh-muted);
}

/* Edit-Profile bottom sheet: real flex column on mobile so the sticky footer
   never overlaps the form (the old max-height math left content tucked behind
   Cancel/Save). Plus iOS home-indicator safe-area padding. Desktop unchanged. */
@media (max-width: 768px){
  #profileEditModal .modal { display: flex; flex-direction: column; }
  #profileEditModal .modal-body { flex: 1 1 auto; min-height: 0; max-height: none; }
  #profileEditModal .modal-footer { padding-bottom: max(14px, env(safe-area-inset-bottom)); }
}

body.dh-customer .modal-overlay > .modal {
  display: flex;
  flex-direction: column;
}
/* Default: nothing shrinks — free-form cards behave as they always have. */
body.dh-customer .modal-overlay > .modal > * { flex: 0 0 auto; }
/* …except the body, which takes the leftover height and scrolls inside it. */
body.dh-customer .modal-overlay > .modal > .modal-body {
  flex: 1 1 auto;
  min-height: 0;
  /* Clears the ≤480px calc() so the flex line, not the arithmetic, sizes it. */
  max-height: none;
  /* Stated here because above 480px styles.css never gave the body an overflow
     at all — without this the flex column would size correctly and still clip. */
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  /* A flick that reaches the end of the form must not scroll the page behind. */
  overscroll-behavior: contain;
}

/* Side-by-side field rows get ONE gutter, not two (reported 2026-08-11: the gap
 * between Preferred Date and Preferred Time Slot is visibly bigger than the gap
 * between every other pair of fields).
 *
 * Two rules in styles.css both claim that space, and on a phone they add up:
 *   .form-group { margin-bottom: 16px }                      (:759)
 *   .form-row   { display:grid; grid-template-columns:1fr 1fr; gap:16px }  (:798)
 *   @media (max-width:768px) { .form-row { grid-template-columns: 1fr } }  (:1426)
 *
 * Above 768px the row is two columns, so the 16px gap is HORIZONTAL and the
 * margin is the only vertical space — correct. At or below 768px the row
 * collapses to one column and the gap becomes VERTICAL, landing on top of the
 * margin the fields still carry: 32px between Date and Time, 16px everywhere
 * else. Nothing in the markup says those two fields are special; they are just
 * the only pair in this form that happens to sit in a .form-row.
 *
 * The fix moves the row's outer spacing from its children to the row itself, so
 * the grid gap is the single source of spacing BETWEEN fields and the row's own
 * margin is the single source of spacing AFTER it. Net effect at every width:
 * every gap in the form is 16px. Two-column layouts are unchanged — the space
 * below the row is still 16px, it is just owned by the row now instead of by
 * whichever child happened to be tallest. */
body.dh-customer .form-row { margin-bottom: 16px; }
body.dh-customer .form-row > .form-group { margin-bottom: 0; }

/* A CHECKBOX IS NOT A TEXT FIELD (reported 2026-08-11: the "Make this my default
 * account" tick in Add Bank Account sits centred ABOVE its label instead of
 * beside it).
 *
 * styles.css:771 styles every input inside a .form-group as a text field —
 * `width: 100%`, 10/12 padding, a 1.5px border. A checkbox honours the width but
 * not the rest, and a native checkbox stretched to the full row draws its glyph
 * in the CENTRE of that box; .form-group label being `display:block` then puts
 * the caption on the next line. Result: a lone blue tick floating mid-row with
 * the text under it.
 *
 * UNSCOPED on purpose. This markup shape — `.form-group > label > input[checkbox]`
 * — appears five times: Add Bank Account (customer) plus the CIBIL-consent,
 * pull-again, gold market-open and bank-name-match rows on staff screens, all
 * broken the same way. Scoping to body.dh-customer would fix one and leave four.
 *
 * Sizing and accent follow .dh-consent (:2796), which is what the loan journey's
 * consent rows already use, so tick boxes look the same wherever they appear.
 * 19px also clears the elderly-friendly tap-target floor the customer scope sets.
 *
 * The base rule alone fixes the layout: with width back to auto the control is
 * inline-block again and sits on the text's line. The :has() rule below is a
 * refinement, not the fix — on an engine without :has() it is simply ignored. */
.form-group input[type="checkbox"],
.form-group input[type="radio"] {
  width: 19px; height: 19px;
  padding: 0;
  margin: 0 8px 0 0;
  vertical-align: -4px;              /* optical centre against 14px label text */
  flex: 0 0 auto;
  accent-color: var(--dh-gold, #C9962A);
  cursor: pointer;
}
/* Progressive enhancement: make the caption a proper row so a label that wraps
 * (e.g. "The customer has consented to this credit check") indents past the box
 * instead of running back under it. */
.form-group label:has(> input[type="checkbox"]),
.form-group label:has(> input[type="radio"]) {
  display: flex; align-items: flex-start; gap: 8px;
  margin-bottom: 0;                  /* it is the whole row, nothing follows it */
  cursor: pointer; line-height: 1.45;
}
.form-group label:has(> input[type="checkbox"]) > input,
.form-group label:has(> input[type="radio"]) > input { margin: 1px 0 0; vertical-align: baseline; }

/* ============================================================================
 * ELDERLY-FRIENDLY POLISH (added 2026-06-18)
 * The NCD customer base skews 60+. These rules bump readability + tap-target
 * size + contrast inside `body.dh-customer` only. Staff / prospect screens
 * are untouched. Pure CSS — no JS behaviour changes here.
 * ========================================================================== */

/* (1) BODY-TEXT SIZE — base bumps 14px → 16px across customer screens.
 *     16px is Apple HIG + Google Material's recommended minimum for readers
 *     60+. Fine-print classes bump 11px → 13px (still small but legible). */
body.dh-customer { font-size: 16px; }
body.dh-customer .dh-fineprint { font-size: 13px; line-height: 1.6; }
body.dh-customer .dh-cap { font-size: 12px; }
body.dh-customer .dh-stepcap { font-size: 12px; }
body.dh-customer .dh-pill { font-size: 12px; padding: 4px 11px; }
/* More-sheet row text — these are the most-tapped navigation surfaces. */
body.dh-customer .dh-sheet-row .dh-srlabel { font-size: 16px; }
body.dh-customer .dh-sheet-row .dh-srsub   { font-size: 13px; }
body.dh-customer #custMoreSheet .dh-sheet-hd  { font-size: 20px; }
body.dh-customer #custMoreSheet .dh-sheet-sub { font-size: 12px; }

/* (2) TAP-TARGET FLOORS — every interactive element ≥48×48 (Google a11y) and
 *     the most-tapped surfaces ≥56. Older hands miss small targets; this
 *     trades a little vertical density for fewer mistaps.                 */
body.dh-customer .dh-btn { min-height: 44pxpx; padding-top: 14px; padding-bottom: 14px; }
body.dh-customer .dh-sheet-row { min-height: 60px; padding-top: 16px; padding-bottom: 16px; }
body.dh-customer .dh-fchip,
body.dh-customer .dh-chip { min-height: 44px; }

/* (3) CONTRAST CLEANUP — `dh-faint` (#C4C4C4 on white) is 2.0:1 — fails
 *     WCAG AA (4.5:1 for body, 3:1 for large). Pull the faint tokens down
 *     INSIDE the customer scope so elderly users can actually read the
 *     fine print. Base tokens unchanged so flow chrome stays consistent.
 *     Old --dh-muted (#8A8A8A) was 4.7:1 (just passes); bumping to #6F6F6F
 *     gives a cleaner 5.8:1.                                              */
body.dh-customer {
  --dh-muted:     #6F6F6F;
  --dh-muted2:    #555555;
  --dh-faint:     #7A7A7A;
  --dh-faint2:    #5F5F5F;
}

/* (4) "CALL SUPPORT" FAB — sits ABOVE the existing chat FAB, ink-coloured.
 *     Phone is more familiar than chat for elderly users. Inserted into the
 *     DOM by _ensureCustShell() in index.html; styles live here so all
 *     visual tweaks ship in one place.                                   */
#custCallFab { display: none; }
body.dh-customer #custCallFab,
body.dh-prospect #custCallFab {
  /* Slot 2 of the arc — 155° at R 68, i.e. 62px inward and 29px up from the
     toggle. Same pre-JS-only caveat as #custChatFab above. */
  position: fixed; right: calc(var(--dh-fab-gutter, 16px) + 62px); z-index: 1199;
  bottom: calc(68px + env(safe-area-inset-bottom, 0px) + 16px + 29px);
  display: inline-flex; align-items: center; justify-content: center;
  width: 48px; height: 48px; padding: 0;
  border: none; border-radius: 50%;
  background: var(--dh-ink); color: #fff;
  cursor: grab; touch-action: none;
  box-shadow: 0 8px 22px rgba(0,0,0,.35), 0 2px 6px rgba(0,0,0,.18);
  transition: transform .12s ease, box-shadow .12s ease,
              left .18s ease, right .18s ease, top .18s ease, bottom .18s ease;
}
body.dh-customer #custCallFab:active,
body.dh-prospect #custCallFab:active { transform: scale(.96); }
body.dh-customer #custCallFab.dh-fab-dragging,
body.dh-prospect #custCallFab.dh-fab-dragging {
  cursor: grabbing; transform: scale(1.08);
  box-shadow: 0 14px 32px rgba(0,0,0,.45), 0 4px 10px rgba(0,0,0,.22);
  transition: none;
}
#custCallFab .dh-fabicon { display: inline-flex; }

/* (5) SUPPORT FAB GROUP — collapsed behind a single "+" toggle (2026-08-06).
 *     Two circles permanently parked over the content were covering cards and
 *     CTAs on every screen. Now only the toggle is on screen; it fans the Call
 *     + Chat buttons out on tap and the plus rotates 45° into an ×.
 *     Positions come from _applyFabCorner (index.html) — this block only owns
 *     the show/hide, so the group still drags to any of the four corners.     */
/*  COLOUR SCHEME — flat gold toggle, two gold-ramp actions.
 *  Before this, the stack was ink (Call) + gold (Chat) + a third circle, which
 *  read as three unrelated buttons. Now the hierarchy carries the meaning:
 *    · the toggle is the ONLY permanently visible control, so it wears the
 *      FLAT brand gold with an ink glyph (7.4:1 — comfortably AA);
 *    · Chat and Call are transient menu items, so they are identical
 *      --dh-goldcard circles with white glyphs and read as one group.
 *  The flat/ramp split is what keeps the toggle distinguishable from the two
 *  actions now that all three are gold — don't collapse them to one fill.
 *  Literal hex for the toggle rather than var(--dh-gold): these three buttons
 *  are appended straight to <body>, outside any token-scoping wrapper, and a
 *  FAB silently losing its background is exactly the kind of thing nobody
 *  notices in review. (--dh-goldcard is :root-level, so it is safe.)
 */
#custFabToggle { display: none; }
body.dh-customer #custFabToggle,
body.dh-prospect #custFabToggle {
  position: fixed; right: var(--dh-fab-gutter, 16px); z-index: 1201;   /* above both actions */
  bottom: calc(68px + env(safe-area-inset-bottom, 0px) + 16px);
  display: inline-flex; align-items: center; justify-content: center;
  width: 48px; height: 48px; padding: 0;
  border: none; border-radius: 50%;
  background: #C9962A; color: #0A0A0A;
  cursor: grab; touch-action: none;
  box-shadow: 0 8px 22px rgba(201,150,42,.42), 0 2px 6px rgba(0,0,0,.16);
  transition: transform .12s ease, box-shadow .12s ease,
              left .18s ease, right .18s ease, top .18s ease, bottom .18s ease;
}
body.dh-customer #custFabToggle:active,
body.dh-prospect #custFabToggle:active { transform: scale(.96); }
body.dh-customer #custFabToggle.dh-fab-dragging,
body.dh-prospect #custFabToggle.dh-fab-dragging {
  cursor: grabbing; transform: scale(1.08);
  box-shadow: 0 14px 32px rgba(201,150,42,.52), 0 4px 10px rgba(0,0,0,.20);
  transition: none;
}

/* Chat and Call share one surface. The base rule near the top of this file
 * still paints Chat flat gold — overridden here (later + same specificity) so
 * the whole FAB colour scheme stays readable in ONE place.
 *
 * The surface is --dh-goldcard (2026-08-11, Prem), the same ramp every hero
 * card in the app wears, so the two action buttons belong to the brand instead
 * of reading as generic ink circles. Unlike the literal hex the toggle uses,
 * the token is safe here: --dh-goldcard is declared on :root, not on a scoping
 * wrapper, so these body-level buttons resolve it.
 *
 * White glyphs stay. The ramp runs #5e4002 → #c9962a and white measures 9.5:1
 * on the dark stop / 2.66:1 on the gold one, but a 48px circle only crosses the
 * middle of that ramp and the glyph is a 1.7px-stroke icon, which is held to
 * the 3:1 non-text minimum, not 4.5:1. */
body.dh-customer #custChatFab,
body.dh-prospect #custChatFab,
body.dh-customer #custCallFab,
body.dh-prospect #custCallFab {
  background: var(--dh-goldcard); color: #FFFFFF;
  box-shadow: 0 8px 22px rgba(0,0,0,.32), 0 2px 6px rgba(0,0,0,.18);
}
body.dh-customer #custChatFab.dh-fab-dragging,
body.dh-prospect #custChatFab.dh-fab-dragging,
body.dh-customer #custCallFab.dh-fab-dragging,
body.dh-prospect #custCallFab.dh-fab-dragging {
  box-shadow: 0 14px 32px rgba(0,0,0,.45), 0 4px 10px rgba(0,0,0,.22);
}
body.dh-customer #custFabToggle:disabled,
body.dh-prospect #custFabToggle:disabled { opacity: .6; cursor: default; }
/* The plus IS the ×  — one glyph, rotated. */
#custFabToggle .dh-fabicon {
  color: #ffffff;
  display: inline-flex;
  transition: transform .22s cubic-bezier(.4, 0, .2, 1);
}
body.dh-fabs-open #custFabToggle .dh-fabicon { transform: rotate(45deg); }

/* Collapsed: both actions fold back ONTO the toggle and stop taking taps.
 *   --dh-fab-hide-x / -y are the per-button travel set by _applyFabCorner: the
 *   vector from where the action sits on the open ARC back to the toggle at the
 *   arc's centre. Both axes are needed because the actions fan out diagonally
 *   (Chat straight inward, Call swung ~65° toward the screen middle) — an
 *   earlier straight column only moved on Y. Signs flip in the left/top
 *   corners, where the fan grows the opposite way.                            */
body.dh-customer #custChatFab,
body.dh-prospect #custChatFab,
body.dh-customer #custCallFab,
body.dh-prospect #custCallFab {
  visibility: visible;
  transition: opacity .18s ease, transform .18s cubic-bezier(.4, 0, .2, 1),
              visibility 0s,
              box-shadow .12s ease,
              left .18s ease, right .18s ease, top .18s ease, bottom .18s ease;
}
/* `visibility` is here to keep folded buttons out of the tab order and off
 * screen readers — but it is not an animatable value, so it has to be HELD
 * until the fade finishes (`visibility 0s linear .18s`), otherwise the button
 * vanishes on frame 1 and the fold is never seen. Opening reverses it: the
 * 0s/no-delay entry above makes it visible immediately so the fan-out shows. */
body.dh-customer:not(.dh-fabs-open) #custChatFab,
body.dh-prospect:not(.dh-fabs-open) #custChatFab,
body.dh-customer:not(.dh-fabs-open) #custCallFab,
body.dh-prospect:not(.dh-fabs-open) #custCallFab {
  opacity: 0; visibility: hidden; pointer-events: none;
  transform: translate(var(--dh-fab-hide-x, 0px), var(--dh-fab-hide-y, 0px)) scale(.4);
  transition: opacity .18s ease, transform .18s cubic-bezier(.4, 0, .2, 1),
              visibility 0s linear .18s,
              box-shadow .12s ease,
              left .18s ease, right .18s ease, top .18s ease, bottom .18s ease;
}

/* A drag still needs to beat the fold transform, so re-assert the drag scale. */
body.dh-customer #custChatFab.dh-fab-dragging,
body.dh-prospect #custChatFab.dh-fab-dragging,
body.dh-customer #custCallFab.dh-fab-dragging,
body.dh-prospect #custCallFab.dh-fab-dragging {
  opacity: 1; visibility: visible; transform: scale(1.08);
}

/* Reduced motion: the folded/open states still apply, they just snap. The
 * `:not()` variants are repeated because the collapsed rule above is more
 * specific than the plain one — without them its transition would survive. */
@media (prefers-reduced-motion: reduce) {
  #custFabToggle .dh-fabicon,
  body.dh-customer #custChatFab,
  body.dh-prospect #custChatFab,
  body.dh-customer #custCallFab,
  body.dh-prospect #custCallFab,
  body.dh-customer:not(.dh-fabs-open) #custChatFab,
  body.dh-prospect:not(.dh-fabs-open) #custChatFab,
  body.dh-customer:not(.dh-fabs-open) #custCallFab,
  body.dh-prospect:not(.dh-fabs-open) #custCallFab { transition: none; }
}

/* ═════════════════════════════════════════════════════════════════════════════
 * PHASE 2: CSS UTILITY LIBRARY — extracting inline styles to reusable classes
 * These classes replace inline style="" attributes across customer screens.
 * All classes respect the Android parity scaling variables (--dh-font-scale, etc).
 * ═════════════════════════════════════════════════════════════════════════════ */

/* TYPOGRAPHY UTILITIES */
.dh-heading-xl { font-family: var(--dh-serif); font-size: calc(26px * var(--dh-font-scale)); font-weight: 700; color: var(--dh-ink); letter-spacing: -0.01em; line-height: 1.25; }
.dh-heading-lg { font-family: var(--dh-serif); font-size: calc(22px * var(--dh-font-scale)); font-weight: 700; color: var(--dh-ink); letter-spacing: -0.01em; line-height: 1.3; }
.dh-heading-md { font-family: var(--dh-serif); font-size: calc(18px * var(--dh-font-scale)); font-weight: 700; color: var(--dh-ink); letter-spacing: -0.01em; }
.dh-heading-sm { font-size: calc(16px * var(--dh-font-scale)); font-weight: 700; color: var(--dh-ink); }
.dh-body-lg { font-size: calc(16px * var(--dh-font-scale)); color: var(--dh-ink); line-height: 1.5; }
.dh-body-md { font-size: calc(14px * var(--dh-font-scale)); color: var(--dh-ink); line-height: 1.5; }
.dh-body-sm { font-size: calc(13px * var(--dh-font-scale)); color: var(--dh-ink); line-height: 1.5; }
.dh-body-xs { font-size: calc(12px * var(--dh-font-scale)); color: var(--dh-ink); line-height: 1.45; }
.dh-label { font-size: calc(13px * var(--dh-font-scale)); font-weight: 600; color: var(--dh-soft); }
.dh-label-sm { font-size: calc(11px * var(--dh-font-scale)); font-weight: 600; color: var(--dh-muted); text-transform: uppercase; letter-spacing: 0.05em; }
.dh-muted { color: var(--dh-muted); }
.dh-muted2 { color: var(--dh-muted2); }
.dh-gold { color: var(--dh-gold); }
.dh-success { color: var(--dh-success); }

/* SPACING UTILITIES */
.dh-m-0 { margin: 0; }
.dh-m-2 { margin: calc(2px * var(--dh-card-pad-scale)); }
.dh-m-4 { margin: calc(4px * var(--dh-card-pad-scale)); }
.dh-m-6 { margin: calc(6px * var(--dh-card-pad-scale)); }
.dh-m-8 { margin: calc(8px * var(--dh-card-pad-scale)); }
.dh-m-10 { margin: calc(10px * var(--dh-card-pad-scale)); }
.dh-m-12 { margin: calc(12px * var(--dh-card-pad-scale)); }
.dh-m-14 { margin: calc(14px * var(--dh-card-pad-scale)); }
.dh-m-16 { margin: calc(16px * var(--dh-card-pad-scale)); }
.dh-m-18 { margin: calc(18px * var(--dh-card-pad-scale)); }
.dh-m-20 { margin: calc(20px * var(--dh-card-pad-scale)); }
.dh-m-24 { margin: calc(24px * var(--dh-card-pad-scale)); }
.dh-m-32 { margin: calc(32px * var(--dh-card-pad-scale)); }
.dh-mb-4 { margin-bottom: calc(4px * var(--dh-card-pad-scale)); }
.dh-mb-6 { margin-bottom: calc(6px * var(--dh-card-pad-scale)); }
.dh-mb-8 { margin-bottom: calc(8px * var(--dh-card-pad-scale)); }
.dh-mb-10 { margin-bottom: calc(10px * var(--dh-card-pad-scale)); }
.dh-mb-12 { margin-bottom: calc(12px * var(--dh-card-pad-scale)); }
.dh-mb-14 { margin-bottom: calc(14px * var(--dh-card-pad-scale)); }
.dh-mb-16 { margin-bottom: calc(16px * var(--dh-card-pad-scale)); }
.dh-mb-18 { margin-bottom: calc(18px * var(--dh-card-pad-scale)); }
.dh-mb-20 { margin-bottom: calc(20px * var(--dh-card-pad-scale)); }
.dh-mb-24 { margin-bottom: calc(24px * var(--dh-card-pad-scale)); }
.dh-mt-4 { margin-top: calc(4px * var(--dh-card-pad-scale)); }
.dh-mt-6 { margin-top: calc(6px * var(--dh-card-pad-scale)); }
.dh-mt-8 { margin-top: calc(8px * var(--dh-card-pad-scale)); }
.dh-mt-10 { margin-top: calc(10px * var(--dh-card-pad-scale)); }
.dh-mt-12 { margin-top: calc(12px * var(--dh-card-pad-scale)); }
.dh-mt-14 { margin-top: calc(14px * var(--dh-card-pad-scale)); }
.dh-mt-16 { margin-top: calc(16px * var(--dh-card-pad-scale)); }
.dh-mt-18 { margin-top: calc(18px * var(--dh-card-pad-scale)); }
.dh-mt-20 { margin-top: calc(20px * var(--dh-card-pad-scale)); }
.dh-mt-24 { margin-top: calc(24px * var(--dh-card-pad-scale)); }
.dh-ml-auto { margin-left: auto; }
.dh-mr-auto { margin-right: auto; }
.dh-py-6 { padding-top: calc(6px * var(--dh-card-pad-scale)); padding-bottom: calc(6px * var(--dh-card-pad-scale)); }
.dh-py-8 { padding-top: calc(8px * var(--dh-card-pad-scale)); padding-bottom: calc(8px * var(--dh-card-pad-scale)); }
.dh-py-10 { padding-top: calc(10px * var(--dh-card-pad-scale)); padding-bottom: calc(10px * var(--dh-card-pad-scale)); }
.dh-py-12 { padding-top: calc(12px * var(--dh-card-pad-scale)); padding-bottom: calc(12px * var(--dh-card-pad-scale)); }
.dh-pt-8 { padding-top: calc(8px * var(--dh-card-pad-scale)); }
.dh-pt-12 { padding-top: calc(12px * var(--dh-card-pad-scale)); }
.dh-pt-16 { padding-top: calc(16px * var(--dh-card-pad-scale)); }
.dh-pb-8 { padding-bottom: calc(8px * var(--dh-card-pad-scale)); }
.dh-pb-12 { padding-bottom: calc(12px * var(--dh-card-pad-scale)); }
.dh-pb-16 { padding-bottom: calc(16px * var(--dh-card-pad-scale)); }
.dh-p-8 { padding: calc(8px * var(--dh-card-pad-scale)); }
.dh-p-10 { padding: calc(10px * var(--dh-card-pad-scale)); }
.dh-p-12 { padding: calc(12px * var(--dh-card-pad-scale)); }
.dh-p-14 { padding: calc(14px * var(--dh-card-pad-scale)); }
.dh-p-16 { padding: calc(16px * var(--dh-card-pad-scale)); }
.dh-p-18 { padding: calc(18px * var(--dh-card-pad-scale)); }
.dh-p-20 { padding: calc(20px * var(--dh-card-pad-scale)); }
.dh-p-22 { padding: calc(22px * var(--dh-card-pad-scale)); }
.dh-px-12 { padding-left: calc(12px * var(--dh-card-pad-scale)); padding-right: calc(12px * var(--dh-card-pad-scale)); }
.dh-px-14 { padding-left: calc(14px * var(--dh-card-pad-scale)); padding-right: calc(14px * var(--dh-card-pad-scale)); }
.dh-px-16 { padding-left: calc(16px * var(--dh-card-pad-scale)); padding-right: calc(16px * var(--dh-card-pad-scale)); }

/* LAYOUT & FLEXBOX */
.dh-flex { display: flex; }
.dh-flex-col { display: flex; flex-direction: column; }
.dh-flex-between { display: flex; justify-content: space-between; align-items: center; }
.dh-flex-center { display: flex; align-items: center; justify-content: center; }
.dh-flex-left { display: flex; align-items: center; }
.dh-flex-wrap { display: flex; flex-wrap: wrap; }
.dh-gap-4 { gap: calc(4px * var(--dh-card-pad-scale)); }
.dh-gap-6 { gap: calc(6px * var(--dh-card-pad-scale)); }
.dh-gap-8 { gap: calc(8px * var(--dh-card-pad-scale)); }
.dh-gap-10 { gap: calc(10px * var(--dh-card-pad-scale)); }
.dh-gap-12 { gap: calc(12px * var(--dh-card-pad-scale)); }
.dh-gap-14 { gap: calc(14px * var(--dh-card-pad-scale)); }
.dh-gap-16 { gap: calc(16px * var(--dh-card-pad-scale)); }
.dh-flex-1 { flex: 1; }
.dh-flex-none { flex-shrink: 0; }
.dh-min-w-0 { min-width: 0; }

/* CARDS & CONTAINERS */
.dh-card-base { border-radius: 18px; box-shadow: 0 2px 8px rgba(0,0,0,0.04); }
.dh-card-padded { padding: calc(18px * var(--dh-card-pad-scale)); }
.dh-card-dark { background: var(--dh-cardgrad); color: var(--dh-oncard-muted); }
.dh-card-light { background: var(--dh-gold-dim); border-color: var(--dh-gold-border); border: 1.5px solid var(--dh-gold-border); }
.dh-card-bordered { border: 1.5px solid var(--dh-border2); }
.dh-card-clickable { cursor: pointer; }

/* SECTION HEADERS */
.dh-section-header { margin-bottom: calc(14px * var(--dh-card-pad-scale)); }
.dh-section-title { font-size: calc(13px * var(--dh-font-scale)); font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: var(--dh-muted); margin: calc(8px * var(--dh-card-pad-scale)) calc(4px * var(--dh-card-pad-scale)) calc(12px * var(--dh-card-pad-scale)); }

/* STATS & VALUES */
.dh-stat-container { display: flex; flex-direction: column; }
.dh-stat-label { font-size: calc(11px * var(--dh-font-scale)); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--dh-muted); margin-bottom: calc(4px * var(--dh-card-pad-scale)); }
.dh-stat-value { font-size: calc(24px * var(--dh-font-scale)); font-weight: 700; color: var(--dh-ink); line-height: 1.2; }
.dh-stat-value-sm { font-size: calc(18px * var(--dh-font-scale)); font-weight: 700; color: var(--dh-ink); }
.dh-stat-delta { font-size: calc(12px * var(--dh-font-scale)); font-weight: 600; color: var(--dh-success); margin-top: calc(4px * var(--dh-card-pad-scale)); }
.dh-stat-divider { width: 1px; height: auto; background: rgba(0,0,0,0.08); }

/* BUTTONS & CONTROLS */
.dh-btn-base { border: none; border-radius: 14px; font-weight: 700; font-family: inherit; cursor: pointer; transition: all 0.15s; }
.dh-btn-primary { background: var(--dh-gold); color: var(--dh-ink); box-shadow: 0 4px 16px rgba(201,150,42,0.30); }
.dh-btn-primary:hover { filter: brightness(0.97); }
.dh-btn-secondary { background: transparent; color: var(--dh-ink); border: 1.5px solid var(--dh-border); }
.dh-btn-secondary:hover { border-color: var(--dh-gold-border); }

/* LINE HEIGHT OVERRIDES FOR ANDROID */
.dh-line-tight { line-height: var(--dh-line-height-tight); letter-spacing: var(--dh-letter-spacing-tight); }
.dh-line-normal { line-height: var(--dh-line-height-normal); letter-spacing: var(--dh-letter-spacing-tight); }

/* TEXT ALIGNMENT */
.dh-text-left { text-align: left; }
.dh-text-center { text-align: center; }
.dh-text-right { text-align: right; }

/* VISIBILITY */
.dh-hidden { display: none !important; }
.dh-visible { display: block; }

/* BANNER & ALERT STYLES */
.dh-alert-banner {
  background: var(--dh-warn-bg);
  border-left: 4px solid var(--dh-warn-line);
  border-radius: 8px;
  padding: calc(12px * var(--dh-card-pad-scale)) calc(14px * var(--dh-card-pad-scale));
  margin-bottom: calc(14px * var(--dh-card-pad-scale));
  font-size: calc(13px * var(--dh-font-scale));
  color: #78350f;
  display: flex;
  align-items: center;
  gap: calc(10px * var(--dh-card-pad-scale));
  flex-wrap: wrap;
}
.dh-alert-banner strong { font-weight: 700; }
.dh-alert-banner button {
  margin-left: auto;
  background: #f59e0b;
  color: #fff;
  border: none;
  padding: calc(6px * var(--dh-card-pad-scale)) calc(14px * var(--dh-card-pad-scale));
  border-radius: 6px;
  font-size: calc(12px * var(--dh-font-scale));
  font-weight: 600;
  cursor: pointer;
}

/* PRODUCT HUB STYLES */
.dh-prodhub-section {
  font-size: calc(12px * var(--dh-font-scale));
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--dh-muted);
  margin: calc(8px * var(--dh-card-pad-scale)) calc(4px * var(--dh-card-pad-scale)) calc(12px * var(--dh-card-pad-scale));
}
.dh-prodhub-note {
  background: var(--dh-surface);
  border: 1px solid var(--dh-border);
  border-radius: 12px;
  padding: calc(11px * var(--dh-card-pad-scale)) calc(14px * var(--dh-card-pad-scale));
  margin-bottom: calc(14px * var(--dh-card-pad-scale));
  font-size: calc(12px * var(--dh-font-scale));
  color: var(--dh-muted2);
  line-height: var(--dh-line-height-normal);
}
/* Product hub stays 2-up at EVERY width — the 1-column phone fallback
   added in PR #305 made each card full-width and turned the dashboard
   into a long unprofessional vertical scroll (one product per screen).
   On narrow phones the gap tightens via --dh-card-pad-scale; we never
   collapse to a single column. */
.product-hub-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: calc(12px * var(--dh-card-pad-scale)); }
.dh-prodhub-cta {
  display: flex;
  align-items: center;
  gap: calc(6px * var(--dh-card-pad-scale));
  font-weight: 700;
  color: var(--dh-gold);
  /* Push the CTA + the "Know more" below it to the bottom of the card so the
     footer lines up across every card in a row regardless of how much chip/
     title content sits above it (NCD's badge+rate vs Gold's one-line tag). */
  margin-top: auto;
  padding-top: calc(10px * var(--dh-card-pad-scale));
}
/* "Coming soon" cards have no CTA — bottom-pin their "Know more" directly. */
.dh-prodhubcard-soon .dh-prodhub-more { margin-top: auto; }
.dh-prodhub-more {
  background: transparent;
  border: none;
  color: var(--dh-muted2);
  font-size: calc(12px * var(--dh-font-scale));
  font-weight: 700;
  padding: calc(6px * var(--dh-card-pad-scale)) 0px;
  margin-top: calc(8px * var(--dh-card-pad-scale));
  cursor: pointer;
  text-decoration: underline;
}
.dh-prodhub-tag {
  display: flex;
  align-items: center;
  gap: calc(5px * var(--dh-card-pad-scale));
  flex-wrap: nowrap;
  /* Keep the chip on a single line — the badge ("Open now") and the rate/tag
     text ("Up to 13% p.a.") must not wrap or get shrunk into a wrap. */
  white-space: nowrap;
  font-size: calc(12px * var(--dh-font-scale));
}
.dh-prodhub-tag > * { flex: 0 0 auto; }

/* BADGE STYLES */
.dh-badge {
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
  gap: calc(4px * var(--dh-card-pad-scale));
  background: var(--dh-gold-dim);
  color: var(--dh-gold);
  padding: calc(3px * var(--dh-card-pad-scale)) calc(8px * var(--dh-card-pad-scale));
  border-radius: 6px;
  font-size: calc(11px * var(--dh-font-scale));
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

/* PRODUCT HUB CTA SECTION */
.dh-prodhub-footer {
  text-align: center;
  margin: calc(18px * var(--dh-card-pad-scale)) auto 0;
  padding: calc(16px * var(--dh-card-pad-scale));
  background: var(--dh-surface);
  border: 1px dashed var(--dh-border);
  border-radius: 14px;
}
.dh-prodhub-footer-text {
  font-size: calc(13px * var(--dh-font-scale));
  color: var(--dh-muted);
  margin-bottom: calc(10px * var(--dh-card-pad-scale));
}
.dh-prodhub-footer button {
  display: inline-flex;
  width: auto;
  padding: calc(11px * var(--dh-card-pad-scale)) calc(22px * var(--dh-card-pad-scale));
  background: var(--dh-gold);
  border-color: transparent;
  color: var(--dh-ink);
  box-shadow: 0 4px 14px rgba(201,150,42,.28);
}

/* ============================================================================
 * Collapsible NCD statement (per holding) — closed by default, native <details>
 * so it works on every WebView without JS. Chevron rotates 90° on open.
 * ========================================================================== */
.dh-ncdstmt > summary { outline: none; }
.dh-ncdstmt > summary::-webkit-details-marker { display: none; }
.dh-ncdstmt > summary::marker { content: ''; }
.dh-ncdstmt[open] > summary .dh-ncdstmt-chev { transform: rotate(90deg); }

/* ============================================================================
 * WIDE-PHONE / FOLDABLE FIX (added 2026-06-22)
 *
 * The Samsung Z Fold 5 inner screen is ~582px CSS wide in portrait — above
 * the 480px mobile breakpoint, so it triggers "tablet" rules even though the
 * device is a phone. Two problems fixed here:
 *
 * 1. PAYMENT CARDS: @media (min-width:481px) in styles.css hides
 *    .payment-cards-wrap and shows a table. Customer screens should always
 *    use the card layout — override unconditionally.
 *
 * 2. LAYOUT CAP: above 540px we cap the customer content column at 480px
 *    centred, matching a normal phone. Also caps the fixed bottom nav so it
 *    doesn't span the full ~582px.
 * ========================================================================== */

/* Fix 1 — always show mobile card layout on customer payment screen. */
body.dh-customer .payment-cards-wrap { display: block !important; }
body.dh-customer #custPaymentsTableWrap,
body.dh-customer #custPaymentsTableEl { display: none !important; }

/* Fix 2 — cap customer layout to 480px centred on screens wider than 540px
   (Z Fold inner, small tablets). Staff/prospect screens are unaffected. */
@media (min-width: 540px) {
  body.dh-customer .main-content {
    max-width: 480px;
    margin-left: auto !important;
    margin-right: auto;
  }
  /* Every sheet tracks the same 480px column so none of them overhangs it. */
  .dh-cfsheet > .modal-content { max-width: 480px !important; }
  body.dh-customer .modal-overlay > .modal { max-width: 480px; }
  body.dh-customer #appPopup > div,
  body.dh-customer #feedbackModal > div,
  body.dh-customer #newLockerReqModal > div,
  body.dh-customer #forceCustPwdModal > div { max-width: 480px !important; }
  /* Bottom nav is position:fixed so needs its own centering rule. It stays a
     floating pill here — only the width and centering change. */
  body.dh-customer #custBottomNav {
    width: 456px;
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    border-radius: 9999px;
  }
  /* More-sheet scrim and sheet-card are already full-screen — keep them so. */
  /* Scrollable sheet card: cap to 480px centred for visual consistency. */
  body.dh-customer #custMoreSheet .dh-sheet {
    max-width: 480px;
    margin-left: auto;
    margin-right: auto;
    border-radius: 20px 20px 0 0;
  }
}

/* — Android WebView modal-open flash fix (belt-and-braces) —
   Pre-promote customer modal overlays + their card onto their own compositing
   layer so Android System WebView (Chromium) doesn't lazily promote the layer
   on the first slide-up frame — lazy promotion repaints the page underneath
   for one frame, which reads as a flash when a Know-more / Book-a-visit modal
   opens. iOS WKWebView already pre-promotes these, so this only brings Android
   in line. Pairs with the JS MutationObserver fix that stops the auto-fit
   reflow on modal insertion (the primary cause). Cosmetic / compositing-only. */
body.dh-customer .modal-overlay {
  transform: translateZ(0);
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}
/* `will-change: transform, opacity` was here too, and it is REMOVED: it pinned
   the modal card on a permanent compositing layer of its own, and text drawn
   into a composited layer loses subpixel antialiasing — which is what made the
   whole sheet (headings, body copy, labels) read as slightly blurry. The flash
   fix does not depend on it: pre-promotion of the OVERLAY above is what stops
   Android's lazy layer promotion on the first slide-up frame, and that rule is
   untouched. Keep backface-visibility, which costs nothing here. */
body.dh-customer .modal {
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}

/* ──────────────────────────────────────────────────────────────────────────
 * TABLET / GALAXY FOLD / WIDE-VIEWPORT SUPPORT (added 2026-06-25)
 *
 * The customer SPA is designed for a phone (375–430px). On tablets (iPad,
 * Galaxy Tab) and Galaxy Fold INNER displays (~720–884px wide unfolded),
 * stretching edge-to-edge looks ugly. We cap the content + topbar inner
 * width and center, so the experience feels native at any size.
 *
 * Breakpoints:
 *   < 720px  → phone (existing edge-to-edge layout, untouched)
 *   ≥ 720px  → tablet / Fold-inner: cap content at 520px (centered)
 *   ≥ 1100px → larger tablet / desktop: same cap, slightly more breathing
 *
 * Login screen (pre-body.dh-customer) is handled with its own media query
 * so it works for prospects + staff too.
 * ────────────────────────────────────────────────────────────────────────── */
@media (min-width: 720px) {
  /* The full-bleed surfaces (topbar, bottom nav) stay edge-to-edge for
     the typical app look, but their INNER row is capped + centered so
     the logo + chip don't drift apart on a wide screen. */
  body.dh-customer .topbar {
    padding-left: max(16px, calc((100vw - 520px) / 2)) !important;
    padding-right: max(16px, calc((100vw - 520px) / 2)) !important;
  }
  body.dh-customer .content-area {
    max-width: 520px;
    margin: 0 auto;
  }
  /* Sheets track the same 520px column (see the 540px block above). */
  .dh-cfsheet > .modal-content { max-width: 520px !important; }
  body.dh-customer .modal-overlay > .modal { max-width: 520px; }
  body.dh-customer #appPopup > div,
  body.dh-customer #feedbackModal > div,
  body.dh-customer #newLockerReqModal > div,
  body.dh-customer #forceCustPwdModal > div { max-width: 520px !important; }
  body.dh-customer #custBottomNav {
    width: 496px;
    max-width: calc(100vw - 24px);
    left: 50% !important;
    right: auto;
    transform: translateX(-50%);
    border-radius: 9999px;
  }
  /* The FAB group anchors near the app's 520px column, not the screen edge.
     Moving the GUTTER TOKEN rather than setting `right` directly is what lets
     the open arc survive here: _applyFabCorner writes each button's anchor as
     `calc(var(--dh-fab-gutter) + <its own inward offset>)`, so the offsets
     still add on top. The previous rule set `right` with !important on Chat and
     Call only, which (a) flattened the arc back to a column on wide screens,
     (b) left the toggle behind at the screen edge, splitting the group, and
     (c) forced a right anchor even when the customer had parked the group in a
     LEFT corner. */
  body.dh-customer { --dh-fab-gutter: max(16px, calc((100vw - 520px) / 2 + 16px)); }
  /* Login + prospect screens (pre body.dh-customer) — keep the form sane. */
  #loginScreen .login-box,
  #prospectScreen .prospect-container {
    max-width: 480px;
    margin: 0 auto;
  }
}

/* On very narrow Fold COVER displays (~280–376px wide, ratio ~1:3) the pill
   needs to hug the edges a little tighter and the raised centre circle shrinks
   so the four side icons keep their tap targets. Labels are already dropped in
   the pill layout, so there is nothing to hide here. */
@media (max-width: 360px) {
  body.dh-customer #custBottomNav { padding-left: 2px; padding-right: 2px; }
  body.dh-customer #custBottomNav .dh-nicon { width: 48px; height: 48px; }
  body.dh-customer #custBottomNav .dh-navbtn.dh-active .dh-nicon {
    transform: translateY(-13px);
  }
  body.dh-customer #custBottomNav .dh-navbtn.dh-active:active .dh-nicon {
    transform: translateY(-13px) scale(.93);
  }
  body.dh-customer #custBottomNav .dh-navbtn.dh-active::before {
    top: -17px; width: 48px; height: 52px; margin-left: -24px; border-radius: 24px;
  }
}

/* ===========================================================================
 * iOS: stop the login screen auto-zooming on focus.
 *
 * ROOT CAUSE: Mobile Safari / WKWebView zoom the page in whenever a focused
 * form control has a computed font-size BELOW 16px. `styles.css` sets
 * `.login-input-wrap input { font-size: 15px !important }`, so tapping the
 * phone/username field zoomed the page and the customer had to pinch back out.
 * The viewport meta is fine (`width=device-width, initial-scale=1`) and
 * deliberately still allows pinch-zoom, so suppressing zoom via
 * `maximum-scale=1` / `user-scalable=no` is NOT the fix — that would break
 * accessibility for everyone on every screen.
 *
 * 16px is the exact threshold, so this is a 1px bump on iOS only.
 * `@supports (-webkit-touch-callout: none)` is true on iOS Safari/WKWebView and
 * false on Android Chrome and every desktop browser, so Android and desktop
 * keep the designed 15px untouched.
 *
 * Lives here (Claude-owned) rather than in styles.css (Emergent-owned) per
 * OWNERSHIP.md; dhanam-app.css loads last, so this !important wins the cascade.
 * ======================================================================== */
@supports (-webkit-touch-callout: none) {
  .login-input-wrap input,
  #loginScreen input,
  #loginScreen select,
  #loginScreen textarea {
    font-size: 16px !important;
  }
}

/* ===========================================================================
 * AI welcome suggestions — a floating overlay that emerges from the chat FAB.
 *
 * Deliberately NOT part of the Home screen markup: the whole thing is appended
 * to <body> and fixed-positioned, so no existing card, banner or section moves
 * and Home keeps working underneath. No backdrop, no dimming, no modal.
 *
 * Anchoring mirrors the FAB pair (which is draggable to any of the four
 * corners) — JS sets the left/right/top/bottom inline, matching _applyFabCorner.
 * ======================================================================== */
#aiWelcomeWrap { display: none; }
body.dh-customer #aiWelcomeWrap {
  display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
  position: fixed; z-index: 1260;           /* above the scrim (1250) — see the scrim block */
  max-width: min(300px, calc(100vw - 32px));
  pointer-events: none;                      /* only the cards catch taps */
}
body.dh-customer #aiWelcomeWrap.is-left { align-items: flex-start; }
#aiWelcomeWrap > * { pointer-events: auto; }

/* Greeting bubble */
.dh-aigreet {
  background: var(--dh-ink, #0A0A0A); color: #fff;
  border-radius: 16px 16px 16px 6px;
  padding: 12px 14px; max-width: 100%;
  box-shadow: 0 10px 28px rgba(0,0,0,.22);
  display: flex; align-items: flex-start; gap: 10px;
}
#aiWelcomeWrap.is-left .dh-aigreet { border-radius: 16px 16px 6px 16px; }
.dh-aigreet-txt { font-size: 13px; line-height: 1.45; }
.dh-aigreet-txt b { display: block; font-weight: 700; margin-bottom: 2px; }
.dh-aigreet-x {
  flex: none; background: none; border: none; cursor: pointer;
  color: rgba(255,255,255,.65); font-size: 18px; line-height: 1;
  padding: 0 2px; margin: -2px -2px 0 0; font-family: inherit;
}
.dh-aigreet-x:hover { color: #fff; }

/* Suggestion chips — independent rounded cards, white + gold accent */
.dh-aichip {
  display: flex; align-items: center; gap: 10px; width: 100%;
  background: #fff; border: 1px solid var(--dh-gold-border, rgba(201,150,42,.28));
  border-left: 3px solid var(--dh-gold, #C9962A);
  border-radius: 14px; padding: 10px 12px; cursor: pointer;
  font-family: inherit; font-size: 12.5px; font-weight: 600;
  color: var(--dh-ink, #0A0A0A); text-align: left;
  box-shadow: 0 6px 18px rgba(0,0,0,.10);
  transition: transform .12s ease, box-shadow .12s ease;
}
.dh-aichip:active { transform: scale(.98); }
/* Product icon. Holds an inline _dhSvg (not an emoji — the customer re-skin
 * renders every icon as SVG so it looks the same on every device). The SVG
 * strokes with currentColor, so the gold here is what tints it, matching the
 * chip's gold left accent and its chevron. Class name kept for continuity. */
.dh-aichip-emoji {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  color: var(--dh-gold, #C9962A);
}
.dh-aichip-txt { flex: 1; min-width: 0; line-height: 1.35; }
.dh-aichip-arrow { flex: none; color: var(--dh-gold, #C9962A); display: inline-flex; }
.dh-ainotnow {
  background: none; border: none; cursor: pointer; font-family: inherit;
  font-size: 12px; font-weight: 600; color: var(--dh-muted, #8A8A8A);
  padding: 2px 6px;
}

/* Fade + slide in from the FAB, staggered per item by JS.
 *
 * A TRANSITION, not an animation. If a WebView never runs it, a transition
 * simply applies the end state instantly — the overlay is visible, just
 * un-animated. A keyframe animation can strand the element in its from-state
 * (opacity:0) while it still catches taps, which is a far worse failure.
 * JS adds .is-shown per child on a setTimeout ladder (timers fire even when
 * requestAnimationFrame is deferred in a backgrounded view).
 */
#aiWelcomeWrap > * {
  opacity: 0; transform: translateY(10px) scale(.97);
  transition: opacity .24s ease, transform .24s cubic-bezier(.2,.8,.3,1);
}
#aiWelcomeWrap > *.is-shown { opacity: 1; transform: none; }
#aiWelcomeWrap.is-out { opacity: 0; transform: translateY(8px); transition: opacity .18s ease, transform .18s ease; }

@media (prefers-reduced-motion: reduce) {
  #aiWelcomeWrap > * { transition: none; }
  #aiWelcomeWrap.is-out { transition: none; }
}

/* ===========================================================================
 * Top edge shadow — mirrors the bottom nav's existing one.
 *
 * The "shadow at the bottom of the screen" is the bottom nav casting onto the
 * content. The topbar was explicitly flat (styles.css sets box-shadow:none),
 * which is why the top edge had no matching depth.
 *
 * NOTE (2026-08-06): the nav became a detached floating pill with its own
 * all-round shadow, so this is no longer a literal mirror of a single
 * upward-cast shadow — it is kept as the topbar's own edge treatment, tuned to
 * the same colour/opacity family so the two edges still read as one system.
 *
 * This gives the topbar the SAME shadow with the y-offset negated, so colour,
 * opacity, blur and spread are identical and the two edges read as one
 * treatment. Both breakpoints are mirrored so they stay in step.
 *
 * Shadow ONLY — no border-bottom. The bottom nav pairs its shadow with a 1px
 * border-top, but adding a border here would occupy a pixel of layout and push
 * the content down; the brief was explicitly "no layout/spacing change".
 *
 * Nothing else moves: box-shadow paints outside the box, never affects layout,
 * and never captures pointer events, so the notification bell, profile chip and
 * every touch target behave exactly as before. The shadow points DOWNWARD, away
 * from the status bar / safe-area inset the topbar already pads for.
 *
 * Scoped to body.dh-customer — the same scope as the bottom nav it mirrors, so
 * the pair is consistent on every customer screen rather than appearing and
 * disappearing during navigation. Staff screens are untouched.
 *
 * UPDATE (2026-08-08, Prem): two changes.
 *
 * 1. Background is the warm cream #f8f0e0 (styles.css sets #ffffff; that file
 *    is Emergent-owned, so the customer value is overridden here instead).
 *
 * 2. The shadow is now ON SCROLL ONLY, not permanent. At rest the topbar sits
 *    flush against the page — with the cream background there is no longer a
 *    colour step at the top edge for a shadow to reinforce, so a permanent one
 *    just reads as grime. It appears the moment the content moves under it,
 *    which is when a shadow actually says something: content is passing
 *    beneath this bar.
 *
 *    `.dh-scrolled` is toggled on <body> by _custBindTopShadow() in
 *    index.html, driven by .content-area's scrollTop (that element is the app's
 *    only scroller — see _scrollAppTop). Still box-shadow only, so the "no
 *    layout/spacing change" rule above holds: nothing reflows when it appears.
 * ======================================================================== */
body.dh-customer .topbar {
  background: var(--dh-band, #f8f0e0);
  box-shadow: none;
  transition: box-shadow .2s ease, background-color .2s ease;
}
/* COLOUR and SHADOW are driven by two different classes (2026-08-10, Prem).
 * They used to share `.dh-scrolled`, which tied the header's colour to a flat
 * 50px of scroll — so on a page with no cream band the bar sat cream above
 * white content until you happened to scroll far enough, and on a banded page
 * it turned white while the band was still under it.
 *
 * `.dh-hdrwhite` is geometric instead: _custBindTopShadow sets it when the
 * white .dh-homesheet actually reaches the bar's bottom edge, and immediately
 * on any screen that has no sheet at all. The header therefore always carries
 * the colour of the surface directly beneath it. */
body.dh-customer.dh-hdrwhite .topbar {
  background-color: #ffffff;
}
/* The shadow (2026-08-11, Prem): on a banded screen `.dh-scrolled` is now set
 * by the SAME geometric test as `.dh-hdrwhite`, so the white sheet meeting the
 * bar turns it white and lifts it off the page in one frame. Screens with no
 * band keep the flat 50px scroll trigger — see _custBindTopShadow. Both classes
 * are toggled there; this file only paints them. */
body.dh-customer.dh-scrolled .topbar {
  box-shadow: 0 4px 18px rgba(0,0,0,.06);
}
@media (min-width: 540px) {
  body.dh-customer.dh-scrolled .topbar {
    box-shadow: 0 4px 24px rgba(0,0,0,.10);
  }
}
/* The shadow is decorative — its fade carries no information. */
@media (prefers-reduced-motion: reduce) {
  body.dh-customer .topbar { transition: none; }
}

/* ===========================================================================
 * AI suggestions — dimming backdrop.
 *
 * z-index is the whole design here. The existing stack is content < bottom nav
 * (1200) = chat FAB (1200). To read as a modal, the scrim has to sit ABOVE all
 * of that, with the popup above the scrim:
 *     scrim 1250  →  popup 1260   (still under #custMoreSheet at 1300)
 * The popup was 1150; raising it to 1260 is the only change to its own rule.
 *
 * The scrim is a separate fixed element rather than a ::before on the wrapper,
 * because the wrapper is pointer-events:none and sized to its content — it
 * could never cover the screen or catch an outside tap.
 *
 * Fade only. No blur (expensive to composite on mid-range Android and it would
 * change how Home *looks*, not just how bright it is), no transform, no layout
 * property — so nothing on Home can shift.
 * ======================================================================== */
#aiWelcomeScrim { display: none; }
body.dh-customer #aiWelcomeScrim {
  display: block; position: fixed; inset: 0; z-index: 1250;
  background: rgba(0, 0, 0, .38);          /* inside the 30–40% brief */
  opacity: 0; transition: opacity .24s ease;
  -webkit-tap-highlight-color: transparent;
}
body.dh-customer #aiWelcomeScrim.is-shown { opacity: 1; }
body.dh-customer #aiWelcomeScrim.is-out   { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  body.dh-customer #aiWelcomeScrim { transition: none; }
}

/* ===========================================================================
 * FLOATING LABELS  —  .dh-float   (2026-08-06)
 *
 * Opt-in, purely additive: a `.form-group` gains `.dh-float` and the label moves
 * to sit AFTER the input in the DOM. Nothing without the class changes, so the
 * ~200 other forms in the SPA are untouched.
 *
 * Required markup (label AFTER the control — the `~` sibling combinator can
 * only look forward, and :has() isn't safe on every shipped WebView):
 *
 *     <div class="form-group dh-float">
 *       <input id="qsName" placeholder="As per ID">
 *       <label for="qsName">Full Name *</label>
 *     </div>
 *
 * A placeholder is LOAD-BEARING: the rest state is detected with
 * `:placeholder-shown`, so an input with no placeholder attribute would never
 * match and its label would sit permanently floated over an empty box. Use a
 * real hint where one helps, `placeholder=" "` (a single space) where it
 * doesn't — never omit it.
 *
 * The placeholder is treated as a HINT, not a label: it is transparent while
 * the field is at rest (the label is occupying that space) and fades in on
 * focus, once the label has moved out of the way. That is what lets us keep
 * "As per ID" / "10 digits" without stacking two texts on top of each other.
 * ======================================================================== */
.dh-float {
  --dh-float-h: 56px;          /* field height; label centring is derived from it */
  position: relative;
}

.dh-float > input,
.dh-float > textarea {
  width: 100%;
  box-sizing: border-box;
  height: var(--dh-float-h);
  /* Top padding clears the floated label; bottom keeps the value optically
     centred in the remaining space. */
  padding: 23px 14px 7px;
  /* 16px minimum — iOS zooms the whole page when a focused input is smaller. */
  font-size: 16px;
  line-height: 1.2;
  border-radius: 12px;
}
.dh-float > textarea {
  height: auto;
  min-height: var(--dh-float-h);
  padding-top: 24px;
}

.dh-float > label {
  position: absolute;
  left: 15px;
  top: 0;
  /* Full-height flex box rather than a top offset + translate: the label stays
     vertically centred whatever --dh-float-h is, and the float animation is then a
     single clean translate. */
  height: var(--dh-float-h);
  display: flex;
  align-items: center;
  margin: 0;                        /* beats .form-group label's margin-bottom */
  max-width: calc(100% - 30px);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: 15px;
  font-weight: 500;
  color: var(--dh-muted2, #6B6B6B);
  /* Clicks must reach the input underneath — the label is decoration here, the
     `for=` attribute already does the accessibility job. */
  pointer-events: none;
  transition: transform .16s ease, font-size .16s ease, color .16s ease;
}

/* Floated state: focused, or holding a value (incl. browser autofill, which
   clears :placeholder-shown just like typing does). */
.dh-float > input:focus ~ label,
.dh-float > input:not(:placeholder-shown) ~ label,
.dh-float > textarea:focus ~ label,
.dh-float > textarea:not(:placeholder-shown) ~ label {
  transform: translateY(-15px);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--dh-gold, #C9962A);
}
/* Resting label stays neutral even when the field is filled but unfocused —
   gold is the focus accent, so only light it up on focus. */
.dh-float > input:not(:focus):not(:placeholder-shown) ~ label,
.dh-float > textarea:not(:focus):not(:placeholder-shown) ~ label {
  color: var(--dh-muted, #8A8A8A);
}

/* Inline "(optional)" etc. inside the label must ride the float, not fight it. */
.dh-float > label span { font-weight: inherit; font-size: inherit; color: inherit; }

/* Hint placeholder: hidden at rest, visible once the label has floated away. */
.dh-float > input:not(:focus)::placeholder,
.dh-float > textarea:not(:focus)::placeholder { color: transparent; }
.dh-float > input:focus::placeholder,
.dh-float > textarea:focus::placeholder { color: var(--dh-faint2, #AAAAAA); }

/* Wrapper the runtime converter inserts around a loose `.dh-flabel` +
   `.dh-input` pair. It has to re-create the vertical rhythm that .dh-flabel's
   own `margin: 16px 0 7px` used to provide, since that margin is zeroed above. */
.dh-float-wrapped { margin: 16px 0 0; }
.dh-float-wrapped:first-child { margin-top: 0; }

/* Helper text under a field (e.g. the referral-code explainer). */
.dh-float > .dh-float-help {
  margin-top: 6px;
  font-size: 12px;
  line-height: 1.4;
  color: var(--dh-muted, #8A8A8A);
}

/* ---------------------------------------------------------------------------
 * SELECT + DATE  (2026-08-08)
 *
 * Both controls were excluded from the converter because `:placeholder-shown`
 * cannot describe them: a <select> always paints its selected <option>, and a
 * date input always paints "dd-mm-yyyy". So Edit Profile floated its text
 * fields but left Date of Birth / Gender / Marital Status / Annual Income /
 * Occupation on stacked labels — five fields in the old style sitting among
 * floated ones, and five extra label rows of height.
 *
 * The rest state comes from JS instead: `_dhFloatArmSpecial` in index.html
 * tags the wrapper `.dh-float-sel` / `.dh-float-date` and keeps
 * `.dh-float-filled` in sync with the control's value. Geometry, the float
 * animation and the colours are all shared with the text-input rules above —
 * this block only supplies the rest-state detector and the two controls' own
 * chrome.
 * ------------------------------------------------------------------------- */
.dh-float > select {
  width: 100%;
  box-sizing: border-box;
  height: var(--dh-float-h);
  /* Matches the input padding, except the right side clears our chevron. */
  padding: 23px 38px 7px 14px;
  font-size: 16px;               /* 16px minimum or iOS zooms the page */
  line-height: 1.2;
  border-radius: 12px;
  /* The OS arrow sits at a different inset on every platform and collides
     with the floated caption on some Android WebViews — draw our own. */
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  /* !important is not decoration here. `.dh-flowmodal select` (~L423) sets
     `background: var(--dh-bg) !important` — a SHORTHAND, so it resets
     background-image to none and our chevron vanishes. Edit Profile is a
     .dh-flowmodal, so every field in it hits that rule. Longhands + the same
     weight is the surgical fix; widening the blanket to skip .dh-float would
     need `:not(.dh-float *)`, which is Selectors-4 and not safe on every
     shipped WebView (same reason the converter avoids :has()). */
  background-color: var(--input-bg, #fff) !important;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%238A8A8A' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") !important;
  background-repeat: no-repeat !important;
  background-position: right 14px center !important;
  background-size: 12px auto !important;
}
/* Options are pinned to ink ONLY while the list can actually be seen — i.e.
   while the select has focus, or it holds a value. Pinning them unconditionally
   backfires: some engines paint the CLOSED select's text in the selected
   option's colour, which would defeat the at-rest transparency below. */
.dh-float-sel:focus-within > select option,
.dh-float-sel.dh-float-filled > select option { color: var(--dh-ink, #0A0A0A); }

/* Floated state for the JS-driven controls. Mirrors the `:placeholder-shown`
   rule above; kept as its own selector list so neither can regress the other. */
.dh-float-sel:focus-within > select ~ label,
.dh-float-sel.dh-float-filled > select ~ label,
.dh-float-date:focus-within > input ~ label,
.dh-float-date.dh-float-filled > input ~ label {
  transform: translateY(-15px);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--dh-gold, #C9962A);
}
/* Filled but not focused stays neutral — gold is the focus accent. */
.dh-float-sel:not(:focus-within).dh-float-filled > select ~ label,
.dh-float-date:not(:focus-within).dh-float-filled > input ~ label {
  color: var(--dh-muted, #8A8A8A);
}

/* At rest and empty, the control's own text is hidden so the label can sit in
   the middle of a clean box exactly like an empty text field. Without this the
   select would read "Select…" and the date "dd-mm-yyyy" UNDER a resting label,
   i.e. two captions stacked in one box.

   !important for the same reason as the chevron above: `.dh-flowmodal select`
   / `.dh-flowmodal input` set `color: var(--dh-ink) !important`, and without
   matching that weight the caption and the control's own text render on top of
   each other — which is exactly the overlap this block exists to prevent. */
.dh-float-sel:not(.dh-float-filled):not(:focus-within) > select,
.dh-float-date:not(.dh-float-filled):not(:focus-within) > input {
  color: transparent !important;
}
/* Kept as its own rule, NOT appended to the list above: an engine that doesn't
   know this WebKit pseudo-element treats the whole selector list as invalid and
   drops it, taking the two real rules with it. */
.dh-float-date:not(.dh-float-filled):not(:focus-within) > input::-webkit-datetime-edit {
  color: transparent !important;
}

/* The text-input rule (`input:not(:placeholder-shown) ~ label`) also matches a
   date input — Chrome never reports :placeholder-shown for one — which would
   pin its label floated over an empty box. Out-specified here (0,3,2 beats
   0,2,2) so `.dh-float-filled` stays the single source of truth. */
.dh-float-date:not(.dh-float-filled):not(:focus-within) > input ~ label {
  transform: none;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: normal;
  text-transform: none;
  color: var(--dh-muted2, #6B6B6B);
}

/* Narrow columns — the 3-up address (City / State / PIN Code) and phone
   (Tel res / Tel office / Extn.) rows leave ~85-92px per field on a phone. The
   floated 11px caption fits there, but the 15px resting one does not, so trim
   the rest state and pull it closer to the edge. Only the resting caption is
   affected; the floated state keeps the shared geometry. */
.dh-float-narrow > label {
  font-size: 13.5px;
  left: 12px;
  max-width: calc(100% - 18px);
}

/* Custom date chrome. The native picker is KEPT — on a phone it is the OS
   wheel/calendar, which beats anything we could draw and is what the
   accessibility layer expects. Only its trigger is restyled: the browser's
   indicator is stretched over the right edge and made invisible, and a gold
   calendar glyph is painted in its place. Tapping anywhere on that edge (or
   on the field itself, on mobile) still opens the OS picker. */
.dh-float-date { position: relative; }
.dh-float > input[type="date"],
.dh-float > input[type="month"],
.dh-float > input[type="datetime-local"] {
  /* Longhands + !important — see the select chevron above: the .dh-flowmodal
     blanket's `background:` shorthand would otherwise erase this icon. */
  background-color: var(--input-bg, #fff) !important;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23C9962A' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cpath d='M16 2v4M8 2v4M3 10h18'/%3E%3C/svg%3E") !important;
  background-repeat: no-repeat !important;
  background-position: right 13px center !important;
  background-size: 17px auto !important;
  padding-right: 42px;
}
.dh-float > input[type="date"]::-webkit-calendar-picker-indicator,
.dh-float > input[type="month"]::-webkit-calendar-picker-indicator,
.dh-float > input[type="datetime-local"]::-webkit-calendar-picker-indicator {
  position: absolute;
  top: 0;
  right: 0;
  width: 44px;
  height: 100%;
  margin: 0;
  padding: 0;
  opacity: 0;                    /* invisible, still the real hit target */
  cursor: pointer;
}
/* Safari/iOS lays the date parts out with its own inner box — strip its
   padding so the value lines up with every other field's text. */
.dh-float > input[type="date"]::-webkit-date-and-time-value { text-align: left; }
.dh-float > input[type="date"]::-webkit-datetime-edit { padding: 0; }

@media (prefers-reduced-motion: reduce) {
  .dh-float > label { transition: none; }
}

/* ===========================================================================
 * SKELETON LOADERS — page transitions + wizard steps  (added 2026-08-07)
 * ---------------------------------------------------------------------------
 * Replaces the centred spinner on every customer screen. A spinner says
 * "something is happening somewhere"; a skeleton says "THIS is the shape of
 * what is arriving", which is what makes a WebView feel native rather than
 * like a page load. Two entry points:
 *
 *   • page → page   `_dhPaintSkel(section)` in index.html paints the
 *                    destination screen's skeleton the moment the screen is
 *                    activated in `switchSection`, BEFORE its loader awaits.
 *   • step → step   `_loanBusy(msg)` (and the gold/locker wizards) paint the
 *                    form skeleton between steps.
 *
 * Every block is a `.dh-skel` — a surface-coloured box with a light sweep
 * across it. Sizes are set by the modifier classes (or inline width/height)
 * so the same primitive builds lines, circles, tiles and hero cards.
 * ------------------------------------------------------------------------- */

/* Placeholder tints. Antique gold at low alpha over the page white — the
 * loader stays inside the ink+gold palette instead of introducing a grey (or,
 * as it did until 2026-08-08, a near-black panel) that belongs to no theme.
 * Alpha rather than a flat hex so the same block sits correctly on white
 * cards, on the cream band and on the gold-tinted product-hub cards. */
:root {
  --dh-skel-bg:    rgba(201,150,42,0.13);   /* body of a placeholder block   */
  --dh-skel-bg-2:  rgba(201,150,42,0.22);   /* on an already gold-tinted panel */
  --dh-skel-sweep: rgba(255,252,245,0.85);  /* warm white, not pure white     */
}

/* The primitive. Anything with .dh-skel becomes a shimmering placeholder box. */
.dh-skel {
  position: relative;
  overflow: hidden;
  background: var(--dh-skel-bg);
  border-radius: 8px;
  /* Placeholders are decorative: never announce them to a screen reader —
     the loader's real content replaces them a moment later. */
  pointer-events: none;
}
.dh-skel::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg,
              rgba(255,252,245,0) 0%,
              var(--dh-skel-sweep) 50%,
              rgba(255,252,245,0) 100%);
  animation: dh-skelSweep 1.4s ease-in-out infinite;
}
@keyframes dh-skelSweep { 100% { transform: translateX(100%); } }

/* A block sitting ON the gold-tinted hero panel needs more contrast than the
   base tint, or it disappears into the panel it is drawn on. */
.dh-skel-alt { background: var(--dh-skel-bg-2); }

/* ── Shapes ─────────────────────────────────────────────────────────────── */
.dh-skel-ln     { height: 12px; margin: 0 0 9px; }        /* body line       */
.dh-skel-ln:last-child { margin-bottom: 0; }
.dh-skel-ln-sm  { height: 10px; margin: 0 0 7px; }        /* caption line    */
.dh-skel-ttl    { height: 19px; margin: 0 0 13px; border-radius: 9px; }
.dh-skel-amt    { height: 30px; margin: 0 0 12px; border-radius: 10px; }
.dh-skel-cir    { border-radius: 50%; flex: 0 0 auto; }
.dh-skel-chip   { height: 26px; width: 84px; border-radius: 20px; }
.dh-skel-btn    { height: 46px; border-radius: 14px; margin-top: 16px; }
.dh-skel-icon   { width: 44px; height: 44px; border-radius: 14px; flex: 0 0 auto; }

/* ── Containers ─────────────────────────────────────────────────────────── */
/* The whole placeholder tree fades in, so a fast (cached) response that
   resolves within a frame or two never flashes a hard grey block. */
.dh-skelwrap { animation: dh-skelIn .22s ease both; }
@keyframes dh-skelIn { from { opacity: 0; } to { opacity: 1; } }

/* A card-shaped placeholder — matches .dh-card's box exactly so the real card
   lands in the same footprint and nothing jumps. */
.dh-skel-card {
  background: var(--dh-bg, #fff);
  border: 1.5px solid var(--dh-border2, rgba(0,0,0,0.07));
  border-radius: 18px;
  box-shadow: 0 7px 8px rgb(0 0 0 / 8%);
  padding: calc(18px * var(--dh-card-pad-scale, 1));
  margin-bottom: 14px;
}
.dh-skel-card:last-child { margin-bottom: 0; }

/* Hero placeholder — stands in for the dark bronze card at the top of My Gold /
 * My NCD / Refer & Earn.
 *
 * Deliberately NOT the real card's dark gradient (Prem, 2026-08-08). A full-width
 * near-black panel is the heaviest thing that can be on screen, and while
 * loading it reads as a rendering fault rather than as a placeholder. The
 * gold-tinted panel below keeps the loader in the theme; the real card fades in
 * over it when the data lands. */
.dh-skel-hero {
  background: var(--dh-gold-dim, rgba(201,150,42,0.10));
  border: 1.5px solid var(--dh-gold-border, rgba(201,150,42,0.25));
  border-radius: 22px;
  padding: 22px 20px 20px;
  box-shadow: 0 6px 18px rgba(201,150,42,.10);
  margin-bottom: 16px;
}

/* Row: icon + two stacked lines (list items, ledger rows, nav rows). */
.dh-skel-row { display: flex; align-items: center; gap: 13px; }
.dh-skel-row .dh-skel-rowtext { flex: 1 1 auto; min-width: 0; }

/* Two-up / three-up stat strips. */
.dh-skel-stats { display: flex; gap: 12px; margin-bottom: 14px; }
.dh-skel-stats > * { flex: 1 1 0; min-width: 0; }

/* Product-hub style tile grid. */
.dh-skel-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.dh-skel-grid > .dh-skel-card { margin-bottom: 0; }

/* Optional caption under a step skeleton ("Checking your eligibility…").
   Kept as real text — it is the one part of a wizard wait that carries
   information the shapes cannot. */
.dh-skel-note {
  margin-top: 14px;
  text-align: center;
  font-size: 13px;
  color: var(--dh-muted, #8A8A8A);
}

/* Motion-sensitive users get the layout without the sweep. The blocks stay
   visible (they still communicate the shape) — only the animation stops. */
@media (prefers-reduced-motion: reduce) {
  .dh-skel::after { animation: none; opacity: 0; }
  .dh-skelwrap    { animation: none; }
}

/* ── Minimum-hold overlay ───────────────────────────────────────────────────
 * Prem, 2026-08-07: "show the loader FIRST for 2-3 sec, then the page."
 *
 * Painting the skeleton into the container alone is not enough to guarantee
 * that — a cached or fast response replaces it within a frame, so the loader
 * flickers past. The overlay is a SIBLING of the content container inside the
 * screen, so the loader is free to replace the container's innerHTML while
 * this stays on top. It is removed on a timer, not on load, which is what
 * makes the minimum a real floor.
 *
 * `.dh-skel-host` is added to the screen only while an overlay is alive — it
 * supplies the positioning context and is removed with the overlay, so no
 * screen is left permanently `position: relative`.
 *
 * Duration lives in _DH_SKEL_MIN_MS (index.html), not here. */
.dh-skel-host { position: relative; }
.dh-skel-ov {
  position: absolute;
  top: 0; left: 0; right: 0;
  /* min-height rather than `inset: 0` so a skeleton taller than the (possibly
     still-empty) screen behind it keeps an opaque backdrop the whole way down
     instead of spilling past the overlay's bottom edge. */
  min-height: 100%;
  z-index: 6;
  background: var(--dh-bg, #fff);
  transition: opacity .18s ease;
}
.dh-skel-ov-out { opacity: 0; pointer-events: none; }

@media (prefers-reduced-motion: reduce) {
  .dh-skel-ov { transition: none; }
}

/* ===========================================================================
 * PULL-TO-REFRESH  (added 2026-08-08)
 * ---------------------------------------------------------------------------
 * The customer app no longer refreshes itself on a 30s timer — the customer
 * pulls the page down to ask for fresh data. This is that indicator: a gold
 * ring in a floating white puck that follows the finger down from under the
 * topbar, goes solid at the threshold, and spins while the section's loader
 * runs. Driven entirely by _initPullToRefresh() in index.html; every geometry
 * value it writes inline (top / transform / opacity) is a live gesture value,
 * so nothing here fights it.
 *
 * Sits at the BODY level, not inside .content-area: the scroller clips its own
 * overflow, so an indicator parked above the content origin would be invisible
 * for exactly the part of the pull that matters.
 * ------------------------------------------------------------------------- */
.dh-ptr {
  position: fixed;
  left: 50%;
  top: 0;                          /* JS moves it to the scroller's top edge */
  z-index: 40;                     /* over content, under modals/sheets */
  pointer-events: none;            /* never intercepts the gesture drawing it */
  opacity: 0;
  transform: translate(-50%, 0) scale(.6);
  transition: opacity .18s ease, transform .22s cubic-bezier(.2,.8,.3,1);
  will-change: transform, opacity;
}
/* While a finger is down the position comes frame-by-frame from touchmove —
   a transition here would make the ring lag behind the thumb. It is added back
   on release so the retreat animates. */
.dh-ptr-drag { transition: none; }

.dh-ptr-circle {
  width: 38px; height: 38px;
  border-radius: 50%;
  background: var(--dh-bg, #fff);
  box-shadow: 0 4px 14px rgba(0,0,0,.16);
  display: flex; align-items: center; justify-content: center;
}

/* One transparent edge turns the ring into an arc — that gap is what makes the
   rotation legible, both as it tracks the pull and as it spins. */
.dh-ptr-ring {
  width: 20px; height: 20px;
  border-radius: 50%;
  border: 2.5px solid var(--dh-gold, #C9962A);
  border-top-color: transparent;
  opacity: .45;
}
/* Past the threshold — release now and it refreshes. */
.dh-ptr-ready .dh-ptr-ring { opacity: 1; }
/* Released: loading. */
.dh-ptr-busy .dh-ptr-ring { opacity: 1; animation: dh-ptrSpin .7s linear infinite; }
@keyframes dh-ptrSpin { to { transform: rotate(360deg); } }

/* Keep the pull inside the app: without this the WebView chains the overscroll
   to the document and rubber-bands the whole page behind the indicator.
   Scoped to the customer shell — the staff portal has no pull gesture. */
body.dh-customer .content-area { overscroll-behavior-y: contain; }

@media (prefers-reduced-motion: reduce) {
  .dh-ptr { transition: none; }
  .dh-ptr-busy .dh-ptr-ring { animation-duration: 1.6s; }
}

/* ===========================================================================
 * PAYMENT METHODS — My Profile → Payment methods sub-page (2026-08-11)
 *
 * Rebuilt to the new reference: instead of ONE card holding the caption, the
 * UPI box, the bank header and the bank list, the page is a stack of
 * SIBLING cards — one per method family (UPI / Bank accounts / Cards) — with
 * the caption + blurb sitting on the page above them. That removes the
 * card-in-card nesting the old markup had (a hand-bordered box inside a
 * .dh-card) and lets each family carry its own "+ Add" affordance.
 *
 * The rows share one shape with .dh-sheet-row (38px chip · title + sub ·
 * trailing control) so this page reads like the Profile menu it opens from.
 * They are NOT .dh-sheet-row itself: these rows are not tappable — the
 * control on the right is, and a row-wide :active would say otherwise.
 * ======================================================================== */
.dh-pmintro { margin: 0 4px; }
.dh-pmintro .dh-stepcap { margin: 0; }
.dh-pmintro .dh-fineprint { margin-top: 4px; line-height: 1.5; }
/* Tighter than .dh-loanwrap's 20px: these cards are lists of rows that carry
   their own padding, not prose blocks. */
.dh-loanwrap .dh-card.dh-pmcard { padding: 16px; }

/* Family header — title + a gold text "+ Add". A text button, not .dh-btn:
   .dh-btn is width:100% and would need three inline overrides to sit here,
   and the reference reads it as a link beside the title. */
.dh-pmhd { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 12px; }
.dh-pmhd-t { font-size: 15px; font-weight: 700; color: var(--dh-ink); }
.dh-pmadd {
  display: inline-flex; align-items: center; gap: 5px;
  background: none; border: none; cursor: pointer; font: inherit;
  font-size: 13px; font-weight: 600; color: #8a6410;
  /* Negative margin cancels the padding so the label still aligns to the
     card edge while the tap target stays ~40px. */
  padding: 8px 6px; margin: -8px -6px;
  border-radius: 10px;
}
.dh-pmadd:active { background: var(--dh-gold-dim); }

/* A method row: chip · text · trailing control. */
.dh-pmrow { display: flex; align-items: center; gap: 12px; }
.dh-pmic {
  width: 38px; height: 38px; border-radius: 12px; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  background: var(--dh-gold-dim); color: #634404; border: 1px solid rgba(201,150,42,.18);
}
.dh-pmtxt { flex: 1; min-width: 0; }
.dh-pmtxt b { display: block; font-size: 14px; font-weight: 700; color: var(--dh-ink); }
.dh-pmsub { display: block; font-size: 12.5px; color: var(--dh-muted); margin-top: 2px; word-break: break-all; }
.dh-pmtxt .dh-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
/* .dh-btn is a full-width flex block — shrink it back to a chip-sized action. */
.dh-pmbtn { width: auto; flex: 0 0 auto; padding: 8px 16px; font-size: 13px; }

/* A saved instrument (bank account / card) inside a family card. */
.dh-pmitem { display: flex; align-items: center; gap: 12px; padding: 12px; border: 1.5px solid var(--dh-border2); border-radius: 14px; }
.dh-pmitem + .dh-pmitem { margin-top: 10px; }
.dh-pmitem-b { flex-wrap: wrap; }
/* Wraps to its own line under the account number. The 50px inset is the chip
   (38px) plus the row gap (12px), so the pills line up with the text they
   belong to instead of with the chip. */
.dh-pmitem-acts { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; width: 100%; padding-left: 50px; margin-top: 2px; }
.dh-pmdel {
  background: none; border: none; cursor: pointer; flex: 0 0 auto;
  color: var(--dh-error); padding: 8px; margin: -8px -4px -8px 0; border-radius: 10px;
  display: inline-flex; align-items: center; justify-content: center;
}
.dh-pmdel:active { background: var(--dh-error-dim); }

/* Empty state — dashed box so it reads as "room for something", not as a
   failure. The old copy was bare fine print with no boundary. */
.dh-pmempty { text-align: center; padding: 22px 14px; border: 1.5px dashed var(--dh-border2); border-radius: 14px; color: var(--dh-muted); font-size: 13px; line-height: 1.5; }
.dh-pmemptyic {
  width: 44px; height: 44px; border-radius: 50%; margin: 0 auto 10px;
  display: flex; align-items: center; justify-content: center;
  background: var(--dh-surface); color: var(--dh-faint2); border: 1px solid var(--dh-border2);
}
