/* =============================================================
   Vex Recovery — portal-v3.css
   Layers over portal.css (loaded first). Only v3-specific components live here.
   Reuses portal.css's :root palette, header, footer, progress bar, .screen,
   .screen-inner, .btn-portal and the h1/h2 element rules inside .screen-inner.

   Note: portal.css also defines .confirm-row / .confirm-label / .confirm-value /
   .confirm-email for the old v2 confirmation screen. This file loads AFTER it, so
   the v3 definitions below win. That is intentional.
   ============================================================= */

/* ── Overview headline ────────────────────────────────────── */
.ov-head { text-align: center; padding: 1.5rem 0 0.5rem; }
.ov-eyebrow { color: var(--muted); font-size: 0.95rem; margin-bottom: 0.35rem; }
.ov-balance {
  font-size: clamp(2.2rem, 8vw, 3rem); font-weight: 700;
  color: var(--navy); line-height: 1.1; letter-spacing: -0.03em;
}
.ov-meta { color: var(--dim); font-size: 0.875rem; margin-top: 0.4rem; }

/* ── Collapsible detail ───────────────────────────────────── */
.ov-detail { margin: 1rem 0 1.75rem; }
.detail-toggle {
  width: 100%; background: none; border: none; padding: 0.6rem 0; cursor: pointer;
  display: flex; align-items: center; justify-content: center; gap: 0.4rem;
  color: var(--blue); font-size: 0.9rem; font-weight: 500; font-family: inherit;
}
.detail-toggle .ti-chevron-down { transition: transform 0.18s ease; }
.detail-toggle[aria-expanded="true"] .ti-chevron-down { transform: rotate(180deg); }
.detail-body {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 1rem 1.15rem; margin-top: 0.4rem;
}
.detail-row {
  display: flex; justify-content: space-between; gap: 1rem;
  padding: 0.42rem 0; border-bottom: 1px solid var(--border); font-size: 0.9rem;
}
.detail-row:last-child { border-bottom: none; }
.detail-label { color: var(--muted); }
.detail-value { color: var(--text); font-weight: 500; text-align: right; }
.mini-miranda-block {
  margin-top: 0.9rem; font-size: 0.78rem; color: var(--dim); line-height: 1.5;
}

/* ── Offer tiles ──────────────────────────────────────────── */
.section-title {
  font-size: 1.05rem; font-weight: 600; color: var(--navy); margin-bottom: 0.75rem;
}
.offer-tiles {
  display: flex; flex-direction: column; gap: 0.75rem; padding-bottom: 1rem;
}

.tile {
  background: var(--white); border: 2px solid var(--border);
  border-radius: var(--radius); padding: 1.1rem 1.2rem; cursor: pointer;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.tile:hover { border-color: var(--blue-lt); }
.tile.selected { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(26,109,216,0.12); }

/* Settlement — the primary, preselected offer */
.tile--primary { padding: 1.35rem 1.3rem; }
.tile-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.75rem; flex-wrap: wrap;
}
.tile-title { font-size: 1.1rem; font-weight: 700; color: var(--navy); }
.tile-save {
  background: rgba(0,201,136,0.12); color: #00875C;
  font-weight: 700; font-size: 0.85rem; padding: 0.2rem 0.6rem; border-radius: 999px;
  white-space: nowrap;
}
.tile-sub { color: var(--muted); font-size: 0.92rem; margin-top: 0.35rem; line-height: 1.5; }
.tile-note { color: var(--dim); font-size: 0.8rem; margin-top: 0.5rem; }

/* Pay Full Balance Today — demoted, small (D3) */
/* .tile--minor / .tile-title-sm / .tile-amt-sm styled the "Pay Full Balance Today"
   tile, which was removed deliberately (see renderTiles in portal-v3.js). Kept because
   .tile--minor is a generic small-tile modifier and costs nothing to leave in place. */
.tile--minor {
  display: flex; align-items: center; justify-content: space-between;
  padding: 0.8rem 1.1rem; gap: 0.75rem;
}
.tile-title-sm { font-size: 0.95rem; font-weight: 500; color: var(--muted); }
.tile-amt-sm { font-size: 0.95rem; font-weight: 600; color: var(--text); }

/* Fallback tile: no ladder and no plan tier fit, so there is nothing to choose. */
.tile--empty { cursor: default; border-style: dashed; }
.tile--empty:hover { border-color: var(--border); }

/* Explains why the savings figure shrinks as the term lengthens. Sits under the term
   chips, so it must stay legible at 320px without stealing height from them. */
.tile-hint {
  margin: 0.55rem 0 0; font-size: 0.8rem; line-height: 1.35;
  color: var(--muted);
}

/* ── Term chips ───────────────────────────────────────────── */
/* Three chips must fit across a 375px phone, where the tile's inner width is ~240px.
   A grid (rather than wrapping flex) keeps them one row and equal width at any size. */
.term-chips {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
  gap: 0.4rem; margin-top: 0.85rem;
}

/* Below ~360px three chips leave only ~45px of content each, which is narrower than
   "12 months" (57px) -- so a three-across row could only fit by breaking mid-token
   ("$840.0" / "0"), and a broken money amount is worse than a second row. Two columns
   give each chip ~80px, so every label and amount stays whole and full size. */
@media (max-width: 360px) {
  .term-chips { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.term-chip {
  min-width: 0; background: var(--surface); cursor: pointer;
  border: 1.5px solid var(--border); border-radius: var(--radius-sm);
  padding: 0.5rem 0.3rem; display: flex; flex-direction: column; gap: 0.1rem;
  text-align: center; font-family: inherit;
  overflow: hidden;              /* a chip must never paint outside its own border */
}
.term-chip:hover { border-color: var(--blue-lt); }
.term-chip.on { border-color: var(--blue); background: rgba(26,109,216,0.06); }

/* `white-space: nowrap` plus a fixed font size made the plan amounts wider than the
   column a three-chip grid leaves on a 375px phone, so they painted straight over the
   neighbouring chip. Now the text scales down with the viewport, and `overflow-wrap`
   is a last-resort guard only -- the two changes that actually make things fit are
   dropping the redundant "/mo" suffix (see planTileHTML) and the two-column grid below
   360px. A chip clips nothing at 320px and up. */
.term-chip-n {
  font-size: clamp(0.62rem, 2.5vw, 0.72rem); color: var(--muted);
  white-space: normal; overflow-wrap: anywhere; line-height: 1.25;
}
.term-chip-amt {
  font-size: clamp(0.72rem, 3.1vw, 0.92rem); font-weight: 600; color: var(--navy);
  white-space: normal; overflow-wrap: anywhere; line-height: 1.25;
}

/* ── Payment method tiles ─────────────────────────────────── */
.method-tiles { display: flex; flex-direction: column; gap: 0.65rem; margin: 1.25rem 0; }
.method-tile {
  background: var(--white); border: 2px solid var(--border); border-radius: var(--radius);
  padding: 0.95rem 1.1rem; display: flex; align-items: center; justify-content: space-between;
  gap: 0.75rem; text-align: left; cursor: pointer; font-family: inherit;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.method-tile:hover { border-color: var(--blue-lt); }
.method-tile.selected { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(26,109,216,0.12); }
.method-name { font-weight: 600; color: var(--navy); font-size: 1rem; }
.brand-marks {
  /* 10px is the OPTICAL gap. Each mark then cancels its own built-in padding with a
     negative margin below, so the visible spacing between artwork is even. Without
     that compensation a uniform gap produced 15.2px between Visa and Mastercard but
     only 8.2px between Mastercard and Discover. */
  display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
  justify-content: flex-end;      /* wrapped marks stay tucked to the tile's right edge */
}

/* ── Card acceptance marks ────────────────────────────────────
   Sized by OPTICAL weight, NOT by a shared height. The three assets are three
   different kinds of object, and giving them one height makes them look wrong:

     visa.svg       Bare WORDMARK, aspect 1.64, tight bounding box -- the letterforms
                    themselves run the full height of the asset. Thin blue strokes on
                    white carry the least visual weight of the three, so it needs the
                    LEAST height in the file but reads level with the others.
     mastercard.png Bare SYMBOL (interlocking circles), aspect 1.42, full-bleed. Two
                    solid saturated discs -- heavy, and with no frame to contain them.
     discover.svg   FRAMED badge, aspect 1.52 (same ratio as the discover.png it
                    replaced, so these heights carried over unchanged): the word sits
                    inside a filled orange rectangle that runs edge to edge. A filled
                    box is the heaviest object here, so it takes the smallest height of
                    the three despite its own text being only ~a third of the badge.
                    This asset uses gradients, clip-paths and one mix-blend-mode:
                    multiply. That blend is scoped by the SVG's own isolation:isolate
                    AND by being referenced through <img>, which renders in its own
                    context -- so it cannot blend with the page behind it.

   Set by eye at 320 / 375 / desktop against the real assets, not derived from the
   aspect ratios -- the ratios describe the boxes, not what is inside them. If an
   asset is ever swapped, look at it again rather than reusing these numbers. */
.brand-mark {
  display: block; width: auto; flex: 0 0 auto;
  object-fit: contain;
}
/* The Visa asset carries Visa's mandated clear space INSIDE its viewBox: the artwork
   is 218.071 x 109.37 within a 278.075 x 169.374 box, so only 64.6% of the declared
   height is visible wordmark. At the old 17px it rendered ~11px of actual mark beside
   Mastercard's 21px symbol, which is why it read as undersized. 31px here renders
   ~20px of wordmark, matching Discover. Do NOT "normalise" these three to one number
   -- they have different internal padding and would look wrong again. */
/* Each asset ships with its own clear space baked into the file, and the amounts
   differ, so a single flex `gap` produces visibly uneven spacing. The negative
   margins below cancel that padding so the 10px gap is what the eye actually sees.
   Measured from the assets, scaled to the heights on the next lines:

     Visa       viewBox 278.075 wide, artwork 218.071 at x=30.002
                -> 30.002 x (31/169.374)  = 5.49px each side
     Mastercard 54px wide at 1x, artwork spans x 6..51  (ASYMMETRIC)
                -> left  6 x (21/38)      = 3.32px
                -> right 2 x (21/38)      = 1.11px
     Discover   viewBox 95.69 wide, card outline spans x 2.3..93.39
                -> 2.3 x (20/62.85)       = 0.73px each side

   IF YOU CHANGE A HEIGHT, RECOMPUTE ITS MARGIN -- the padding scales with it. */
.brand-mark--visa { height: 31px; margin-left: -5.49px; margin-right: -5.49px; }
.brand-mark--mc   { height: 21px; margin-left: -3.32px; margin-right: -1.11px; }
.brand-mark--disc { height: 20px; margin-left: -0.73px; margin-right: -0.73px; }

/* The bank mark has no licensed asset, so it stays text. Stripped of the old chip
   border/background so it reads as a quiet label rather than a fourth, fake logo
   badge sitting under three real ones. */
.brand {
  font-size: 0.7rem; font-weight: 600; letter-spacing: 0.01em;
  color: var(--muted); background: none; border: 0; padding: 0;
  white-space: nowrap; display: inline-flex; align-items: center; gap: 0.28rem;
}
.brand-bank { color: var(--muted); }
.brand-bank i { font-size: 0.95rem; }

/* ── Panels + fields ──────────────────────────────────────── */
.pay-panel { padding-bottom: 0.5rem; }
.field { margin-bottom: 0.85rem; }
.field label { display: block; font-size: 0.82rem; color: var(--muted); margin-bottom: 0.3rem; }
.field input, .field select {
  width: 100%; padding: 0.7rem 0.8rem; font-size: 1rem; font-family: inherit;
  border: 1.5px solid var(--border); border-radius: var(--radius-sm);
  background: var(--white); color: var(--text);
}
.field input:focus, .field select:focus { outline: none; border-color: var(--blue); }
.field input.invalid, .field select.invalid {
  border-color: var(--red); background: rgba(229,52,74,0.04);
}
.field-row { display: flex; gap: 0.6rem; }
.field-row .field { flex: 1 1 0; min-width: 0; }
/* These size modifiers must out-specify `.field-row .field` (0-2-0) above, so they are
   written as descendant selectors too. As bare `.field--sm` (0-1-0) they lose the
   cascade and every column silently collapses to an equal third. */
.field-row .field--sm    { flex: 0 0 88px; }
/* City / State / ZIP share one row. State is two letters and ZIP is five, so both are
   pinned narrow to leave the city name real room on a 375px phone. */
.field-row .field--state { flex: 0 0 64px; }
.field-row .field--zip   { flex: 0 0 78px; }
.field--state select { padding-left: 0.45rem; padding-right: 0.2rem; }
.field-error { color: var(--red); font-size: 0.85rem; min-height: 1.2em; margin: 0.2rem 0 0.6rem; }

/* Stands in for the City / State / ZIP row once Places has resolved a complete
   address (the widget above already spells it out, so the row was the same address a
   second time). Deliberately reads as quiet confirmation text, NOT as a form control:
   nothing here is editable, and the only affordance is the Edit link back to the real
   fields. `display: flex` is set from JS -- the inline `display:none` in the markup is
   what keeps it hidden by default, so a debtor who never triggers autocomplete never
   sees it at all. */
.addr-summary {
  align-items: baseline; justify-content: space-between; gap: 0.6rem;
  margin: -0.35rem 0 0.85rem; font-size: 0.9rem; color: var(--muted);
}
/* Not .btn-link: that one is display:block/width:100%/centered, built for the standalone
   "Enter bank details manually" action. This sits inline at the end of a sentence. */
.addr-edit {
  background: none; border: none; padding: 0; cursor: pointer;
  font-family: inherit; font-size: 0.85rem; color: var(--blue);
  text-decoration: underline; flex: 0 0 auto;
  /* 320px: the summary line wraps before this ever gets squeezed off the row. */
}
.fatal-error {
  background: rgba(229,52,74,0.08); border: 1px solid var(--red); color: var(--red);
  border-radius: var(--radius); padding: 1rem; margin: 1.5rem auto;
  max-width: 480px; text-align: center;
}
.bank-connected { color: var(--green); font-weight: 500; margin: 0.6rem 0; }

/* "Connect your bank instantly" is the recommended path and must read as the primary
   action, not as a line of text. It inherits the solid blue .btn-portal fill; only the
   layout is set here. "Enter bank details manually" stays a quiet text link below it. */
.btn-bank-connect {
  width: 100%; justify-content: center; font-size: 0.95rem;
  padding-top: 0.85rem; padding-bottom: 0.85rem; min-height: 48px;
}
.btn-link {
  background: none; border: none; color: var(--blue); font-size: 0.88rem;
  padding: 0.7rem 0 0.2rem; cursor: pointer; font-family: inherit; display: block;
  width: 100%; text-align: center; text-decoration: underline;
}

/* Disclosure sentence with three inline document links. It now lives INSIDE the sticky
   bar, directly above the pay button (see index.html for why it moved out of the form).

   Sized tight on purpose: the bar carries four stacked elements now, and every pixel
   here comes off the viewport. The base 1rem top margin is dropped because the bar's
   own flex `gap` already spaces it -- keeping both double-spaced it. Wording is fixed
   (it is the authorization artifact); only its size may be tuned. */
.agreement-line {
  text-align: center; font-size: 0.75rem; line-height: 1.45; margin: 0;
  color: var(--muted); text-wrap: pretty;
}
.agreement-line a { color: var(--blue); text-decoration: underline; }
.affiliate-link { text-align: center; font-size: 0.85rem; color: var(--muted); margin-bottom: 1rem; }

/* Google Places widget, once it swaps in for the plain input.

   The widget is a web component with its own shadow DOM, so it cannot be styled by
   ordinary selectors. Two things are needed:

   1. `color-scheme: light`. The component honours prefers-color-scheme, so on a
      debtor with dark mode enabled it renders black-on-black against our light form.
      This pins it light regardless of OS setting -- the rest of the portal has no
      dark theme, so following the OS here is simply wrong.
   2. Google's --gmp-mat-* custom properties, which DO pierce the shadow boundary.
      These map the widget onto the same tokens as `.field input` so it reads as one
      of our fields rather than a Google control dropped into the middle of the form.

   Keep these in sync with `.field input, .field select` above. */
#addressAutocompleteHost { width: 100%; }

#addressAutocompleteHost gmp-place-autocomplete {
  width: 100%;
  color-scheme: light;

  --gmp-mat-color-surface:            var(--white);
  --gmp-mat-color-on-surface:         var(--text);
  --gmp-mat-color-on-surface-variant: var(--muted);
  --gmp-mat-color-outline:            var(--border);
  --gmp-mat-color-outline-decorative: var(--border);
  --gmp-mat-color-primary:            var(--blue);
  --gmp-mat-color-on-primary:         var(--white);
  --gmp-mat-color-info:               var(--blue);
  --gmp-mat-font-family:              inherit;
  --gmp-mat-font-size-body-medium:    1rem;
}

/* ── Sticky footer (D20) ──────────────────────────────────────
   The bar is fixed, so the page reserves bottom space for it.

   THIS RESERVE HAS BEEN WRONG TWICE. History, so it is not re-broken a third time:

     v1  `padding-bottom: 7rem` INSIDE .screen-inner. The 7rem sat inside the white
         card, so the card ran ~112px past its last tile -- an empty white box.
     v2  Moved to `margin-bottom: 7rem` on the OVERVIEW screen only. That fixed the
         white box, but two problems survived and one was introduced:
           - 7rem (112px) is a GUESS. The bar actually measures 71px on overview and
             81px on payment, so 31-41px of the reserve was clearance for nothing.
           - .portal-main's own `padding-bottom` (clamp -> up to 56px) sat BELOW the
             reserve as well, so the true gap under the card was reserve + 56px.
           - Payment kept the inside-the-card padding, so the white void v1 fixed on
             overview was still there on payment.
         Measured at 1440x900: 160px of dead space below the overview card and 112px
         of empty white inside the payment card, for a bar 71-81px tall. That is what
         made the page scroll when the content otherwise fitted.

   v3 (here) reserves the MEASURED bar height, once, on .portal-main -- so it applies
   to every screen uniformly, sits outside the white card (no void), and the footer
   clears the bar instead of hiding behind it. syncStickyHeight() in portal-v3.js
   writes --sticky-h from the live bar and toggles .has-sticky; the 7rem fallback is
   the pre-JS value and should never be the one in use. Screens with no bar (the
   gate, the confirmation) get no reserve at all, because .has-sticky is off.

   TWO DIFFERENT THINGS LIVE AT THE BOTTOM OF THIS PAGE. Keep them separate:

     --content-gutter   CONSTANT. The breathing room between the content and the
                        footer's border-top. Always present, on every screen, whether
                        or not a bar exists. Defined just below.
     --sticky-h reserve DYNAMIC. Measured bar height, so a `position: fixed` bar cannot
                        cover the footer. Lives on .portal-footer's padding-bottom.

   Folding one into the other is exactly how the 250px-of-reserve-behind-a-107px-bar
   stack happened. They are different quantities answering different questions.

   v4 (here) moves the bar clearance OFF .portal-main and onto .portal-footer -- see the
   footer rule further down. The footer sits BETWEEN the card and the bar, so once the
   footer clears the bar, the card clears it transitively; reserving on both stacked two
   bar-heights of empty space (measured: 250px of reserve for a 107px bar). Main now
   keeps only a gutter above the footer, and total bottom reserve is back to roughly
   what it was before the footer was fixed -- 143px vs 139px, i.e. the footer stopped
   being hidden at a cost of 4px rather than 111px. */
/* The constant gutter between the content and the footer's border-top.

   THE HEADER IS THE REFERENCE, not a number invented here. portal.css sets
   `.portal-main { padding: clamp(2rem, 5vw, 3.5rem) 1rem }` -- that TOP value is the
   header-to-content spacing, and it is what makes the top of the page feel right
   (32px at 375, 56px at 1440). This is the identical expression, so header-to-content
   and content-to-footer are a matched pair at every width instead of a considered 56px
   above and a leftover 0-16px below.

   It is a constant. It does NOT scale with the sticky bar, and the bar's clearance is
   NOT folded into it -- that lives on .portal-footer. Two rules, two jobs.

   Costs nothing on a short page: body is a flex column with min-height:100vh and
   .portal-main is flex:1, so on content that already fits, main is stretched and this
   padding is absorbed by the stretch rather than adding height. Verified on the
   confirmation screen (the shortest card) at 450px and 700px tall: no new scroll. */
:root { --content-gutter: clamp(2rem, 5vw, 3.5rem); }

body.has-sticky .portal-main {
  padding-bottom: var(--content-gutter);
}
#screenOverview .screen-inner { margin-bottom: 0; }
#screenPayment  .screen-inner { padding-bottom: 0; }

.sticky-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  background: rgba(255,255,255,0.97);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  border-top: 1px solid var(--border);
  padding: 0.75rem clamp(1rem, 5vw, 2.5rem);
  padding-bottom: calc(0.75rem + env(safe-area-inset-bottom));
}
.sticky-bar-inner {
  max-width: 560px; margin: 0 auto; display: flex; align-items: center;
  justify-content: space-between; gap: 1rem;
}
.sticky-terms { display: flex; flex-direction: column; line-height: 1.3; min-width: 0; }
.sticky-due {
  font-weight: 700; color: var(--navy); font-size: 1.05rem; white-space: nowrap;
}
/* "then 2 payments of $333.34" is disclosure, not decoration -- it wraps rather than
   overflowing. Left nowrap, it pushed the page 5px wider than the viewport at 320px and
   gave the whole overview screen a horizontal scroll. */
.sticky-rest { font-size: 0.8rem; color: var(--muted); white-space: normal; min-width: 0; }
/* Only the overview bar's compact "Next" button holds one line. */
#stickyOverview .sticky-bar-inner .btn-portal { white-space: nowrap; }

/* The pay button's label IS the authorization of record ("Authorize $2,606.66 today +
   2 payments of $2,606.66"), so it may never be clipped. It wraps to as many lines as
   the text needs and the bar grows around it -- base .btn-portal sets line-height:1 and
   the bar previously forced nowrap, which together sheared the label off at both ends
   on a phone. */
.btn-portal--pay {
  width: 100%; justify-content: center; font-size: 0.98rem;
  white-space: normal; text-wrap: balance; line-height: 1.3;
  padding-top: 0.85rem; padding-bottom: 0.85rem; height: auto; min-height: 48px;
}
@media (max-width: 400px) {
  .btn-portal--pay { font-size: 0.9rem; padding-left: 0.9rem; padding-right: 0.9rem; }
}

/* ── The gated pay button ─────────────────────────────────────
   syncPayButtonState() greys this out in exactly one state: "Pay by Bank" is selected
   and no funding source has been supplied yet (no Plaid token, manual form not opened).

   portal.css already has a real .btn-portal:disabled -- grey fill, dim text,
   not-allowed cursor -- and nothing here overrides background on the pay button, so that
   rule reaches it unaided. It is restated anyway because THIS button is the one that
   must read as unavailable at a glance: it is full-width, blue, and the only primary
   action on the screen, so a subtle difference would read as "blue button, just a
   slightly different blue". The opacity drop is what makes it unmistakable next to the
   still-blue "Connect your bank instantly" button directly above it.

   :hover is neutralised explicitly. A disabled button still receives hover on desktop,
   and lifting/shadowing a control the debtor cannot press invites the click that
   produced this bug report in the first place. */
.btn-portal--pay:disabled,
.btn-portal--pay[aria-disabled="true"] {
  background: var(--border);
  color: var(--dim);
  opacity: 0.85;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}
.btn-portal--pay:disabled:hover,
.btn-portal--pay[aria-disabled="true"]:hover {
  background: var(--border);
  color: var(--dim);
  transform: none;
  box-shadow: none;
}

/* The reason the button is grey. Only ever on screen while it IS grey. Sized and
   coloured to match .mini-miranda-pay directly beneath it -- these are two captions
   under one button and they should not compete with each other, or with the button. */
.pay-hint {
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--muted);
  text-align: center;
  margin: 0;
}

/* ── Mini-Miranda placement ───────────────────────────────────
   "This is a communication from a debt collector." is an FDCPA disclosure, and it used
   to sit in .portal-footer -- the one region of the page a fixed sticky bar can cover.
   Measured at 375x700 on the payment screen, scrolled to the bottom: the bar occupied
   viewport y 619-700 and the disclosure sat at y 669-684, fully behind it. It was not
   "tight", it was invisible, at exactly the moment the debtor authorizes a charge.

   It now appears twice, both times in the flow and both times un-coverable:
     .mini-miranda-pay     inside the sticky bar, directly under the pay button. Rides
                           the bar at every width, so the bar cannot cover it.
     .mini-miranda-inline  in the gate form. That screen has no sticky bar at all.
   The wording is fixed and identical in both places. Do not shorten it. */
#screenPayment .sticky-bar-inner {
  /* Was a row holding one full-width button. The disclosure has to sit BENEATH that
     button, not beside it, so the bar stacks. At >=900px the two-column block below
     already sets this; here it is the phone/fixed case. */
  flex-direction: column;
  align-items: stretch;
  gap: 0.55rem;
}
.mini-miranda-pay,
.mini-miranda-inline {
  font-size: 0.72rem; line-height: 1.45; color: var(--dim);
  font-style: italic; text-align: center; margin: 0;
}
/* In the gate form it is the last thing on the screen rather than a caption under a
   button, so it gets real separation from the phone-number fallback above it. */
.mini-miranda-inline { margin-top: 1rem; text-align: left; }

/* ── The footer must clear a fixed bar ────────────────────────
   .portal-main's reserve keeps the CARD clear of the bar, but the footer sits AFTER
   main, so at full scroll it still ended up underneath: at 375x700 the whole footer
   (top y 615) was inside the bar's 619-700 band, taking the Privacy and Terms links
   with it.

   Reserved from the MEASURED --sticky-h -- not a fixed guess. That distinction is the
   whole lesson of the reserve history logged above: the 7rem guess is what produced
   160px of dead space. Here the reserve is the real bar height and it only exists while
   .has-sticky is on, so the gate, the confirmation and every >=900px screen get nothing.

   THIS IS NOW THE ONLY BAR CLEARANCE ON THE PAGE. .portal-main used to carry one too,
   and having both stacked 250px of reserve behind a 107px bar. Because the footer sits
   between the card and the bar, this single reserve clears both. If this rule is ever
   removed, main's needs to come back -- otherwise the card goes behind the bar. */
body.has-sticky .portal-footer {
  padding-bottom: calc(var(--sticky-h, 0px) + 1.25rem);
}

/* The footer's own bottom padding was 1rem against a 0.78rem line -- cramped, and more
   so now that the copyright row is the last thing on the page. */
.portal-footer { padding-bottom: 1.75rem; }

/* portal.css stretches .btn-portal to full width. In the overview bar the button must
   size to its own text so the terms line ("then 2 payments of $306.66") is never
   clipped -- those terms are the disclosure, not decoration. */
#stickyOverview .sticky-terms { flex: 1 1 auto; }
#stickyOverview .sticky-bar-inner .btn-portal { flex: 0 0 auto; width: auto; }

/* Only the overview bar splits; the pay bar is a single full-width button. */
@media (max-width: 420px) {
  #stickyOverview .sticky-bar-inner { flex-direction: column; align-items: stretch; gap: 0.55rem; }
  #stickyOverview .sticky-terms {
    flex-direction: row; justify-content: space-between; align-items: baseline;
    flex-wrap: wrap; column-gap: 0.5rem;
  }
  #stickyOverview .btn-portal { width: 100%; justify-content: center; }
}

/* ── Agreement modal ──────────────────────────────────────── */
.modal-backdrop {
  position: fixed; inset: 0; z-index: 100; background: rgba(13,27,53,0.55);
  display: flex; align-items: center; justify-content: center; padding: 1rem;
}
.modal {
  background: var(--white); border-radius: var(--radius); max-width: 560px; width: 100%;
  max-height: 85vh; display: flex; flex-direction: column; overflow: hidden;
}
.modal-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 1rem 1.2rem; border-bottom: 1px solid var(--border);
}
.modal-head h2 { font-size: 1.05rem; color: var(--navy); margin: 0; }
.modal-close {
  background: none; border: none; color: var(--muted); font-size: 1.2rem;
  cursor: pointer; line-height: 1; padding: 0.2rem;
}
.modal-body {
  padding: 1.2rem; overflow-y: auto; font-size: 0.88rem; line-height: 1.6; color: var(--text);
}
.modal-body p { margin-bottom: 0.7rem; }
.modal-body ul { margin: 0.4rem 0 0.8rem 1.1rem; }
.modal-body li { margin-bottom: 0.2rem; }
.modal-body hr { border: none; border-top: 1px solid var(--border); margin: 0.8rem 0; }
.modal-foot { padding: 0.9rem 1.2rem; border-top: 1px solid var(--border); }

/* ── Confirmation ─────────────────────────────────────────── */
.confirm-icon { font-size: 3rem; color: var(--green); margin-bottom: 0.5rem; line-height: 1; }
.confirm-card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 1rem 1.15rem; margin: 1.25rem 0; text-align: left; width: 100%;
}
.confirm-row {
  display: flex; justify-content: space-between; gap: 1rem;
  padding: 0.45rem 0; border-bottom: 1px solid var(--border); font-size: 0.9rem;
}
.confirm-row:last-child { border-bottom: none; }
.confirm-label { color: var(--muted); }
.confirm-value { color: var(--text); font-weight: 600; text-align: right; }
.confirm-email { font-size: 0.85rem; color: var(--muted); margin-bottom: 1.25rem; }

/* Screen 3 has no sticky bar, so it needs no bottom padding reserve. */
#screenConfirm .screen-inner { padding-bottom: 2rem; }

/* Desktop-only summary block inside the payment sticky panel. Present in the markup
   at every width, but only ever painted at >=900px -- on a phone the same numbers are
   already on the overview sticky bar one screen earlier, and repeating them above the
   pay button would push the button off a short phone viewport. */
.pay-summary { display: none; }

/* =============================================================
   TWO-COLUMN CHECKOUT (>= 900px)

   Below 900px NOTHING here applies and the portal is exactly what it was: three
   sequential screens, a fixed sticky footer, a three-step indicator. That boundary is
   load-bearing -- every rule in this block is nested inside both the media query AND
   a body class, so a phone can never reach any of it.

   Two body classes, set by applyLayoutMode() in portal-v3.js, and they are NOT the
   same condition:

     .vp-wide   viewport is >= 900px. Nothing more. Drives the progress indicator,
                which must show two steps on desktop on the CONFIRMATION screen too
                -- and the confirmation is not a two-column view.
     .two-col   .vp-wide AND we are on the checkout (not the confirmation, not the
                no-token gate) AND an offer is actually priced. Drives the grid.

   Collapsing these into one class was the first attempt and it regressed the desktop
   confirmation back to a three-step indicator.
   ============================================================= */
@media (min-width: 900px) {

  /* ── Progress: hidden outright ───────────────────────────────
     A step indicator narrates a sequence, and at this width there is no sequence to
     narrate: the offer and the payment form are on screen together, so "Review -> Pay"
     describes two things the debtor is already looking at.

     An earlier pass tried to keep the indicator and collapse it to "Review -> Done" by
     hiding step 2 and re-labelling step 3 via a ::after. That is superseded -- the whole
     bar goes at >=900px.

     Keyed off .vp-wide, NOT .two-col, and that distinction is load-bearing: the
     CONFIRMATION screen at >=900px is a single full-width screen and drops .two-col
     (applyLayoutMode refuses two-column on S.screen === 3), so a .two-col rule here
     would let the three-step indicator reappear on desktop the moment the payment
     succeeded. .vp-wide is on at >=900px on every screen -- checkout, gate and
     confirmation alike -- which is exactly the scope this needs.

     updateProgress() still runs and still maintains the markup; nothing about the
     phone path changes. This only stops it being painted. */
  body.vp-wide .progress-bar { display: none; }

  /* ── The grid ────────────────────────────────────────────────
     Both screens are shown at once, so `.screen.active` no longer decides what is
     visible on the checkout -- the grid does. #screenConfirm has no rule here and
     stays governed by .active, and .two-col is dropped the moment it is reached, so
     the confirmation is always a single full-width screen. */
  body.two-col .portal-main {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 1.5rem;
    align-items: start;            /* columns size to their own content, not to each other */
    max-width: 1120px;
    margin: 0 auto;
    /* Was 0 -- correct about the bar (there is no fixed bar to clear at this width) but
       wrong about the gutter, so both columns ran straight into the footer's border-top.
       The two are separate concerns; this is the constant gutter, and it is the same
       value as the padding-top above it.

       In two-column .portal-main is the GRID, so its content box is as tall as the
       TALLER column -- this padding therefore measures from whichever column is taller,
       which is what keeps the tall one off the border. The shorter column necessarily
       has a larger gap; that is what unequal columns are. */
    padding-bottom: var(--content-gutter);
  }
  body.two-col #screenOverview { display: block; grid-column: 1; }
  body.two-col #screenPayment  { display: block; grid-column: 2; }
  body.two-col .fatal-error    { grid-column: 1 / -1; }

  /* The 520px cap is a phone-column measure; here each column is already ~420px. */
  body.two-col .screen-inner { max-width: none; }

  /* The overview bar's only job is "Next", and there is nowhere to go -- the payment
     column is already open. Its due-today + terms move into the summary panel. */
  body.two-col #stickyOverview { display: none !important; }

  /* Same numbers as the summary panel two feet to the right. */
  body.two-col #paySummaryTerms { display: none; }

  /* ── Sticky summary panel ────────────────────────────────────
     The fixed footer becomes a card that rides the bottom of the right column. While
     the column is taller than the viewport it stays pinned above the fold as the
     debtor scrolls the card form; once the column ends it settles at its natural
     place. Nothing is clipped either way -- the panel has no height constraint and
     .btn-portal--pay already wraps, which is what keeps the authorization label
     (the legal record) whole at this narrower width.

     WHY IT CANNOT REACH THE FOOTER.
     A sticky element is bounded by its containing block. This panel is the last child
     of #screenPayment, which is a grid item in .portal-main -- and .portal-footer sits
     AFTER .portal-main. So the panel's lowest possible edge is the end of its own
     column, which is above where the footer begins. Concretely: the panel's bottom is
     min(viewport bottom - 1rem, column bottom), and the column bottom is at or above
     the footer's top, so the two bands cannot intersect at ANY scroll offset or
     viewport height. The panel stops sticking exactly when its column ends, which is
     the requirement.

     That is a structural guarantee, not a tuned number, and the thing that would break
     it is moving this bar OUT of #screenPayment (e.g. making it a child of
     .portal-main, or position:fixed at this width). Don't. Verified by scanning every
     scroll offset at 1024x700 and at 1024x450: zero footer intersection.

     NOTE this is also why the fixed (<900px) bar needed a separate fix -- a fixed bar
     has no containing block to bound it, which is why the footer reserve above exists.
     Two bars, two different mechanisms, same guarantee. */
  body.two-col #screenPayment .sticky-bar {
    position: sticky;
    top: auto; left: auto; right: auto; bottom: 1rem;
    z-index: 1;
    margin-top: 1rem;
    background: var(--white);
    border: 1px solid var(--border);
    border-top: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: 0 6px 24px rgba(13,27,53,0.10);
    padding: 1rem 1.15rem;

    /* No height cap here, deliberately -- see .sticky-bar--tall below. */
  }

  /* THE PANEL MUST NEVER BE TALLER THAN THE VIEWPORT WHILE IT IS STUCK (2026-09-19).
     A bottom-stuck sticky element is pinned by its BOTTOM edge, so once its own height
     exceeds the viewport the browser holds that bottom at `viewport bottom - 1rem` and
     pushes the top ABOVE the window -- where no scrolling reaches it, because a stuck
     element does not move with the page. Reported live on a deferred 12-payment plan.

     The first fix capped it with max-height + overflow-y: auto. That traded one problem
     for another: the panel became scrollable-by-a-hair on windows where it very nearly
     fit, with no visible scrollbar on platforms that use overlay ones -- content the
     debtor cannot see and has no cue to look for. On a panel whose job is to state the
     terms being authorized, a silent 12px of hidden text is worse than the layout bug.

     So it is never internally scrollable. When it does not fit, it stops being sticky
     and rejoins the flow, and the PAGE scrolls -- which reaches every part of it and is
     the behaviour a debtor already understands. renderStickyFooter() measures and sets
     the class; CSS cannot ask an element whether it fits. */
  body.two-col #screenPayment .sticky-bar--tall {
    position: static;
    bottom: auto;
    box-shadow: none;
  }
  body.two-col #screenPayment .sticky-bar-inner {
    max-width: none;
    flex-direction: column;
    align-items: stretch;
    gap: 0.75rem;
  }
  body.two-col .pay-summary {
    display: block;
    padding-bottom: 0.75rem;
    border-bottom: 1px solid var(--border);
  }
  body.two-col .pay-summary .sticky-terms { flex-direction: column; }
  body.two-col .pay-summary .sticky-due   { font-size: 1.15rem; white-space: normal; }

  /* ── The gate, two columns ───────────────────────────────────
     Gated on .gate-open (set by applyLayoutMode from S.gateOpen), NOT .two-col: the
     gate has no account and therefore no payment column, so it is a different layout
     that happens to share a breakpoint. See the note where .gate-open is toggled.

     .gate-form stops being `display: contents` here and becomes the real left column;
     the two helper sections are placed into column 2. Below 900px none of this exists.

     GRID PLACEMENT, NOT SOURCE ORDER. The helper precedes the form in the markup (see
     the note there -- the phone needs it above the fields) and the outcomes follow it,
     so column 2 is assembled here out of two items that are not adjacent in the DOM:
     the form spans both rows of column 1 and keeps its single uninterrupted block flow,
     while rows 1 and 2 of column 2 take the helper and the outcomes.

     THE FORM SPANS RATHER THAN SITTING IN ROW 1. If it sat in row 1, that row would be
     max(form, helper) and the outcomes would be pushed to below whichever is taller.
     Spanning keeps row 1 at exactly the helper's height, which is what puts the
     outcomes 1.5rem under the helper card -- the same 1.5rem it used to get from
     .gate-outcomes' margin-top, now supplied by the grid's row-gap. */
  body.gate-open .screen-inner { max-width: 880px; }

  body.gate-open #gatePanel {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    /* row column: 1.5rem replaces .gate-outcomes' old margin-top, 2rem is the
       column gutter this layout has always had. */
    gap: 1.5rem 2rem;
    align-items: start;
  }
  body.gate-open .gate-form     { display: block; grid-column: 1; grid-row: 1 / span 2; }
  body.gate-open .gate-helper   { grid-column: 2; grid-row: 1; }
  body.gate-open .gate-outcomes { grid-column: 2; grid-row: 2; margin-top: 0; }
}

/* Desktop defaults for the collapsible parts, stated BEFORE the max-width block that
   overrides them -- these carry no extra specificity, so source order is the only thing
   that decides, and putting them after would have silently un-collapsed the phone.

   The toggle never renders at >=900px, so its aria-expanded and aria-controls are not
   exposed to anything: display:none takes the button out of the accessibility tree
   entirely rather than leaving a control that claims to expand a section which is
   already open and offers no way to close it. */
.gate-collapse-head   { display: none; }
.gate-collapse-toggle { display: none; }
.gate-collapse-body   { display: block; }

/* ── Gate: helper sections ────────────────────────────────────
   Painted at EVERY width now: the right-hand column at >=900px, two collapsed
   disclosures below it. .gate-form is `display: contents` on a phone so its children
   lay out as direct children of #gatePanel, which is also what lets the helper sit
   between the top of the panel and the fields without a wrapper box in the way. */
.gate-form { display: contents; }

.gate-aside-title {
  font-size: 0.95rem; font-weight: 600; color: var(--navy);
  margin: 0 0 0.6rem; line-height: 1.35;
}
.gate-helper {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 1.15rem 1.25rem;
}
.gate-outcomes { margin-top: 1.5rem; padding: 0 0.15rem; }

/* Decorative stand-in for the letter. Scales with the column: at the 900px breakpoint
   the column is ~382px, and capping the graphic keeps it a diagram rather than a hero
   image, leaving the two explanatory sentences below it above the fold.

   220px, not 260px. The viewBox is 200x150, so the cap sets the height too (220/1.333 =
   165px). At 260 the graphic was ~195px tall and made the gate 22px taller than a
   1440x1000 viewport -- a screen that previously fitted without scrolling. It is
   supporting art for the sentence beneath it, so it gave up the pixels. */
.gate-letter {
  display: block; width: 100%; max-width: 220px; height: auto;
  margin: 0 auto 0.9rem; border-radius: var(--radius-sm);
}
.gl-page      { fill: var(--white); stroke: var(--border); stroke-width: 1.5; }
.gl-logo      { fill: var(--navy); }
.gl-line      { fill: var(--border); }
/* The point of the whole graphic: the account number's position on the page. Blue tint
   plus a solid blue rule for the value itself, so it reads as "this bit" without any
   text inside the SVG (which would not scale with the page font or translate). */
.gl-highlight { fill: rgba(26,109,216,0.10); stroke: var(--blue); stroke-width: 1.5; }
.gl-label     { fill: var(--muted); }
.gl-value     { fill: var(--blue); }

.gate-aside-note {
  font-size: 0.85rem; line-height: 1.55; color: var(--muted); margin: 0 0 0.6rem;
}
.gate-aside-note:last-child { margin-bottom: 0; }
.gate-aside-note a { color: var(--blue); text-decoration: underline; }

/* Monospace so "VEX-" reads as something to match against a printed page, not as prose. */
.gate-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.9em; color: var(--navy); font-weight: 600;
  background: rgba(26,109,216,0.08); border-radius: 3px; padding: 0.05em 0.3em;
}

.gate-outcome-list { list-style: none; margin: 0; padding: 0; }
.gate-outcome-list li {
  position: relative; padding-left: 1.5rem; margin-bottom: 0.55rem;
  font-size: 0.88rem; line-height: 1.5; color: var(--text);
}
/* A tick drawn in CSS -- no icon font dependency and nothing to fail to load. It is
   purely decorative punctuation for a list that already reads as a list. */
.gate-outcome-list li::before {
  content: ""; position: absolute; left: 0.15rem; top: 0.42em;
  width: 0.42rem; height: 0.72rem;
  border-right: 2px solid var(--blue); border-bottom: 2px solid var(--blue);
  transform: rotate(45deg);
}

/* ── Gate helper sections, accordions (below 900px only) ──────
   Scoped to a max-width query on purpose: every rule that makes these sections
   collapsible is unreachable at >=900px, so the desktop gate cannot be altered by any
   of it. The desktop side of the feature is the grid placement in the min-width block
   above and nothing else.

   LAST IN THE FILE, DELIBERATELY. Everything here overrides a base .gate-* rule of the
   same specificity -- the helper's card padding, the outcomes' margin, the title's
   margin -- so source order is the only thing deciding the winner. Written earlier in
   the file (where it reads better) the base padding won and the sections rendered with
   the wrong box.

   WHY NOT .detail-toggle. These reused it once and were rejected: that component is a
   centred, borderless, blue, chevron-suffixed text row, and it works on the overview
   screen ONLY because it sits inside an already-bordered card that supplies all the
   structure. Standing alone on a sparse screen the same treatment reads as two stray
   links, not as controls. So the section itself becomes the bordered object and the
   toggle becomes a full-width row inside it: label left, chevron hard right, 52px of
   tap target, body text colour rather than link blue -- the accordion shape people
   already recognise. The chevron still rotates off aria-expanded, matching
   .detail-toggle[aria-expanded="true"], because that idiom was never the problem. */
@media (max-width: 899.98px) {
  /* ORDER. #gatePanel becomes a flex column purely so one child can be moved: the
     Mini-Miranda lives inside .gate-form (which is display:contents here, so its
     children are this flex container's items) and is pushed past the accordions to the
     end of the screen. Everything else keeps source order at the default order:0.
     A disclosure belongs after the help, not wedged between the form and it. */
  #gatePanel { display: flex; flex-direction: column; }
  .mini-miranda-inline { order: 1; }

  /* Flex items do not collapse adjacent margins the way the block flow this replaced
     did, which silently added 3.2px above the error line and 8px above the submit
     button. Zeroing the smaller of each pair restores the exact gaps the form has
     always had -- 13.6px and 9.6px -- so the ONLY thing this rewrite moves on a phone
     is the help sections themselves. */
  #gateError { margin-top: 0; }
  #btnGate   { margin-top: 0; }

  /* THE PAIR. Both sections are the same object: same border, same radius, same
     surface, same width, stacked with one consistent gap and no stray margins, so they
     read as two halves of one help zone rather than two unrelated fragments.
     overflow:hidden clips the body's square top corners to the container's radius. */
  .gate-helper,
  .gate-outcomes {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 0;
    margin: 0;
    overflow: hidden;
  }
  /* Separation from the fallback line above, then the tighter intra-pair gap. */
  .gate-helper   { margin-top: 1.5rem; }
  .gate-outcomes { margin-top: 0.6rem; }

  /* One heading per section, never two: the static <h2> is the desktop one, the
     button-wrapped <h2> is this one. The hidden half is display:none, so no assistive
     technology ever meets the same title twice, and the phone keeps a real heading
     rather than a bare button. */
  .gate-title-static  { display: none; }
  .gate-collapse-head { display: block; margin: 0; font-size: inherit; font-weight: inherit; }

  .gate-collapse-toggle {
    display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
    width: 100%; min-height: 52px;          /* >=44px touch target, with room to spare */
    padding: 0.8rem 1.1rem;
    background: none; border: 0; cursor: pointer;
    font-family: inherit; font-size: 0.95rem; font-weight: 600; line-height: 1.35;
    color: var(--navy);                     /* a section heading, NOT a hyperlink */
    text-align: left;
  }
  .gate-collapse-toggle .ti-chevron-down {
    flex: none; font-size: 1.05rem; color: var(--muted);
    transition: transform 0.18s ease;
  }
  .gate-collapse-toggle[aria-expanded="true"] .ti-chevron-down { transform: rotate(180deg); }

  /* The body sits INSIDE the container it belongs to -- same inline padding as the
     header above it, a hairline rule tying the two together, nothing spilling below.

     NO BACKGROUND HERE. The section already paints --surface; repeating it on the body
     was a second identical fill for no reason. (It was never a doubled PADDING -- the
     section is padding:0 above -- but one surface is one surface.) */
  .gate-collapse-body { display: none; }
  .gate-collapse.open > .gate-collapse-body {
    display: block;
    margin: 0;
    padding: 1rem 1.1rem 1.1rem;
    border-top: 1px solid var(--border);
  }

  /* THE DIAGRAM OWNS ITS COLUMN. .gate-letter's 220px cap is a DESKTOP constraint: it
     stops the right-hand column making a 1440x1000 gate scroll. Inherited into a phone
     accordion it left a WHITE letter-page floating in 14px of tint on either side --
     which is what read as a box inside a box, and as a spot illustration rather than
     the explanatory diagram it is. Here it spans the content width. */
  .gate-collapse-body .gate-letter { max-width: none; }

  /* ONE RHYTHM for everything inside an open accordion. The helper was running three
     different gaps (16 / 14.4 / 9.6) against the outcomes list's uniform 8.8; both now
     sit on the 0.8rem step, which is the spacing .detail-row already establishes
     between rows elsewhere in the portal.

     Expressed as margin-TOP on every block after the first, not margin-bottom with a
     :last-child reset. The reset version zeroed the last <li>'s margin and the trailing
     note then butted straight against the list with a 0px gap -- the gap belongs
     BETWEEN siblings, so it is owned by the second one. Bottom margins are cleared so
     the body's own bottom padding is the only thing under the last block. */
  .gate-collapse-body > *     { margin-bottom: 0; }
  .gate-collapse-body > * + * { margin-top: 0.8rem; }
  .gate-collapse-body .gate-outcome-list li { margin-bottom: 0; }
  .gate-collapse-body .gate-outcome-list li + li { margin-top: 0.8rem; }
}

/* ── Card BIN gate ─────────────────────────────────────────────
   Appended to portal-v3.css. Shown ONLY when the first eight digits identify a
   credit or charge card, which Visa's MCC 7322 rules bar a collection agency
   from accepting. Never shown to confirm a good card -- see renderBinNotice().

   Styled as a refusal that comes with a door, not a wall: the red is the same
   --red used by every other error in this form so it reads as "this won't work"
   rather than "something broke", and the alternative sits INSIDE the notice so
   the next step is where the bad news is, not somewhere she has to go find it. */
.bin-block {
  display: flex; gap: 0.7rem; align-items: flex-start;
  background: rgba(229,52,74,0.06);
  border: 1px solid var(--red);
  border-radius: var(--radius);
  padding: 0.85rem 0.95rem;
  margin: -0.35rem 0 0.9rem;
}
.bin-block > i {
  color: var(--red); font-size: 1.15rem; line-height: 1.35; flex: 0 0 auto;
}
.bin-block-body { flex: 1 1 auto; min-width: 0; }
.bin-block-msg {
  color: var(--text); font-size: 0.9rem; line-height: 1.45; margin: 0;
}
/* The pivot. A real button, full-width on a phone, because this is the action we
   actually want her to take next -- not a footnote under the bad news. */
.bin-block-alt {
  display: inline-block; margin-top: 0.6rem;
  background: var(--blue); color: var(--white);
  border: none; border-radius: var(--radius-sm);
  padding: 0.6rem 0.95rem; min-height: 40px;
  font-family: inherit; font-size: 0.875rem; font-weight: 500;
  cursor: pointer;
}
.bin-block-alt:hover { background: #155cb8; }
.bin-block-alt:focus-visible { outline: 2px solid var(--navy); outline-offset: 2px; }

@media (max-width: 420px) {
  .bin-block-alt { display: block; width: 100%; text-align: center; }
}

/* ── Promotional offer banner ─────────────────────────────────
   Sits directly above "Choose how to resolve this". Present ONLY when the server
   returns a promo object; the page never decides on its own that a promotion is
   running, and never computes the price.

   TONE IS DELIBERATE. This is a debt collector, and a countdown is the single
   most abusable element on the page. It is navy-on-brand rather than red-alarm,
   the language states a fact ("ends at midnight") rather than manufacturing
   pressure, and there is no pulsing, flashing or shrinking-bar animation. If the
   deadline ever stops being literally true, remove the component -- do not soften
   the styling and keep the claim. */
.promo-banner {
  background: linear-gradient(135deg, var(--navy) 0%, #1B4B8F 65%, var(--blue) 130%);
  border-radius: var(--radius);
  padding: 0.9rem 1.1rem;
  margin-bottom: 0.85rem;
  color: #FFFFFF;
}
.promo-top {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.75rem; flex-wrap: wrap;
}
.promo-label {
  display: flex; align-items: center; gap: 0.4rem;
  font-size: 0.95rem; font-weight: 700; letter-spacing: 0.01em;
}
.promo-label .ti { color: #FFC857; font-size: 1rem; }

/* tabular-nums stops the countdown re-flowing its own width every second, which
   otherwise makes the whole banner twitch on each tick. */
.promo-clock {
  background: rgba(255,255,255,0.16);
  border-radius: 999px; padding: 0.22rem 0.7rem;
  font-size: 0.9rem; font-weight: 700; white-space: nowrap;
  font-variant-numeric: tabular-nums; font-feature-settings: "tnum";
}
.promo-sub {
  margin: 0.45rem 0 0; font-size: 0.85rem; line-height: 1.45;
  color: rgba(255,255,255,0.88);
}

/* Shown in place of the banner when the deadline passes and the page could not
   silently re-price itself (the no-token entry, where re-querying needs the
   account number and SSN again). Stale promotional prices must never sit on
   screen looking live. */
.promo-ended {
  background: #FFF8E6; border: 1px solid #F0DCA8; border-radius: var(--radius);
  padding: 0.85rem 1.05rem; margin-bottom: 0.85rem;
}
.promo-ended p { margin: 0; font-size: 0.88rem; color: #6B5410; line-height: 1.45; }
.promo-ended button {
  margin-top: 0.55rem; background: var(--navy); color: #fff; border: 0;
  border-radius: 8px; padding: 0.45rem 0.9rem; font: inherit;
  font-size: 0.85rem; font-weight: 600; cursor: pointer;
}
.promo-ended button:hover { background: #16294B; }

/* The settlement tile while a promotion is live. The tile is REPRICED IN PLACE --
   there is deliberately no second settlement tile at the ordinary price, because
   that would show the same product twice and one of them would always be the
   worse choice. This eyebrow is the only thing tying the tile to the banner. */
.promo-flag {
  display: inline-block;
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em;
  text-transform: uppercase; color: #8A5300;
  background: rgba(255,200,87,0.22);
  border-radius: 999px; padding: 0.18rem 0.55rem;
  margin-bottom: 0.5rem;
  /* The label is DATA (promotions.label), so its length is not ours to assume.
     "Limited Time Settlement Offer" is 29 characters uppercase and does not fit a
     one-line pill inside a tile on a 360px phone. inline-block + normal wrapping
     lets it become a two-line pill rather than overflowing the tile, which is what
     inline-flex with the default nowrap did. */
  white-space: normal; line-height: 1.3; max-width: 100%;
}

@media (max-width: 380px) {
  .promo-top { gap: 0.4rem; }
  .promo-label { font-size: 0.88rem; }
  .promo-clock { font-size: 0.84rem; padding: 0.2rem 0.55rem; }
}

/* =============================================================
   LOAN PARTNERS CARD — overview screen (added 2026-09-08)

   Fourth affiliate placement, under the offer tiles and above the sticky bar.
   Markup is built by loanCtaHTML() in portal-v3.js.

   It must never read as a fourth offer tile: the tiles are click-to-select and
   drive the sticky bar, so this uses its own component, its own divider and a
   tinted rather than filled surface, keeping it visibly subordinate to the
   settlement tile.
   ============================================================= */

/* The divider. Says "Or" because that is what this is -- another way to handle
   the balance, not another price for it. */
.loan-rule {
  display: flex; align-items: center; gap: 0.75rem; margin: 1.4rem 0 1rem;
  color: var(--dim); font-size: 0.75rem; letter-spacing: 0.06em;
  text-transform: uppercase; font-weight: 500;
}
.loan-rule::before,
.loan-rule::after { content: ""; flex: 1; height: 1px; background: var(--border); }

/* --- The card, and the light that runs its border -----------------------
   Structure: an SVG rounded-rect stroke sits behind the card body and IS the
   border. Three stacked paths share that one outline -- a static base, a soft
   tail and a bright head -- and the two moving ones are animated by
   stroke-dashoffset, so the light travels the perimeter at one constant speed
   and covers every side and both corners equally.

   Two things here are load-bearing and were each a bug once:

   1. The 2px band is PADDING on .loan-cta, never a margin on .loan-cta-in.
      With a margin, the inner surface's LEFT and RIGHT margins behave but its
      TOP and BOTTOM margins collapse straight through .loan-cta (no border, no
      padding, nothing to stop them), so the surface sits flush against the card
      edge and paints over the stroke on exactly two sides -- and the light then
      appears to run only the left and right edges. Padding cannot collapse.

   2. The outline is a <path>, not a <rect>, because pathLength normalisation is
      what keeps the dash figures below independent of card size.

   Nothing here uses @property, Houdini or a mask. An engine that cannot run the
   animation simply paints the static base stroke, which is the border. */
.loan-cta {
  position: relative; isolation: isolate;
  border-radius: var(--radius);
  padding: 2px;                          /* the band the stroke lives in */
  --lap: 6s;                             /* one circuit; set by portal-v3.js */
}
.loan-rim {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%; overflow: visible;
  pointer-events: none;
}
.loan-rim path { fill: none; }

.rim-base { stroke: #DCE7F7; stroke-width: 2; }   /* resting border + fallback */

/* A long soft tail with a short bright head running 13 units in front of it. */
.rim-tail {
  stroke: rgba(41,140,255,0.42); stroke-width: 2; stroke-linecap: round;
  stroke-dasharray: 17 83;
  animation: vexLoanTail var(--lap) linear infinite;
}
.rim-head {
  stroke: #8FCDFF; stroke-width: 2; stroke-linecap: round;
  stroke-dasharray: 5 95;
  animation: vexLoanHead var(--lap) linear infinite;
}
@keyframes vexLoanTail { from { stroke-dashoffset: 0; }   to { stroke-dashoffset: -100; } }
@keyframes vexLoanHead { from { stroke-dashoffset: -13; } to { stroke-dashoffset: -113; } }

/* Lap-limited mode. Off by default (LOAN_CTA_LAPS = 0 in portal-v3.js runs the
   light forever); set that to a positive number and the .laps class plus the
   --laps property fade the light out after that many circuits, leaving the
   plain border behind. */
.loan-cta.laps .rim-tail {
  animation: vexLoanTail var(--lap) linear var(--laps, 3),
             vexLoanRest 0.9s ease calc(var(--lap) * var(--laps, 3) - 0.3s) forwards;
}
.loan-cta.laps .rim-head {
  animation: vexLoanHead var(--lap) linear var(--laps, 3),
             vexLoanRest 0.9s ease calc(var(--lap) * var(--laps, 3) - 0.3s) forwards;
}
@keyframes vexLoanRest { to { opacity: 0; } }

/* Non-negotiable. Anyone who has asked their device for less motion gets a
   plain border, and the card loses nothing: the light carries no information
   the copy does not already state, which is also why the SVG is aria-hidden. */
@media (prefers-reduced-motion: reduce) {
  .rim-tail, .rim-head { display: none; }
  .rim-base { animation: none; }
}

.loan-cta-in {
  position: relative; z-index: 2;
  border-radius: calc(var(--radius) - 2px);
  background: linear-gradient(180deg, #F7FAFF 0%, #EFF5FE 100%);
  padding: 1.15rem 1.2rem;
}
.loan-cta-head { display: flex; align-items: center; gap: 0.55rem; }
.loan-ico {
  width: 30px; height: 30px; flex: none; border-radius: 8px;
  background: rgba(26,109,216,0.12); color: var(--blue);
  display: grid; place-items: center;
}
.loan-ico .ti { font-size: 1rem; }
.loan-cta-title {
  font-size: 1.02rem; font-weight: 700; color: var(--navy); letter-spacing: -0.01em;
}
.loan-cta-sub {
  color: var(--muted); font-size: 0.9rem; line-height: 1.5; margin-top: 0.55rem;
}
/* The three facts that replaced a promise we could not keep. They wrap to two
   rows on a narrow phone rather than shrinking; none of them may be dropped. */
.loan-facts {
  display: flex; flex-wrap: wrap; gap: 0.35rem 0.9rem; margin-top: 0.7rem;
  font-size: 0.78rem; color: var(--muted);
}
.loan-facts span { display: inline-flex; align-items: center; gap: 0.3rem; }
.loan-facts .ti { color: var(--green); font-size: 0.85rem; }

/* .btn-portal is not reused here: it is 100% wide with a top margin tuned for
   the sticky bar, and its blue is the same blue as the settlement tile's
   selected state. This button is the same size but its own component. */
.loan-btn {
  display: flex; align-items: center; justify-content: center; gap: 0.45em;
  width: 100%; margin-top: 0.9rem;
  background: var(--blue); color: var(--white);
  border-radius: var(--radius-sm); padding: 13px 20px;
  font-size: 0.92rem; font-weight: 600; line-height: 1;
  box-shadow: 0 1px 3px rgba(26,109,216,0.2);
  text-decoration: none;
}
/* The label is white and stated twice on purpose. portal.css:39 sets a global
   `a { color: var(--blue) }`; a bare `.loan-btn` (0,1,0) does beat that type
   selector (0,0,1), so this is belt-and-braces rather than a live fix -- but
   the mockup for this card carried a scoped `.vex a` rule that DID win, and the
   button rendered blue text on a blue fill, completely invisible. Any future
   rule of the form `<something> a` in this file would do the same. Keeping
   `a.loan-btn` here means no descendant-anchor rule can take this label back
   without also naming the class. */
.loan-btn,
a.loan-btn { color: var(--white); }
.loan-btn:hover {
  background: #155fc0; color: var(--white); text-decoration: none;
  box-shadow: 0 4px 14px rgba(26,109,216,0.3);
}
.loan-btn:focus-visible { outline: 2px solid var(--navy); outline-offset: 2px; }

/* Required disclosure, mirroring the counsel-approved banner at the top of
   loans.vexrecovery.com. It is here because this placement sits inside the
   collection decision, not beside it. Do not move it behind a toggle. */
.loan-disc {
  font-size: 0.72rem; color: var(--dim); line-height: 1.5;
  margin-top: 0.7rem; text-align: center;
}

/* ── V4: first payment date control (spec 8.4, D11/D12) ─────────────────────
   A small outlined button beside "$X today", greyed out with a reason under a
   promotion. One panel: a bottom sheet below 900px, a popover above the button
   at 900px and up (TWO_COL_MIN in portal-v4.js). */
.sticky-due-row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; min-width: 0; }
.date-btn {
  display: inline-flex; align-items: center; gap: 0.25rem;
  padding: 0.2rem 0.55rem; border: 1px solid var(--border); border-radius: 999px;
  background: var(--white); color: var(--blue); font: 500 0.75rem/1.2 var(--font);
  cursor: pointer; white-space: nowrap;
}
.date-btn:hover { border-color: var(--blue-lt); }
.date-btn:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
.date-btn[disabled] { color: var(--dim); background: var(--surface); cursor: not-allowed; }
.date-btn .ti { font-size: 0.9rem; }
.date-btn-reason, .date-notice { font-size: 0.75rem; line-height: 1.3; }
.date-btn-reason { color: var(--muted); }
.date-notice { color: var(--navy); font-weight: 500; }

.fd-backdrop { position: fixed; inset: 0; background: rgba(13, 27, 53, 0.35); z-index: 90; }
.fd-panel {
  position: fixed; z-index: 91; background: var(--white); color: var(--text);
  font-family: var(--font); box-shadow: 0 12px 40px rgba(13, 27, 53, 0.18);
  padding: 1rem 1rem 0.75rem; max-height: 85vh; overflow-y: auto; box-sizing: border-box;
}
.fd-panel--sheet {
  left: 0; right: 0; bottom: 0; border-radius: 16px 16px 0 0;
  padding-bottom: calc(0.75rem + env(safe-area-inset-bottom));
}
/* WIDE, NOT TALL (2026-09-19). At 360px the panel was a single column -- calendar, then
   keep, notice, a schedule preview of up to twelve rows, then the footer -- which on a
   laptop window ran taller than the space above the button and had to scroll internally
   once a date was picked. Scrolling a date picker to reach "Use this date" is a poor
   control, and the preview is the whole point of picking a date.

   620px puts the calendar and the schedule side by side, which roughly halves the
   height: the panel is governed by whichever column is taller rather than by their sum.
   The mobile sheet is untouched -- it is full-width and stacks, where a tall column is
   the right shape.

   positionFirstDatePopover() reads the rendered width rather than assuming one, so this
   number lives here alone. */
.fd-panel--popover {
  width: 620px; border-radius: var(--radius); border: 1px solid var(--border);
  display: grid;
  grid-template-columns: 290px minmax(0, 1fr);
  column-gap: 1.15rem;
  align-content: start;
}
.fd-panel--popover .fd-head,
.fd-panel--popover .fd-sub,
.fd-panel--popover .fd-foot { grid-column: 1 / -1; }
.fd-panel--popover .fd-cal  { grid-column: 1; align-self: start; }
.fd-panel--popover .fd-side { grid-column: 2; align-self: start; min-width: 0; }
/* The side column starts level with the calendar, so its children drop the top margins
   they need when stacked in the sheet, and the column spaces itself with `gap` instead.

   Flex rather than `:first-child`: the "Keep <date>" row is hidden via display:none on
   the token paths that have no collector date beyond the window, and :first-child still
   MATCHES a hidden element -- so the preview would keep a top margin it should not have
   and sit a few pixels lower than the calendar beside it. A flex column drops a
   display:none child from layout entirely, and `gap` only applies between what is
   actually visible, so either arrangement lines up. */
.fd-panel--popover .fd-side { display: flex; flex-direction: column; gap: 0.75rem; }
.fd-panel--popover .fd-side > * { margin-top: 0; }
.fd-head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.fd-head h2 { font-size: 1rem; font-weight: 600; margin: 0; }
.fd-close { border: 0; background: none; color: var(--muted); font-size: 1.1rem; cursor: pointer; padding: 0.25rem; }
.fd-sub { font-size: 0.8rem; color: var(--muted); margin: 0.25rem 0 0.75rem; }
.fd-weekdays, .fd-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 0.25rem; }
.fd-weekdays { margin-bottom: 0.25rem; }
.fd-weekdays span { text-align: center; font-size: 0.7rem; color: var(--dim); font-weight: 600; }
.fd-day {
  position: relative; height: 2.25rem; border-radius: var(--radius-sm);
  border: 1px solid var(--border); background: var(--white); color: var(--text);
  font: 500 0.85rem var(--font); cursor: pointer; padding: 0;
}
.fd-day--off {
  display: flex; align-items: center; justify-content: center;
  border-color: transparent; color: var(--border); cursor: default;
}
.fd-day--today { border-color: var(--blue-lt); }
.fd-day--friday::after {
  content: ''; position: absolute; left: 50%; bottom: 3px; width: 4px; height: 4px;
  margin-left: -2px; border-radius: 50%; background: var(--blue);
}
.fd-day.is-selected { background: var(--blue); border-color: var(--blue); color: var(--white); }
.fd-day.is-selected::after { background: var(--white); }
.fd-day:focus-visible, .fd-keep-btn:focus-visible, .fd-close:focus-visible {
  outline: 2px solid var(--blue); outline-offset: 2px;
}
.fd-keep { margin-top: 0.75rem; }
.fd-keep-btn {
  width: 100%; text-align: left; padding: 0.5rem 0.75rem; border-radius: var(--radius-sm);
  border: 1px solid var(--border); background: var(--white); color: var(--text);
  font: 600 0.85rem var(--font); cursor: pointer;
}
.fd-keep-btn span { display: block; font-weight: 400; font-size: 0.75rem; color: var(--muted); }
.fd-keep-btn.is-selected { border-color: var(--blue); box-shadow: inset 0 0 0 1px var(--blue); }
/* Sits under the calendar grid in both layouts. The 0.75rem top margin below already
   gives it the gap it needs there -- no separate rule required. */
.fd-notice {
  margin: 0.75rem 0 0; padding: 0.5rem 0.75rem; border-radius: var(--radius-sm);
  background: var(--surface); border-left: 3px solid var(--blue); font-size: 0.8rem; color: var(--navy);
}
.fd-preview { margin-top: 0.75rem; }
.fd-preview-head { font-size: 0.8rem; font-weight: 600; margin: 0 0 0.25rem; }
.fd-preview ul { list-style: none; margin: 0; padding: 0; }
.fd-preview li { display: flex; justify-content: space-between; font-size: 0.8rem; padding: 0.15rem 0; color: var(--muted); }
.fd-preview li.fd-more { justify-content: flex-start; }
.fd-call { font-size: 0.75rem; color: var(--muted); margin: 0.75rem 0 0; }
.fd-call a { color: var(--blue); }
.fd-foot { display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 0.75rem; }
