/* =========================================================================
   Veteran Strong — PORTAL MOBILE LAYER  (≤768px)
   Loaded after portal.css / pages.css on every portal page and on the auth
   pages (which use the same .portal-* component bank).

   Held to the standard approved on 2026-07-26 — see the "Mobile home
   redesign" section of components.md — and built sprint by sprint per
   mobile-portal-sprints.md. Desktop is never touched: every rule lives
   inside @media (max-width: 768px).

   Sprint P1, step 1: the canonical .portal-* components adapted at the
   breakpoint. NOT page-scoped tweaks — that distinction is the whole point.
   An earlier revision expressed the same intent by prefixing each canonical
   with the page's own root class, and audit-components.sh correctly flagged
   14 violations of the component bank. A responsive adaptation belongs on
   the canonical once, so every page inherits the same behaviour and no page
   fights the bank (CLAUDE.md Rule 3).
   ========================================================================= */

/* Self-contained token set. This layer must not depend on
   marketing-mobile.css — portal pages do not load it, so referencing its
   --vsm-* tokens here silently produced invalid declarations and every rule
   below was a no-op. Values match the approved mobile standard; where the
   portal already has an equivalent token (--portal-*) it is reused. */
:root {
  --pm-2: 8px;
  --pm-3: 12px;
  --pm-4: 16px;
  --pm-5: 20px;
  --pm-6: 24px;
  --pm-8: 32px;
  --pm-10: 40px;

  --pm-gutter: 20px;
  --pm-bar: 56px;            /* app-bar content height */
  --pm-tap: 48px;            /* minimum interactive box */
  --pm-cta: 56px;            /* primary action height */

  --pm-r-input: 12px;
  --pm-r-card: 16px;

  --pm-ease: cubic-bezier(0.23, 1, 0.32, 1);

  /* Tick glyph as a mask, so it can take `currentColor` and follow the row
     it marks instead of shipping a second coloured SVG. */
  --pm-tick: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 12.5 L9.5 18 L20 6.5'/%3E%3C/svg%3E");
  /* Down-caret, same masking trick, for disclosure rows. */
  --pm-caret: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 9 L12 16 L19 9'/%3E%3C/svg%3E");
}

/* --------------------------------------------------------------------------
   Muted ink is too light for its own floor. --portal-ink-muted is #7a7d75,
   which is 4.19:1 on white — under WCAG AA (4.5) for normal text, and it is
   used for stat labels, sublabels and timestamps all over the portal. On a
   phone, held at arm's length by a 70-year-old, that is the text most likely
   to be lost.
   #565951 is 7.14:1 on white — AAA for normal text — and keeps the warm
   neutral cast, so the hierarchy against --portal-ink (#15171a, 18:1) and
   --portal-ink-secondary (#4a4d47, 8.6:1) still reads. Scoped to the
   breakpoint: desktop keeps its own value.
   -------------------------------------------------------------------------- */
@media (max-width: 768px) {
  :root { --portal-ink-muted: #565951; }
}

/* =========================================================================
   PORTAL CANONICALS  (≤768px)  —  Portal Sprint P1, step 1

   These are the canonical .portal-* components adapted at the breakpoint,
   NOT page-scoped tweaks. That distinction is the whole point: an earlier
   revision expressed the same intent by prefixing each canonical with the
   page's own root class, and audit-components.sh correctly flagged 14
   page-scoped overrides of the component bank. A responsive adaptation
   belongs on the canonical once, so every page that uses it inherits the
   same behaviour and no page fights the bank (CLAUDE.md Rule 3).

   Scope note: these rules reach every portal page, which is intended —
   larger targets and non-truncating labels are correct everywhere, and the
   portal sprints build on this foundation rather than re-deciding it.
   ========================================================================= */
@media (max-width: 768px) {
  /* Buttons: the 65+ action height and press feedback, everywhere. */
  .portal-btn {
    min-height: var(--pm-cta);
    font-size: var(--portal-text-body);
    border-radius: var(--pm-r-input);
    transition: transform 140ms var(--pm-ease), background-color 200ms ease;
  }
  .portal-btn:active { transform: scale(0.98); }
  /* --block is already full width; make sure it reads as the primary action. */
  .portal-btn--block { width: 100%; justify-content: center; }

  /* Brand lock-up is a link — a real target on both axes. */
  .portal-brand {
    min-height: var(--pm-tap);
    display: inline-flex;
    align-items: center;
  }

  /* Stepper: labels wrap inside their track instead of overflowing it.
     `white-space: nowrap` on a ~105px grid track is what made three labels
     run together ("Verify phoneCreate password"). Hiding them — the previous
     workaround — leaves bare circles and no idea what any step is. 14px is
     the portal's own documented floor for metadata, which is what these are. */
  .portal-stepper--vertical { gap: var(--pm-2); }
  .portal-stepper__item { min-width: 0; }
  .portal-stepper__btn { min-width: 0; width: 100%; }
  .portal-stepper__label {
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
    text-align: center;
    max-width: 100%;
    font-size: var(--portal-text-caption);
    line-height: 1.25;
  }
}

/* =========================================================================
   APP BAR + MENU SHEET  (≤768px)  —  Portal Sprint P1

   Desktop renders a 3-column grid: brand · up to 6 inline links (one of them
   a hover dropdown) · messages + avatar + "Account" + chevron. At 375px that
   is ~14 tap targets in one row, and the dropdown needs hover, so the credit
   sections are unreachable entirely.

   Mobile: [brand] ······ [avatar] [Menu]. Everything else moves into the
   full-screen sheet that portal-mobile-nav.js builds from the live nav.
   ========================================================================= */

/* Injected by JS on every viewport — inert above the breakpoint. */
.portal-nav__menu-btn,
.portal-menu { display: none; }

@media (max-width: 768px) {
  /* ---- Seam kill ----
     Both bars were showing a 1px strip of page against the screen edge. It
     is not a layout gap — the nav measures top:0 and the tab bar's bottom
     equals innerHeight. It is what shows THROUGH: at fractional device pixel
     ratios the fixed/sticky box rounds a hair short, and on iOS a rubber-band
     overscroll drags the page past the bar entirely. Matching the document
     canvas to the bars means the sliver, when it happens, is the same colour
     as the bar and no longer reads as a gap. */
  html { background: var(--portal-surface); }

  /* The nav is sticky on desktop; keep that, it costs nothing and the bar is
     the only way back out of a page. */
  .portal-nav {
    /* Tuck away on the way down, come back on the way up. Transform, not
       `top`, so the measured height stays put for --vs-nav-h. */
    transition: transform 240ms var(--pm-ease);
    will-change: transform;
  }
  .portal-nav--tucked { transform: translateY(-100%); }
  @media (prefers-reduced-motion: reduce) {
    .portal-nav { transition: none; }
    .portal-nav--tucked { transform: none; }
  }

  .portal-nav__inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--pm-2);
    max-width: none;
    padding: var(--pm-2) var(--pm-gutter);
    min-height: var(--pm-bar);
  }

  /* Inline links and the hover dropdown live in the sheet now. */
  .portal-nav__links { display: none; }

  /* Once the wordmark hides (<390px) the brand is just a 36px mark, which
     leaves the link 44px wide — under the floor on the horizontal axis. */
  .portal-nav__brand { gap: 10px; min-height: var(--pm-tap); min-width: var(--pm-tap); }
  .portal-nav__brand-mark { width: 34px; height: 26px; }
  .portal-nav__brand-word { font-size: var(--portal-text-caption); letter-spacing: 0.12em; }

  .portal-nav__right {
    display: flex;
    align-items: center;
    gap: var(--pm-2);
  }
  /* The messages icon takes the avatar's slot (2026-07-27). It is the one
     thing in this bar a member acts on rather than navigates with, and it
     was previously buried as a sheet row. */
  .portal-nav__icon-btn {
    display: inline-grid;
    place-items: center;
    width: var(--pm-tap);
    height: var(--pm-tap);
    border-radius: var(--pm-r-input);
    color: var(--portal-ink);
    background: transparent;
    transition: transform 140ms var(--pm-ease), background-color 180ms ease;
  }
  .portal-nav__icon-btn:active {
    transform: scale(0.95);
    background: rgba(14, 15, 12, 0.08);
  }
  .portal-nav__icon-btn svg { display: block; }

  /* NOTE ON NAVIGATION MODEL
     The bottom tab bar covers the three destinations a member uses daily —
     Home · Credit · Benefits. This sheet is the OVERFLOW: My Assets,
     Concierge, Support, Referral, the credit sections and the Member ID.
     Between them nothing is unreachable, and Concierge additionally has the
     messages icon in this bar. */

  /* The whole account cluster goes (2026-07-27). The avatar was a decorative
     initials chip that opened a dropdown we already suppress, and Account
     lives in the sheet. Its slot is the messages icon now. */
  .portal-nav__account-wrap { display: none; }

  .portal-nav__menu-btn {
    appearance: none;
    display: inline-grid;
    place-items: center;
    width: var(--pm-tap);
    height: var(--pm-tap);
    padding: 0;
    background: rgba(14, 15, 12, 0.06);
    border: 0;
    border-radius: var(--pm-r-input);
    color: var(--portal-ink);
    cursor: pointer;
    flex-shrink: 0;
    transition: transform 140ms var(--pm-ease), background-color 180ms ease;
  }
  .portal-nav__menu-btn:active { transform: scale(0.95); background: rgba(14, 15, 12, 0.10); }
  .portal-nav__menu-btn svg { display: block; }

  /* Narrow phones: the wordmark yields, the mark carries the brand. */
  @media (max-width: 389px) {
    .portal-nav__brand-word { display: none; }
  }

  /* ---- Bottom tab bar: three destinations ----
     Home · Credit · Benefits. Concierge and Account left the bar — five
     labelled tabs never fit ("Concierge" alone wanted ~66px of a 64px tab
     at 320px), and the old answer was a rule that hid Account below 360px,
     i.e. a destination that silently vanished on the narrowest phones.
     Three tabs give ~130px each, so the label can sit at the interactive
     size instead of the caption floor and the active state can be a real
     shape rather than a colour change. */
  .portal-tabbar {
    padding-bottom: max(env(safe-area-inset-bottom, 0px), 0px);
  }
  /* Paints 3px of the bar's own background BELOW itself. Covers the
     subpixel seam at dpr 3 and the first moments of an overscroll, without
     nudging the bar off the bottom edge. */
  .portal-tabbar::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    height: 3px;
    background: var(--portal-surface);
    pointer-events: none;
  }
  .portal-tabbar__item {
    min-height: 64px;
    gap: 5px;
    padding: var(--pm-2) var(--pm-2) var(--pm-3);
    border-radius: var(--pm-r-card);
  }
  /* The icon sits in a pill that fills when the tab is current. Colour
     alone is a weak signal at arm's length, and it is the only signal a
     colour-blind member gets. */
  .portal-tabbar__icon {
    display: inline-grid;
    place-items: center;
    width: 56px;
    height: 32px;
    border-radius: 999px;
    background: transparent;
    transition: background-color 200ms ease, color 200ms ease;
  }
  .portal-tabbar__icon svg { width: 24px; height: 24px; }
  .portal-tabbar__label {
    font-size: var(--portal-text-label);
    font-weight: 600;
    line-height: 1;
  }
  .portal-tabbar__item--active .portal-tabbar__icon {
    background: var(--portal-olive);
    color: #fff;
  }
  .portal-tabbar__item--active { color: var(--portal-olive); }
  @media (prefers-reduced-motion: reduce) {
    .portal-tabbar__icon { transition: none; }
  }
  /* Content clears the taller bar. */
  body.has-portal-tabbar {
    padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px));
  }
  body.has-portal-tabbar .bnft-wiz__footer {
    bottom: calc(72px + env(safe-area-inset-bottom, 0px));
  }

  /* ---- Onboarding bar, INCOMPLETE state (the default) ----

     234px on a 390×844 phone — a third of the usable canvas, spent above
     every portal page's own first line. portal.css's 768px block stacks the
     desktop composition to one column but keeps every part of it: title,
     body, meter, counter, badges (hidden), and a 52px CTA, at 20/24 padding
     with a 12px gap between all six. On the benefit pages the result was
     396px of nav + banner + crumbs before the H1's first word.

     `display: contents` on the two layout wrappers lifts their children into
     this grid, which is the only way the meter and the counter can share a
     row — they live in different subtrees (`__left` and `__action`), so no
     amount of ordering inside either one could pair them. Composition:

         COMPLETE YOUR PROFILE
         Unlock your free benefits — 2 quick steps left.
         ▓▓▓░░░░░░  33%          1 of 3 earned
         [ Complete profile → ]

     Every string survives, including the CTA label: the whole bar is already
     one <button>, but an explicit labelled action is the 65+ rule and hiding
     a label would be a copy change. ~179px. */
  .portal-onboarding-bar__inner:not(.portal-onboarding-bar__inner--complete) {
    grid-template-columns: minmax(0, 1fr) auto !important;
    align-items: center;
    column-gap: var(--pm-3) !important;
    row-gap: var(--pm-2) !important;
    padding: var(--pm-4) var(--pm-gutter) !important;
  }
  .portal-onboarding-bar__left,
  .portal-onboarding-bar__inner:not(.portal-onboarding-bar__inner--complete)
    .portal-onboarding-bar__action {
    display: contents;
  }
  .portal-onboarding-bar__title,
  .portal-onboarding-bar__body { grid-column: 1 / -1; }
  /* The title stays at 18px: it is tracked 0.16em caps, and at 20px
     "COMPLETE YOUR PROFILE" wraps on a 360px screen. */
  /* portal.css gives the meter a 12px top margin for the desktop stack; the
     grid owns vertical rhythm here. */
  .portal-onboarding-bar__meter {
    grid-column: 1;
    margin-top: 0;
    min-width: 0;
  }
  .portal-onboarding-bar__inner:not(.portal-onboarding-bar__inner--complete)
    .portal-onboarding-bar__counter {
    grid-column: 2;
    justify-self: end;
    white-space: nowrap;
  }
  .portal-onboarding-bar__inner:not(.portal-onboarding-bar__inner--complete)
    .portal-onboarding-bar__cta {
    grid-column: 1 / -1;
    margin-top: var(--pm-2);
  }

  /* ---- Onboarding bar, complete / promo state ----

     portal.css carries TWO responsive blocks for this strip and they
     disagree. The 720px block lays it out as `auto 1fr auto` and places
     the copy at `grid-column: 2`; the 768px block, later in the file,
     forces `grid-template-columns: 1fr !important` and hides the medal.
     The later one wins the column count, but the stale placements survive
     — so the copy is dealt into a phantom second column, the CTA into a
     phantom second row, and the dismiss ✕ ends up over the title.

     Restated here as one composition, matching the incomplete bar the
     member has already seen: block of copy, then a full-width action.
     `!important` because the declaration it has to beat carries it. */
  .portal-onboarding-bar__inner--complete {
    display: grid !important;
    grid-template-columns: minmax(0, 1fr) !important;
    grid-template-rows: auto !important;
    row-gap: var(--pm-3) !important;
    padding: var(--pm-5) var(--pm-gutter) var(--pm-6) !important;
    position: relative;
  }
  .portal-onboarding-bar__inner--complete > * {
    grid-column: 1 !important;
    grid-row: auto !important;
  }
  /* The medal is the state's identity — the incomplete bar has no icon, so
     this is what tells you at a glance that something changed. Its own row
     at 40px, not a column fighting the copy for width. */
  .portal-onboarding-bar__medal {
    display: inline-grid !important;
    place-items: center;
    width: 44px;
    height: 44px;
    justify-self: start;
  }
  .portal-onboarding-bar__medal svg { width: 24px; height: 24px; }
  /* Keep the title clear of the dismiss target in the corner. */
  .portal-onboarding-bar__copy { padding-right: 44px; min-width: 0; }
  .portal-onboarding-bar--complete .portal-onboarding-bar__title {
    font-size: var(--portal-text-card-title);
    line-height: 1.25;
  }
  .portal-onboarding-bar--complete .portal-onboarding-bar__body {
    font-size: var(--portal-text-label);
    line-height: 1.45;
  }
  .portal-onboarding-bar--complete .portal-onboarding-bar__cta {
    justify-self: stretch;
    width: 100%;
    justify-content: center;
    min-height: 52px;
  }
  .portal-onboarding-bar__dismiss {
    position: absolute;
    top: var(--pm-3);
    right: var(--pm-3);
    width: var(--pm-tap);
    height: var(--pm-tap);
  }

  /* ---- Sheet ---- */
  .portal-menu[hidden] { display: none; }   /* author display must not defeat `hidden` */
  .portal-menu {
    position: fixed;
    inset: 0;
    z-index: 200;
    display: flex;
    flex-direction: column;
    background: var(--portal-surface, #fff);
    padding:
      calc(env(safe-area-inset-top, 0px) + var(--pm-2))
      var(--pm-gutter)
      calc(env(safe-area-inset-bottom, 0px) + var(--pm-6));
    opacity: 0;
    transition: opacity 200ms var(--pm-ease);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  .portal-menu.is-open { opacity: 1; }
  /* Above the tab bar (z-index 40) so the sheet is not sliced by it. */
  body:has(.portal-menu.is-open) .portal-tabbar { display: none; }

  /* Header: who you are, then your ID. Replaces the word "Menu" — a sheet
     covering the screen with a list of destinations does not need to be
     told it is a menu, and the Member ID is what a member actually reads
     out to a partner. It used to sit in a footer under every row. */
  .portal-menu__bar {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--pm-4);
    min-height: var(--pm-bar);
    margin-bottom: var(--pm-6);
  }
  .portal-menu__id { min-width: 0; }
  .portal-menu__welcome {
    margin: 0 0 2px;
    font-family: var(--portal-font-body);
    font-weight: 600;
    font-size: var(--portal-text-caption);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--portal-olive);
  }
  .portal-menu__name {
    margin: 0;
    font-family: var(--portal-font-body);
    font-weight: 600;
    font-size: var(--portal-text-hero);
    letter-spacing: -0.5px;
    line-height: 1.1;
    color: var(--portal-ink);
  }
  .portal-menu__close {
    appearance: none;
    width: var(--pm-tap);
    height: var(--pm-tap);
    border: 0;
    border-radius: var(--pm-r-input);
    background: rgba(14, 15, 12, 0.06);
    color: var(--portal-ink);
    display: inline-grid;
    place-items: center;
    cursor: pointer;
    transition: transform 140ms var(--pm-ease), background-color 180ms ease;
  }
  .portal-menu__close:active { transform: scale(0.95); background: rgba(14, 15, 12, 0.10); }
  .portal-menu__close svg { display: block; }

  .portal-menu__nav { display: flex; flex-direction: column; gap: var(--pm-6); }
  .portal-menu__group { display: flex; flex-direction: column; }
  .portal-menu__group-title {
    margin: 0 0 var(--pm-2);
    font-family: var(--portal-font-body);
    font-weight: 700;
    font-size: var(--portal-text-caption);
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--portal-ink-muted);
  }
  .portal-menu__row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--pm-4);
    min-height: 60px;
    padding: 0 4px;
    border-bottom: 1px solid var(--portal-hairline, rgba(14, 15, 12, 0.08));
    text-decoration: none;
    color: var(--portal-ink);
    font-family: var(--portal-font-body);
    font-size: var(--portal-text-section);
    font-weight: 500;
    letter-spacing: -0.3px;
  }
  .portal-menu__row--active { color: var(--portal-olive, #576038); font-weight: 600; }
  .portal-menu__row-chev { color: var(--portal-ink-muted); display: inline-flex; }

  /* Accordion parent — a <button>, so it needs the row's look reset onto it. */
  .portal-menu__row--parent {
    appearance: none;
    width: 100%;
    background: none;
    text-align: left;
    cursor: pointer;
    /* A <button> keeps the UA's outset border on the three sides the row's
       own `border-bottom` does not overwrite — which drew a box around the
       row. Reset all four, then restore the one the row wants. */
    border: 0;
    border-bottom: 1px solid var(--portal-hairline);
  }
  .portal-menu__row-caret {
    color: var(--portal-ink-muted);
    display: inline-flex;
    transition: transform 200ms var(--pm-ease);
  }
  .portal-menu__row--parent[aria-expanded="true"] .portal-menu__row-caret {
    transform: rotate(180deg);
  }
  .portal-menu__row--parent[aria-expanded="true"] { border-bottom-color: transparent; }
  /* Children are indented and quieter than their parent — they read as
     inside it, not as six more top-level destinations. */
  .portal-menu__panel[hidden] { display: none; }
  .portal-menu__panel {
    border-bottom: 1px solid var(--portal-hairline);
    padding-bottom: var(--pm-2);
    /* Height is driven from JS for the duration of the open/close; the
       curve is the drawer one used elsewhere in this layer. */
    transition: height 280ms cubic-bezier(0.32, 0.72, 0, 1);
  }
  @media (prefers-reduced-motion: reduce) {
    .portal-menu__panel { transition: none; }
  }
  .portal-menu__row--child {
    min-height: 52px;
    padding-left: var(--pm-4);
    border-bottom: 0;
    font-size: var(--portal-text-body);
    font-weight: 500;
    letter-spacing: 0;
    color: var(--portal-ink-secondary);
  }
  .portal-menu__row--child.portal-menu__row--active { color: var(--portal-olive); font-weight: 600; }
  /* Children are revealed by a click, not by the sheet opening, so they must
     not inherit the entrance animation's opacity:0 start state. */
  .portal-menu__panel .portal-menu__row {
    opacity: 1;
    transform: none;
    transition-delay: 0ms;
  }

  /* Account is a secondary group — same rows, one step down the scale.
     14px is this project's floor for the 65+ audience; the demotion comes
     from the size gap against the 24px primary rows, not from going under
     it. */
  .portal-menu__row--compact {
    min-height: 52px;
    font-size: var(--portal-text-label);
    font-weight: 500;
    letter-spacing: 0;
    color: var(--portal-ink-secondary);
  }

  .portal-menu__member {
    margin: 6px 0 0;
    display: flex;
    align-items: baseline;
    gap: var(--pm-2);
    font-family: var(--portal-font-body);
    font-size: var(--portal-text-caption);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--portal-ink-muted);
  }
  .portal-menu__member strong {
    letter-spacing: 0;
    text-transform: none;
    color: var(--portal-ink-secondary);
    font-variant-numeric: tabular-nums;
  }

  /* Rows rise 8px as the sheet fades in. */
  .portal-menu__row,
  .portal-menu__group-title {
    opacity: 0;
    transform: translateY(8px);
    transition:
      opacity 240ms var(--pm-ease),
      transform 240ms var(--pm-ease);
    transition-delay: calc(var(--menu-i, 0) * 30ms);
  }
  .portal-menu.is-open .portal-menu__row,
  .portal-menu.is-open .portal-menu__group-title {
    opacity: 1;
    transform: translateY(0);
  }
  @media (prefers-reduced-motion: reduce) {
    .portal-menu { transition: opacity 150ms ease; }
    .portal-menu__row,
    .portal-menu__group-title {
      transition: opacity 150ms ease;
      transform: none;
      transition-delay: 0ms;
    }
  }
}

/* =========================================================================
   SUB-NAV · BREADCRUMBS · FOOTER  (≤768px)  —  Portal Sprint P1
   ========================================================================= */
@media (max-width: 768px) {
  /* ---- Credit sub-nav: a 3×2 grid, not a tab row ----

     Six sections — Overview, Score, Debts, Report, Alerts, Identity —
     will not sit on one 335px row. Two attempts have now failed:

     1. Desktop underline tabs, which is what actually shipped: the base
        `.portal-subnav__inner` centres an overflowing flex row, and a
        centred overflow is unreachable on BOTH sides. Overview was
        scrolled off the left with no way to scroll back to it and
        Identity was clipped at the right. Two of six sections were
        simply gone.
     2. A scroll-snap chip rail (Sprint P1). It was never applied —
        `.portal-body--credit .portal-subnav__link` in portal.css is
        (0,2,0) and the rail was written on the bare class at (0,1,0),
        so it lost every declaration. Even had it landed, it answers
        the wrong question: a rail still hides sections behind a gesture
        that nothing on screen advertises.

     3. A 3×2 grid. All six were visible and one tap each, which fixed
        the reachability — but six equal cells read as a keypad rather
        than as navigation, and it spent ~130px above every page in the
        cluster to say where you are.

     So: a picker. One full-width control naming the section you are on,
     which opens a sheet listing all six as full-width rows with the
     current one marked. Nothing hidden, no gesture, and the chrome on
     the page is a single row. It borrows the sheet language the portal
     menu already uses, so it is not a new pattern to learn.

     Specificity note: these are matched at (0,2,0) via the block +
     element pair so they beat the credit-scoped rules in portal.css on
     source order — without introducing a page-scoped selector of their
     own (CLAUDE.md Rule 3). */
  .portal-subnav { margin-top: var(--pm-2); }

  .portal-subnav .portal-subnav__picker {
    appearance: none;
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "eyebrow caret" "value caret";
    align-items: center;
    column-gap: var(--pm-3);
    width: calc(100% - (2 * var(--pm-gutter)));
    margin: 0 var(--pm-gutter) var(--pm-4);
    min-height: 64px;
    padding: var(--pm-2) var(--pm-4);
    border: 1.5px solid var(--portal-hairline-strong);
    border-radius: var(--pm-r-card);
    background: var(--portal-surface);
    text-align: left;
    cursor: pointer;
    transition: transform 140ms var(--pm-ease), border-color 180ms ease;
  }
  .portal-subnav .portal-subnav__picker:active { transform: scale(0.99); }
  .portal-subnav__picker[aria-expanded="true"] { border-color: var(--portal-olive); }
  .portal-subnav__picker-eyebrow {
    grid-area: eyebrow;
    font-family: var(--portal-font-body);
    font-weight: 600;
    font-size: var(--portal-text-caption);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--portal-ink-muted);
  }
  .portal-subnav__picker-value {
    grid-area: value;
    font-family: var(--portal-font-body);
    font-weight: 600;
    font-size: var(--portal-text-card-title);
    letter-spacing: -0.2px;
    color: var(--portal-ink);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .portal-subnav__picker-caret {
    grid-area: caret;
    display: inline-grid;
    place-items: center;
    width: 32px;
    height: 32px;
    color: var(--portal-ink-secondary);
    transition: transform 200ms var(--pm-ease);
  }
  .portal-subnav__picker[aria-expanded="true"] .portal-subnav__picker-caret {
    transform: rotate(180deg);
    color: var(--portal-olive);
  }

  /* ---- The sheet: the same six links, laid out for a thumb ---- */
  .portal-subnav .portal-subnav__inner {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 120;
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
    max-width: none;
    /* vh first as the iOS <15.4 fallback — those Safaris drop the dvh line
       as invalid and would otherwise revert to the desktop cascade. */
    max-height: 78vh;
    max-height: 80dvh;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--pm-2) var(--pm-gutter)
             calc(var(--pm-4) + env(safe-area-inset-bottom, 0px));
    background: var(--portal-surface);
    border-radius: var(--pm-r-card) var(--pm-r-card) 0 0;
    box-shadow: 0 -16px 40px -16px rgba(14, 15, 12, 0.45);
    transform: translateY(100%);
    visibility: hidden;
    transition: transform 220ms cubic-bezier(0.4, 0, 1, 1), visibility 0s linear 220ms;
  }
  .portal-subnav.is-open .portal-subnav__inner {
    transform: translateY(0);
    visibility: visible;
    transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1), visibility 0s;
  }
  /* Backdrop. A pseudo-element rather than a node so the sheet needs no
     extra markup on a component that also renders on desktop. */
  .portal-subnav::after {
    content: '';
    position: fixed;
    inset: 0;
    z-index: 119;
    background: rgba(14, 15, 12, 0.55);
    opacity: 0;
    visibility: hidden;
    transition: opacity 220ms ease, visibility 0s linear 220ms;
  }
  .portal-subnav.is-open::after {
    opacity: 1;
    visibility: visible;
    transition: opacity 260ms ease, visibility 0s;
  }

  .portal-subnav .portal-subnav__link {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    text-align: left;
    min-height: 60px;
    height: auto;
    padding: var(--pm-2) var(--pm-2);
    border: 0;
    border-bottom: 1px solid var(--portal-hairline);
    border-radius: 0;
    background: transparent;
    /* Sentence case, not the desktop tabs' tracked caps — this is a list
       of destinations now, and caps at 14px is the harder read. */
    font-size: var(--portal-text-section);
    font-weight: 500;
    letter-spacing: -0.3px;
    text-transform: none;
    line-height: 1.2;
    color: var(--portal-ink);
    opacity: 1;
    transition: background-color 180ms ease, color 180ms ease;
  }
  .portal-subnav .portal-subnav__link:last-child { border-bottom: 0; }
  .portal-subnav .portal-subnav__link--active,
  .portal-subnav .portal-subnav__link--active:hover {
    background: transparent;
    color: var(--portal-olive);
    font-weight: 600;
  }
  /* Where you are, marked with a tick rather than a fill — the row is a
     link, and a filled row reads as a button. */
  .portal-subnav .portal-subnav__link--active::after {
    content: '';
    margin-left: auto;
    width: 22px;
    height: 22px;
    background: currentColor;
    -webkit-mask: var(--pm-tick) center / contain no-repeat;
    mask: var(--pm-tick) center / contain no-repeat;
  }

  @media (prefers-reduced-motion: reduce) {
    .portal-subnav .portal-subnav__inner,
    .portal-subnav.is-open .portal-subnav__inner { transition: visibility 0s; }
    .portal-subnav__picker-caret { transition: none; }
  }

  /* Breadcrumbs: a three-level trail wraps to three lines at this width and
     pushes the page title below the fold. One line instead.

     P1 solved that by making the line scroll horizontally with the scrollbar
     hidden. That trades a wrap for a hidden gesture: nothing on screen tells
     the member the trail continues past the right edge, and a 65+ reader is
     the least likely to go hunting for it (playbook §2 — a swipe always needs
     a visible affordance). The overflow is kept as a safety net for an
     unusually long current label, but it should no longer be what carries the
     component. */
  .portal-crumbs {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: var(--pm-2);
    overflow-x: auto;
    scrollbar-width: none;
    white-space: nowrap;
    font-size: var(--portal-text-caption);
  }
  .portal-crumbs::-webkit-scrollbar { display: none; }

  /* `.portal-crumbs--bare` is documented in pages.css as "type styles only —
     the shell owns the geometry", but the variant's rule there is empty, so
     every bare trail still inherits the base component's page padding —
     `0 24px` at this breakpoint, on top of the 20px gutter `main` already
     supplies. The crumbs sat indented 44px while the page's own H1 sat at 20,
     which is two left edges at the very top of the page. Make bare bare. */
  .portal-crumbs--bare.portal-crumbs--bare { padding-inline: 0; }
  /* Inside a benefit action bar the band's hairline does the separating, so
     the trail's own bottom margin is 24px of nothing. Left alone elsewhere,
     where the crumbs float above a page title with no rule between them.
     Both bars: `.bnft-actionbar` on the pitch pages, `.bnft-active__bar` on
     the managed ones. */
  .bnft-actionbar__inner .portal-crumbs,
  .bnft-active__bar-inner .portal-crumbs { margin-bottom: 0; }

  /* Deep trails collapse to their last two levels, so the whole trail is
     visible without scrolling. The portal has exactly two crumb shapes:
     `parent › current` (3 li — two crumbs and a separator) and
     `grandparent › parent › current` (5 li). Only the second can afford to
     lose a level, and the level it loses is the one already permanently on
     screen as the first item in the tab bar.

     `:nth-child(-n+2):nth-last-child(n+4)` hides the first two items only
     when at least four follow — true for the 5-li trail, false for the 3-li
     one, which keeps its parent link. Structural, so no page is scoped and
     no markup changes; a future 7-li trail collapses the same way.

     .portal-crumbs__list is a flex row on desktop; the min-width: 0 lets the
     current label's ellipsis actually engage instead of forcing the row wide. */
  .portal-crumbs__list { min-width: 0; }
  .portal-crumbs__list > li:nth-child(-n+2):nth-last-child(n+4) {
    display: none;
  }
  .portal-crumbs__current {
    display: block;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  /* (0,1,1) — beats .portal-crumbs__link (0,1,0), so this is where the
     breadcrumb target size has to be set, not in the guard section below. */
  .portal-crumbs a { min-height: var(--pm-tap); display: inline-flex; align-items: center; }

  /* Footer: brand, then copy, then the legal list as rows. The desktop
     pipe-separated single line is unreadable and untappable at 375px. */
  .portal-footer__inner {
    max-width: none;
    padding: var(--pm-10) var(--pm-gutter);
    gap: var(--pm-5);
  }
  .portal-footer__bottom {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--pm-3);
  }
  .portal-footer__copy { margin: 0; font-size: var(--portal-text-caption); line-height: 1.5; }
  .portal-footer__legal-links {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    width: 100%;
  }
  /* The "|" separators are decorative and meaningless once stacked. */
  .portal-footer__legal-links li[aria-hidden="true"] { display: none; }
  .portal-footer__legal-links li { width: 100%; }
  .portal-footer__legal-links li + li { border-top: 1px solid var(--portal-hairline, rgba(14, 15, 12, 0.08)); }
  .portal-footer__legal-links a {
    display: flex;
    align-items: center;
    gap: var(--pm-2);
    min-height: var(--pm-tap);
    font-size: var(--portal-text-caption);
  }
}

/* =========================================================================
   LAYOUT GUARD  (≤768px)  —  Portal Sprint P1

   The portal's shared containers are fixed-column grids
   (`repeat(3, 1fr)`, `repeat(4, 1fr)`). At a 311px content width those
   tracks resolve to ~220px each and the page scrolls sideways — /dashboard
   measured a 728px scrollWidth in a 375px viewport.

   This is the shell-level guard, not the per-page design: it collapses the
   shared grids and stops grid children from forcing their own minimum
   width. The actual per-page composition (score gauge, charts, report
   tables) is Sprint P2 and is not attempted here.
   ========================================================================= */
@media (max-width: 768px) {
  /* Grid items default to `min-width: auto`, so a long word or a wide child
     can push a track past its share. This is the single most common cause of
     sideways scroll in a collapsed grid. */
  .portal-stats > *,
  .portal-quick > *,
  .portal-hero > *,
  .portal-section > * { min-width: 0; }

  /* Stat rows: three across becomes one. */
  .portal-stats { grid-template-columns: minmax(0, 1fr); }

  /* Quick tiles are compact and pair well two-up; four across does not fit. */
  .portal-quick { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  @media (max-width: 360px) {
    .portal-quick { grid-template-columns: minmax(0, 1fr); }
  }

  /* Credit hero: copy | rule | score becomes a stack; the 1px vertical rule
     between the columns has nothing to separate once stacked. */
  .portal-hero { grid-template-columns: minmax(0, 1fr); }
  .portal-hero__divider,
  .portal-hero > hr { display: none; }

  /* Page shell padding: 32px gutters leave 311px of content at 375px. */
  .portal-body main,
  main.vs-main,
  .portal-shell { padding-left: var(--pm-gutter); padding-right: var(--pm-gutter); }

  /* Wide tabular data scrolls inside its own box rather than widening the
     page. Pages that need more than this get it in their own sprint. */
  .portal-table-wrap,
  .portal-scroll-x {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }

  /* Shared chrome targets that were under the floor. */
  .portal-section__link { min-height: var(--pm-tap); display: inline-flex; align-items: center; }
  .portal-filter { min-height: var(--pm-tap); }
  .portal-footer__brand { min-height: var(--pm-tap); display: inline-flex; align-items: center; }
}

/* =========================================================================
   MEMBER HOME  (/portal · /portal-activated)  —  Portal Sprint P1
   ========================================================================= */
@media (max-width: 768px) {
  /* Section headers pair a title with a right-aligned CTA in a nowrap flex
     row. At 335px the CTA starts at x=245 and runs off the page. Stack. */
  /* My own regression, from Sprint P1. Stacking the head was right, but
     `.pact-page .pact-more__head` sets `align-items: flex-end` at (0,2,0)
     and beat the `flex-start` here — so the direction flipped to column
     while the alignment stayed pinned to the end, and the title and its
     link both hugged the RIGHT edge of the page. Doubled to (0,3,0). */
  .pact-more__head.pact-more__head.pact-more__head,
  .portal-section__head.portal-section__head.portal-section__head {
    flex-direction: column;
    align-items: flex-start;
    text-align: left;
    gap: var(--pm-3);
  }
  .pact-more__cta,
  .portal-section__link {
    min-height: var(--pm-tap);
    display: inline-flex;
    align-items: center;
  }

  /* Inline "status" and phone links are primary actions on the member home
     — a member calling their concierge is the whole point of the card. */
  .portal-offer-status__link,
  .portal-concierge__phone {
    min-height: var(--pm-tap);
    display: inline-flex;
    align-items: center;
  }

  /* Member-home hero and spotlight blocks: single column, no fixed tracks. */
  .portal-spotlight,
  .portal-concierge {
    grid-template-columns: minmax(0, 1fr);
  }
  .portal-spotlight > *,
  .portal-concierge > * { min-width: 0; }
}

/* =========================================================================
   ACCOUNT · SUPPORT · REFERRAL  —  Portal Sprint P1
   ========================================================================= */
@media (max-width: 768px) {
  /* ---- Account ---- */
  /* Section tabs are a horizontal rail; let them scroll instead of
     overflowing the page, and give each a real target. */
  /* The rail is a flex item; without min-width:0 it sizes to its content
     (523px) rather than to its 309px parent, so overflow-x never engages and
     the right-hand tabs sit off-screen with no way to reach them. */
  .account-tabs__head { min-width: 0; }
  .account-tabs__rail {
    flex: 1 1 auto;
    min-width: 0;
    max-width: 100%;
    display: flex;
    gap: var(--pm-2);
    overflow-x: auto;
    scrollbar-width: none;
    scroll-snap-type: x proximity;
    scroll-padding-left: var(--pm-gutter);
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
  }
  .account-tabs__rail::-webkit-scrollbar { display: none; }
  .account-tabs__rail > * { flex: 0 0 auto; scroll-snap-align: start; }
  .account-tabs__logout,
  .account-link {
    min-height: var(--pm-tap);
    display: inline-flex;
    align-items: center;
  }
  .account-grid,
  .account-card__grid { grid-template-columns: minmax(0, 1fr); }

  /* ---- Support ---- */
  .portal-faq__link {
    min-height: var(--pm-tap);
    display: inline-flex;
    align-items: center;
  }
  /* Video carousel dots were 8×8. Same rule as the marketing carousel: a
     control has to be reachable, so the button carries a 48px hit area and
     the visible dot is drawn inside it. */
  /* support.html defines these at `.sup-page .support-videos__dot` (0,2,0);
     match it and add one class so the hit area wins without !important. */
  .sup-page .support-videos__dot.support-videos__dot {
    width: var(--pm-tap);
    height: var(--pm-tap);
    padding: 0;
    background: transparent;
    border: 0;
    display: inline-grid;
    place-items: center;
    cursor: pointer;
  }
  .sup-page .support-videos__dot::after {
    content: "";
    width: 10px;
    height: 10px;
    border-radius: 999px;
    background: rgba(14, 15, 12, 0.22);
    transition: width 200ms var(--pm-ease), background-color 200ms ease;
  }
  .sup-page .support-videos__dot[aria-current="true"]::after,
  .sup-page .support-videos__dot--active::after {
    width: 26px;
    background: var(--portal-olive, #576038);
  }
  @media (prefers-reduced-motion: reduce) {
    .sup-page .support-videos__dot::after { transition: none; }
  }

  /* ---- Referral ---- */
  /* .ref-grid is the 2-column parent; .ref-tracker/.ref-premium are flex
     columns inside it. Collapsing the children does nothing — the track
     itself has to collapse, and the children need min-width:0 or their
     content keeps the track at min-content width (measured 360px in a
     311px parent). */
  .ref-grid,
  .ref-steps { grid-template-columns: minmax(0, 1fr); }
  .ref-grid > *,
  .ref-steps > * { min-width: 0; }
  .ref-tracker,
  .ref-premium { min-width: 0; padding: var(--pm-8) var(--pm-5); }
  /* Share row: field above button, both full width — the desktop inline pair
     leaves the field too narrow to read a referral link back. */
  .ref-link-form {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--pm-3);
  }
  .ref-link-form__input {
    width: 100%;
    height: auto;
    min-height: var(--pm-cta);
    padding: 0 var(--pm-4);
    border-radius: var(--pm-r-input);
    /* ≥16px or iOS zooms on focus. pages.css sets this from
       --portal-text-label, which resolves to 14px on this page. */
    font-size: var(--portal-text-body);
  }
  /* referral.html's inline <style> has `.ref-page .ref-link-form input`
     (0,2,1) and inline sheets load after this <link>, so a matching
     specificity loses the tie. (0,3,1) takes it without !important. */
  .ref-page .ref-link-form input.ref-link-form__input {
    font-size: var(--portal-text-body);
  }
  .ref-link-form__btn,
  .ref-link-form button {
    width: 100%;
    min-height: var(--pm-cta);
    border-radius: var(--pm-r-input);
  }
}

/* =========================================================================
   PORTAL LEGAL  (/portal-privacy-policy · /portal-terms-of-use ·
                  /portal-ca-privacy-notice · /portal-your-privacy-choices)
   Portal Sprint P1 — one shared template.

   These pages carry .vs-legal-page and legal.css, same as the marketing
   legal set, but they run on the portal shell and therefore load this layer
   rather than marketing-mobile.css. The prose is already sized correctly
   (20px/1.5 body, 18px lists); only the navigation targets need lifting.
   ========================================================================= */
@media (max-width: 768px) {
  .vs-legal-page .lgl-layout {
    display: flex;
    flex-direction: column;
    grid-template-columns: none;
    gap: var(--pm-8);
  }
  .vs-legal-page .lgl-prose { max-width: none; }

  /* On a phone the sidebar contents list is the page's main navigation. */
  .vs-legal-page .lgl-toc { position: static; max-width: none; }
  .vs-legal-page .lgl-toc__list { gap: 0; }
  .vs-legal-page .lgl-toc__list li + li {
    border-top: 1px solid var(--portal-hairline, rgba(14, 15, 12, 0.08));
  }
  .vs-legal-page .lgl-toc a,
  .vs-legal-page .lgl-hero__crumbs a,
  .vs-legal-page .lgl-back-to-top {
    display: inline-flex;
    align-items: center;
    min-height: var(--pm-tap);
  }
  .vs-legal-page .lgl-toc a { display: flex; }

  /* Wide legal tables scroll in their own box, never widen the page. */
  .vs-legal-page .lgl-table-wrap {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }
}

/* =========================================================================
   CREDIT SURFACES  (≤768px)  —  Portal Sprint P2

   /dashboard · /credit · /debts · /report · /protection · /alerts ·
   /optimal-path share one component vocabulary (`vs-*` plus a few
   page-local grids). Every one of them scrolled sideways at 375px —
   /optimal-path by 330px, /protection by 308px — because the shared
   containers are fixed multi-column grids and nowrap flex rows.

   Fixing the vocabulary once keeps the seven pages consistent, which is the
   reason they are one sprint rather than seven.
   ========================================================================= */
@media (max-width: 768px) {
  /* Sub-pixel guard. Collapsed grid tracks round to fractional widths
     (/alerts measured a 0.78px overhang), which is enough for the browser to
     report scrollWidth > clientWidth and show a horizontal scrollbar.
     `clip`, not `hidden`: overflow-x:hidden on the body forces overflow-y to
     auto and turns the body into the scroll container. */
  .portal-body,
  .portal-body--credit,
  .portal-body--white { overflow-x: clip; }

  /* ---- Shared containers ---- */
  /* .portal-body--credit .dash-stats in portal.css is (0,2,0), so the bare
     class would lose on every credit page. Match its specificity here. */
  /* `body.` qualifier lifts these to (0,2,1). The page-local base rules for
     the --3/--4/--5 variants are (0,2,0) and live in an inline <style> that
     loads after this file, so a matching specificity loses the tie. */
  body.portal-body--credit .dash-stats,
  body.portal-body--credit .vs-grid,
  body.portal-body--credit .vs-grid--3,
  body.portal-body--credit .vs-grid--4,
  body.portal-body--credit .vs-grid--5,
  .vs-section,
  .dash-stats,
  .vs-cards,
  .vs-grid,
  .factor-grid,
  .cost-of-carry,
  .cost-of-carry__split {
    grid-template-columns: minmax(0, 1fr);
  }
  .cost-of-carry > * { min-width: 0; }
  .vs-section > *,
  .dash-stats > *,
  .vs-cards > *,
  .vs-grid > *,
  .vs-card,
  .vs-stat,
  .portal-stat,
  .portal-alert,
  .portal-quick-tile,
  .factor { min-width: 0; }
  /* A `1fr` track is `minmax(auto, 1fr)`, so a child's min-content width can
     push the track wider than its container — /alerts had a single 343.78px
     track inside a 311px box. min-width:0 on the children is what actually
     lets a collapsed grid stay inside the page. */
  .alerts-tiles,
  .alerts-tiles > * { min-width: 0; }
  /* The stat's inner text column is the piece that actually overflows —
     min-width:0 has to reach it, not just the card. */
  .vs-stat > *,
  .vs-stat > * > * { min-width: 0; }

  /* Step rows are `icon | label | chip` 3-column grids. At 375px the chip
     has nowhere to go, so it hangs off the row. Two columns instead, with
     the chip dropping under the label it belongs to. */
  .vs-steps > li {
    grid-template-columns: 36px minmax(0, 1fr);
    row-gap: var(--pm-2);
  }
  .vs-steps > li > *:nth-child(3) { grid-column: 2; }
  .vs-steps > li > * { min-width: 0; }

  /* Nowrap flex rows that carry content, not controls. */
  .vs-stat,
  .vs-steps__body,
  .cost-of-carry__split,
  .factor {
    flex-wrap: wrap;
  }
  .factor > * { min-width: 0; }

  /* ---- Chips and filter rows become scroll rails ------------------------
     A filter set is a control group: wrapping it to three ragged lines
     hides which filters exist. A rail keeps them on one line and tells the
     reader there are more by letting them scroll. ------------------------ */
  /* CORRECTION (2026-07-27). The rail was the wrong call for filters.
     Technically it scrolled — 571px of chips in a 326px box, scrollLeft
     responded — but nothing on screen said so: the chips were cut dead at
     the container edge with no peek and no fade, so "Blocked" and "Info"
     were invisible and unguessable. A filter group is the one place you
     must be able to see what is filterable before you choose.

     Wrapping five chips at desktop padding gives three ragged lines,
     which is what the rail was avoiding. Tightening the horizontal
     padding at this breakpoint takes the set from 571px to ~455px, and
     it lands in two clean rows with every filter visible and no gesture
     to discover. Tap height is untouched. */
  .portal-filters {
    display: flex;
    flex-wrap: wrap;
    gap: var(--pm-2);
    min-width: 0;
    max-width: 100%;
    overflow: visible;
  }
  .portal-filters > * { flex: 0 0 auto; }
  .portal-filter {
    white-space: nowrap;
    padding-left: var(--pm-3);
    padding-right: var(--pm-3);
  }
  .portal-filter__count { margin-left: 6px; }

  /* .vs-chips stays a rail — those sets are open-ended lists rather than
     a fixed group of filters, and they are not the only route to their
     content. */
  .vs-chips {
    display: flex;
    flex-wrap: nowrap;
    gap: var(--pm-2);
    min-width: 0;
    max-width: 100%;
    overflow-x: auto;
    scrollbar-width: none;
    scroll-snap-type: x proximity;
    /* Declare the axis: with `auto`, a short horizontal swipe on a rail
       this shallow is often claimed by the page's vertical scroll. */
    touch-action: pan-x;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
  }
  .vs-chips::-webkit-scrollbar { display: none; }
  .vs-chips > * { flex: 0 0 auto; scroll-snap-align: start; }
  .vs-chip { white-space: nowrap; }

  /* ---- Tables scroll in their own box ---------------------------------
     A credit report table cannot be collapsed without losing the row
     relationships that make it a report. It scrolls horizontally inside its
     own container instead, so the page itself never moves sideways. ------ */
  .vs-table-wrap,
  .vs-table__wrap {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    max-width: 100%;
  }
  /* Most report tables have no wrapper element at all — /report's sits in a
     bare `div.vs-mt-6` margin utility — so the table itself becomes the
     scroll box. Rows stay intact, which is the point: a credit report table
     read row-by-row is the document; collapsed into stacked pairs it isn't. */
  .vs-table {
    display: block;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    max-width: 100%;
    width: 100%;
  }

  /* ---- Controls ---- */
  /* Score Coach: the best-match header is `title block | chip` on one
     nowrap row. The chip has no room at 375px, so the row wraps and the
     chip sits under the pathway name it describes. */
  .bestmatch-head,
  .bestmatch-title-row {
    flex-wrap: wrap;
    min-width: 0;
  }
  .bestmatch-head > *,
  .bestmatch-title-row > * { min-width: 0; }

  .vs-btn {
    min-height: var(--pm-tap);
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .vs-table__chevron-btn {
    width: var(--pm-tap);
    height: var(--pm-tap);
    display: inline-grid;
    place-items: center;
  }
}

/* =========================================================================
   PROFILE WIZARD  (≤768px)  —  "one question, one fold, one action"

   The Complete-your-profile wizard is the second most important flow in
   the portal, after credit-monitoring activation. On a phone it was the
   worst thing in it.

   What was wrong (measured at 375×812):
   - The panel is anchored to the BOTTOM OF THE STICKY BAR via
     --vs-onb-bar-h. That bar is 259px tall on a phone, so the dialog
     started at y=324 with 488px of screen left — and still declared
     `height: 812px`, hanging 324px below the fold. The Continue button
     was literally off-screen on arrival.
   - Sub-step 0 alone is 855px of content (a 48px/3-line question plus
     six 106px tiles) inside a 488px window.
   - The footer was a 3-row stack (Back / help / Continue) — 162px of
     chrome that scrolled away with the content.
   - The upload step offers a "scan this QR with your phone" handoff
     card. On a phone.

   The fix is a full-screen dialog with three zones and a fixed budget:

     ┌──────────────────────────────┐
     │ ▬▬▬▬ ▬▬▬▬ ▭▭▭▭ ▭▭▭▭          │  segmented meter, flush to the edge
     │ Tell us about your service   │  context + "1 of 4"      ✕
     ├──────────────────────────────┤  ← 48px rail, sticky
     │                              │
     │  Which best describes        │  ONE question, 24px
     │  you today?                  │
     │                              │
     │  [ Veteran              ]    │  the answer, fitting whole
     │  [ Active-duty          ]    │
     │  [ ... ]                     │
     │                              │
     ├──────────────────────────────┤  ← ~130px footer, sticky
     │ [ Back ] [    Continue     ] │  thumb zone, safe-area aware
     │  Prefer the phone? Call …    │
     └──────────────────────────────┘

   Budget at 812: 48 rail + 130 footer = 618 for content. Every sub-step
   is sized to land inside it, so a standard phone never scrolls. Smaller
   phones scroll the middle zone only — the question's answer is always
   one thumb-reach from the action that submits it.

   Zero JS changes: the rail and the footer are `position: sticky` inside
   the body scroller, so sub-step state, validation, skip logic and the
   celebration all behave exactly as they do on desktop.
   ========================================================================= */
@media (max-width: 768px) {

  /* ---- 1. Shell: full-screen dialog, not a hanging sheet ---- */

  /* Dim the whole screen. The base rule starts the backdrop below the
     bar, which left the top 324px undimmed AND tappable behind a modal. */
  .portal-onb-sheet__backdrop {
    top: 0;
    /* `overflow: hidden` on the body is not a reliable scroll lock on
       iOS — a drag that starts on the backdrop still moves the page
       behind the modal. Nothing on the backdrop needs to pan. */
    touch-action: none;
  }

  /* `inset: 0` overrides top/left, `width: auto` the 1280px cap, and
     `max-height: none` the calc() that fought the height. The panel now
     IS the viewport. Enters from the bottom edge (where the thumb is)
     rather than dropping from under a bar that is no longer relevant. */
  .portal-onb-sheet__panel {
    inset: 0;
    width: auto;
    height: auto;
    max-height: none;
    border-radius: 0;
    border-top: 0;
    box-shadow: none;
    transform: translateY(100%);
    /* Exit — snappier than the enter, and inside the 260ms teardown
       timer in closeSheet() so the panel never snaps mid-flight. */
    transition: transform 220ms cubic-bezier(0.4, 0, 1, 1);
  }
  .portal-onb-sheet.is-open .portal-onb-sheet__panel {
    transform: translateY(0);
    /* Enter — the iOS drawer curve: fast off the mark, long settle. */
    transition: transform 320ms cubic-bezier(0.32, 0.72, 0, 1);
  }

  /* The middle zone is the only scroller, and its scroll never chains
     out to the page behind the modal. The 8px top pad the base adds for
     head-less panels would push the sticky rail off the container's
     scroll origin, leaving an 8px gap that content slides through —
     matched at (0,3,0) so it actually lands. */
  .portal-onb-sheet .portal-onb-sheet__panel .portal-onb-sheet__body {
    padding-top: 0;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
  }

  /* Close — a real 44px target parked in the rail, not floating over
     content at the desktop's 40/36 inset. Above the rail's own stacking
     level, or the sticky rail paints over it.
     The rail is 68px and the button 44px, so it clears the bottom
     hairline by 12px and the screen edge by 12px. At 8px it read as
     stuck to both, and to the counter beside it. */
  .portal-onb-sheet__close--floating {
    top: calc(12px + env(safe-area-inset-top, 0px));
    right: 12px;
    z-index: 4;
    width: 44px;
    height: 44px;
    box-shadow: none;
    background: var(--portal-surface);
  }
  .portal-onb-sheet__close--floating:active { transform: scale(0.94); }

  /* (The help control lives below the screen title — see
     .portal-onb-callme in portal.css. Nothing extra beside the ✕.) */

  /* The article stops owning padding — the rail and footer need to run
     edge to edge so their backgrounds cover content sliding under them.
     It also has to fill the scroller, otherwise a short sub-step (rank
     is one select) leaves the sticky footer floating mid-screen with
     dead white beneath it. */
  .portal-onb-tab-body {
    padding: 0;
    gap: 0;
    min-height: 100%;
  }
  .portal-onb-form--substeps {
    flex: 1;
    min-height: 0;
    justify-content: space-between;
  }

  /* ---- 2. Rail: segmented meter + context + counter, sticky ---- */

  .portal-onb-tab-body__head {
    position: sticky;
    top: 0;
    z-index: 3;
    background: var(--portal-surface);
    border-bottom: 1px solid var(--portal-hairline);
    /* The ✕ lives in this corner on every step, including the ones with
       no sub-progress meter (the address step has a single sub). 68px of
       reservation = 12 edge + 44 button + 12 gap to whatever sits left
       of it, so nothing ever runs under the close target. */
    padding: 0 68px 12px var(--pm-gutter);
    padding-top: env(safe-area-inset-top, 0px);
    justify-content: center;
    min-height: 68px;
    margin: 0;
    max-width: none;
  }
  /* Two rows: the meter runs full-bleed across the top, then a single
     line of [context ......... 1 of 4] with the ✕ sitting to its right.
     `display: contents` lets the progress wrapper's own children take
     part in the head's grid instead of forming a second box. */
  .portal-onb-tab-body__head--with-progress {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "meter meter"
      "copy  count";
    align-items: center;
    column-gap: var(--pm-3);
    row-gap: 12px;
  }
  .portal-onb-tab-body__head-copy { grid-area: copy; }
  .portal-onb-sub-progress { display: contents; }
  .portal-onb-sub-progress__dots {
    grid-area: meter;
    display: flex;
    gap: 3px;
    /* Pull out past the head's gutter AND past the ✕ reservation so the
       meter reads as the panel's own progress edge. */
    margin: 0 -68px 0 calc(-1 * var(--pm-gutter));
  }
  /* Base sets a fixed 28×6 pill with !important; a meter that spans the
     screen has to match that weight to win. */
  .portal-onb-sub-progress__dot {
    flex: 1 1 0;
    width: auto !important;
    height: 4px !important;
    border-radius: 0 !important;
  }
  /* `display: inline-block` on the base rule defeats the [hidden]
     attribute, so the sub-step that showSub() skips (rank, for spouses)
     left a dead segment in the meter. */
  .portal-onb-sub-progress__dot[hidden] { display: none !important; }

  .portal-onb-sub-progress__label {
    grid-area: count;
    font-size: var(--portal-text-caption);
    font-weight: 600;
    color: var(--portal-ink-secondary);
    white-space: nowrap;
  }
  /* Step name reads as context at caption size on one line — the
     question below it is the only thing competing for attention.
     Sentence case, not the tracked caps of the desktop eyebrow: "TELL
     US ABOUT YOUR SERVICE" needs 240px of the 243px left beside the
     counter and the ✕, so it truncated. Same words, 165px, no ellipsis
     — and easier to read at arm's length besides. */
  .portal-onb-tab-body__heading {
    font-size: var(--portal-text-caption);
    text-transform: none;
    letter-spacing: 0;
    line-height: 1.3;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* ---- 3. Content: one question, sized to the fold ---- */

  /* `align-items: center` on the base collapses every child to its own
     content width — that is why the six status tiles rendered as a
     277px column floating in a 375px panel. Stretch, and the answer
     fills the phone. The 320px min-height and 28px gap are desktop
     breathing room the fold cannot afford. */
  .portal-onb-sub[data-current] {
    align-items: stretch;
    min-height: 0;
    padding: var(--pm-5) var(--pm-gutter) var(--pm-4);
    gap: var(--pm-4);
    /* Fill the space between rail and footer and sit in the middle of
       it. A short step (rank is a single select) then reads as a card
       held between two edges instead of one line stranded at the top.
       When the content is taller than the slack there is nothing to
       centre and it simply scrolls. */
    flex: 1;
    justify-content: center;
  }
  .portal-onb-sub[data-sub-key="upload"][data-current] {
    padding-top: var(--pm-4);
    padding-bottom: var(--pm-3);
    gap: var(--pm-3);
  }
  .portal-onb-tab-body--address .portal-onb-sub[data-current] {
    padding-top: var(--pm-4);
    padding-bottom: var(--pm-3);
  }
  .portal-onb-sub__head { margin: 0; }
  /* 48px display type spent three lines and 155px on the question. At
     24px it is two lines and 60px, and the answer comes with it. */
  .portal-onb-sub__title,
  .portal-onb-sub[data-sub-key="upload"] .portal-onb-sub__title,
  .portal-onb-form[data-onb-form="address"] .portal-onb-sub__title {
    font-size: var(--portal-text-section);
    line-height: 1.2;
    letter-spacing: -0.3px;
  }
  .portal-onb-form[data-onb-form="address"] .portal-onb-sub__lead {
    font-size: var(--portal-text-label);
    line-height: 1.45;
  }
  .portal-onb-sub__ack {
    font-size: var(--portal-text-label);
    margin-bottom: var(--pm-2);
  }
  .portal-onb-sub__lead {
    font-size: var(--portal-text-label);
    line-height: 1.45;
  }

  /* Status tiles — six of them have to share ~470px. 60px tall keeps
     every one above the 48px tap floor with the label at the body size.
     The --lg variant carries the 96px desktop height at (0,2,0), so the
     compact height has to be stated there too. */
  .portal-onb-status-pick,
  .portal-onb-status-pick--lg { gap: var(--pm-2); }
  .portal-onb-status-pick--lg .portal-onb-status-pick__option { min-height: 60px; }
  .portal-onb-pick__option,
  .portal-onb-branch-pick__option,
  .portal-onb-status-pick__option {
    min-height: 60px;
    padding: var(--pm-3) var(--pm-4);
    border-radius: var(--pm-r-input);
    transition:
      border-color 200ms ease,
      background-color 200ms ease,
      transform 140ms var(--pm-ease);
  }
  .portal-onb-pick__option:active,
  .portal-onb-branch-pick__option:active,
  .portal-onb-status-pick__option:active { transform: scale(0.985); }

  /* Branch — seven tiles in two columns is four rows. The 132px desktop
     tile makes that 570px; 92px makes it 398px and it lands in the fold. */
  .portal-onb-branch-pick { gap: var(--pm-2); }
  .portal-onb-branch-pick__option {
    min-height: 92px;
    gap: var(--pm-2);
    padding: var(--pm-3) var(--pm-2);
  }
  .portal-onb-branch-pick__emblem { width: 40px; height: 40px; }
  .portal-onb-branch-pick__emblem svg { width: 22px; height: 22px; }
  .portal-onb-branch-pick__label { font-size: var(--portal-text-label); }

  /* Rank / address inputs at the portal's control height. */
  .portal-onb-input,
  .portal-onb-select {
    min-height: var(--portal-control-h);
    font-size: var(--portal-text-label);
    border-radius: var(--pm-r-input);
  }
  .portal-onb-field { gap: var(--pm-2); }
  .portal-onb-field__label { font-size: var(--portal-text-label); }
  .portal-onb-field__hint { font-size: var(--portal-text-caption); }

  /* ---- 3b. Upload step ----

     The desktop screen is [ dropzone | or | QR card ] — a handoff that
     says "scan this code with your phone". On the phone that is circular,
     and it costs ~300px of the fold to say it. So the QR column goes, and
     the handoff it stood in for becomes the direct thing: a camera zone
     (`capture="environment"`, rendered by cameraZoneHtml, hidden outside
     this breakpoint) sitting above the file zone.

     The result is two equal-weight ways in, stacked, both a full-width
     72px target, then the disclosure, then the reassurance — instead of
     one heavy dropzone and a QR card nobody can use. */
  .portal-onb-upload-row__col--qr,
  .portal-onb-upload-row__divider { display: none; }
  .portal-onb-upload-row { gap: var(--pm-3); }
  .portal-onb-upload-row__col--picker {
    display: grid;
    gap: var(--pm-2);
  }
  .portal-onb-upload--camera { display: flex; }
  .portal-onb-upload__label--pointer { display: none; }
  .portal-onb-upload__label--touch { display: block; }

  /* Source order puts the 13-item document list before the two ways to
     send one. Lead with the actions; the reference material follows. */
  .portal-onb-sub[data-sub-key="upload"] .portal-onb-upload-row { order: 1; }
  .portal-onb-sub[data-sub-key="upload"] .portal-onb-docs-accepted { order: 2; }
  .portal-onb-sub[data-sub-key="upload"] .portal-onb-reassure { order: 3; }

  .portal-onb-upload {
    min-height: 72px;
    padding: var(--pm-3) var(--pm-4);
    gap: var(--pm-3);
    border-radius: var(--pm-r-card);
    transition:
      border-color 200ms ease,
      background-color 200ms ease,
      transform 140ms var(--pm-ease);
  }
  .portal-onb-upload:active { transform: scale(0.99); }
  /* Hover lift fires on tap on touch and fights the press feedback. */
  .portal-onb-upload:hover { transform: none; }
  .portal-onb-upload__icon {
    width: 44px;
    height: 44px;
    flex: 0 0 44px;
  }
  .portal-onb-upload__icon svg { width: 22px; height: 22px; }
  .portal-onb-upload__copy { min-width: 0; }
  .portal-onb-upload__label {
    font-size: var(--portal-text-label);
    line-height: 1.3;
  }
  .portal-onb-upload__hint {
    font-size: var(--portal-text-caption);
    line-height: 1.35;
  }
  /* The camera zone leads, so it carries the stronger affordance — a
     filled olive icon against the file zone's tint. */
  .portal-onb-upload--camera .portal-onb-upload__icon {
    background: var(--portal-olive);
    color: #fff;
  }

  /* Picked files. The list sits directly beneath both zones, so the
     per-zone "2 files selected" caption is duplicate information — and
     worse, it hangs under whichever zone happens to own it, crediting
     the file picker for a photo the camera took. The list is the
     feedback; the caption goes. */
  .portal-onb-upload__filename { display: none; }
  .portal-onb-upload-list { text-align: left; }
  .portal-onb-upload-list__item { padding: var(--pm-2) var(--pm-3); }
  .portal-onb-upload-list__name { font-size: var(--portal-text-label); }
  .portal-onb-upload-list__meta { font-size: var(--portal-text-caption); }
  .portal-onb-upload-list__remove {
    width: var(--pm-tap);
    height: var(--pm-tap);
    flex: 0 0 var(--pm-tap);
  }

  /* The sub-step centres its text; a disclosure row with a chevron on
     the right has to read left-to-right or the summary looks unhooked
     from its own control. */
  .portal-onb-docs-accepted { text-align: left; }
  /* Secondary reference, not a third way in — the desktop padding made
     a two-line summary the heaviest block on the screen at 132px. */
  .portal-onb-docs-accepted summary {
    min-height: var(--pm-tap);
    display: flex;
    align-items: center;
    padding: var(--pm-2) var(--pm-4);
    font-size: var(--portal-text-label);
    line-height: 1.3;
  }
  .portal-onb-docs-accepted__list li,
  .portal-onb-docs-accepted__note { font-size: var(--portal-text-caption); }
  /* An open disclosure is 13 documents long — it gets its own scroll
     rather than pushing Submit out of the fold. */
  .portal-onb-docs-accepted[open] .portal-onb-docs-accepted__list {
    max-height: 168px;
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  /* Centred list items give three different left edges and three ragged
     ticks. Centre the BLOCK, left-align inside it, and the checkmarks
     line up into a column. */
  .portal-onb-reassure {
    gap: var(--pm-2);
    width: fit-content;
    max-width: 100%;
    margin-inline: auto;
    text-align: left;
  }
  .portal-onb-reassure li { font-size: var(--portal-text-caption); }

  /* Address step. The card photo is decorative (aria-hidden) and the
     three-across city/state/ZIP row cannot hold a city name at 375px. */
  .portal-onb-address-split { display: block; }
  .portal-onb-address-split__media { display: none; }
  /* Five labelled controls is the tallest step in the wizard — the
     rhythm between them is where the last 24px comes from. */
  .portal-onb-fields { gap: var(--pm-2); }
  .portal-onb-form[data-onb-form="address"] .portal-onb-fields { gap: var(--pm-2); }
  .portal-onb-form[data-onb-form="address"] .portal-onb-field { gap: 6px; }
  /* City / State / ZIP. Three across cannot hold a city name at 375px,
     but stacking all three costs a whole row of the fold. State and ZIP
     are both short — they pair; City takes its own line. */
  .portal-onb-field-row--address {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--pm-3);
  }
  .portal-onb-field-row--address > .portal-onb-field:first-child {
    grid-column: 1 / -1;
  }

  /* ---- 4. Footer: the action never leaves the screen ---- */

  .portal-onb-substep-foot {
    position: sticky;
    bottom: 0;
    z-index: 3;
    background: var(--portal-surface);
    border-top: 1px solid var(--portal-hairline);
    box-shadow: 0 -10px 24px -20px rgba(14, 15, 12, 0.55);
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas:
      "back next"
      "help help";
    /* Stretch, so a two-line label ("Submit for Verification") grows the
       whole row instead of cramping itself inside a 56px box — and Back
       grows with it rather than sitting short beside it. */
    align-items: stretch;
    gap: var(--pm-2) var(--pm-3);
    padding: var(--pm-3) var(--pm-gutter);
    padding-bottom: calc(var(--pm-3) + env(safe-area-inset-bottom, 0px));
    margin: 0;
  }
  /* justify-self start/end on the base keeps both buttons at their
     content width — on a phone the primary action takes the room. */
  .portal-onb-substep-foot > [data-onb-action="sub-back"] {
    grid-area: back;
    justify-self: stretch;
  }
  /* The primary CTA (+ optional Skip) live inside the __actions wrapper
     since the 2026-08-02 footer rework. */
  .portal-onb-substep-foot > .portal-onb-substep-foot__actions {
    grid-area: next;
    display: flex;
    gap: var(--pm-2);
    margin: 0;
  }
  .portal-onb-substep-foot__actions [data-onb-action="sub-next"] { flex: 1; }
  .portal-onb-substep-foot__actions [data-onb-action="sub-skip"] { flex: 0 0 auto; }
  .portal-onb-substep-foot > .portal-onb-help { grid-area: help; }

  /* On the first sub-step Back is disabled. A dead 100px button next to
     the only live action is worse than no button — drop it and let
     Continue take the full width it deserves. */
  .portal-onb-substep-foot > [data-onb-action="sub-back"]:disabled { display: none; }
  .portal-onb-substep-foot:has([data-onb-action="sub-back"]:disabled) {
    grid-template-areas:
      "next next"
      "help help";
  }

  .portal-onb-btn--xl {
    min-height: var(--pm-cta);
    height: auto;
    border-radius: var(--pm-r-input);
    padding: var(--pm-2) var(--pm-4);
    line-height: 1.25;
    transition:
      background-color 200ms ease,
      border-color 200ms ease,
      transform 140ms var(--pm-ease);
  }
  .portal-onb-btn--xl:active:not(:disabled) { transform: scale(0.98); }
  /* Hover lift is a mouse idiom; on touch it fires on tap and fights
     the press feedback. */
  .portal-onb-btn--primary:hover:not(:disabled),
  .portal-onb-sheet__close--floating:hover {
    transform: none;
    box-shadow: none;
  }
  .portal-onb-help {
    max-width: none;
    font-size: var(--portal-text-caption);
    line-height: 1.4;
  }
  /* The phone number is the fallback for anyone who cannot finish the
     form — it has to be a target, not just coloured text. */
  /* This is the number a member calls when the screen has defeated them,
     so it is the last link that should be hard to hit — and it was 24px
     tall. Padding grows the hit area to 44px; the negative margin keeps
     the line rhythm of the sentence it sits inside, since vertical padding
     on an inline box expands the target without moving the text. */
  .portal-onb-help__tel {
    display: inline-block;
    /* 14px text on a 19.6px line box, so 13px of padding each side is what
       actually reaches 44 — 10px landed at 40, which still failed. */
    padding: 13px 6px;
    margin: -13px -6px;
    text-decoration: underline;
    text-underline-offset: 3px;
  }

  /* ---- 5. Celebration ---- */

  .portal-onb-celebration {
    min-height: 100%;
    justify-content: center;
    padding: var(--pm-6) var(--pm-gutter)
             calc(var(--pm-6) + env(safe-area-inset-bottom, 0px));
    gap: var(--pm-3);
  }
  /* The finale carries a whole offer card underneath — that one scrolls
     from the top rather than floating in the middle of the fold. */
  .portal-onb-celebration--final { justify-content: flex-start; }
  .portal-onb-celebration .portal-onb-badge-art { width: 88px; height: 88px; }
  .portal-onb-celebration__title { font-size: var(--portal-text-section); }
  .portal-onb-celebration__body { font-size: var(--portal-text-label); }
  .portal-onb-celebration .portal-onb-btn {
    width: 100%;
    min-height: var(--pm-cta);
    border-radius: var(--pm-r-input);
  }
  .portal-onb-credit-offer { grid-template-columns: minmax(0, 1fr); }
  .portal-onb-credit-offer__actions { display: grid; gap: var(--pm-2); }

  /* ---- 6. Reduced motion ---- */

  @media (prefers-reduced-motion: reduce) {
    .portal-onb-sheet__panel,
    .portal-onb-sheet.is-open .portal-onb-sheet__panel {
      transition: opacity 140ms ease;
      transform: none;
    }
    .portal-onb-btn--xl:active:not(:disabled),
    .portal-onb-pick__option:active,
    .portal-onb-branch-pick__option:active,
    .portal-onb-status-pick__option:active,
    .portal-onb-upload:active,
    .portal-onb-sheet__close--floating:active { transform: none; }
  }
}

/* =========================================================================
   CREDIT SURFACES — DATA DISPLAY  (≤768px)

   Sprint P2 made the credit pages fit: nothing scrolled sideways any more.
   It did not make them read. Every component in this section was still a
   desktop layout reflowed into 390px, and the failure mode was the same one
   each time — a two- or three-column row whose narrow column is a *sentence*.
   At this width that column becomes a ribbon and the sentence becomes one
   word per line:

     "Navy / Federal / Credit / Card"      (accounts table)
     "Revol / ving"                        (mid-WORD, type column)
     "Apr 22, / 2026 Review inquiry → / ·  / Equifax"   (alert rows)
     "12- / month / score / history"       (history card)

   The rule this section applies everywhere: on a phone, a row that pairs a
   label with a value stacks; a row that pairs content with an action puts
   the action underneath; and a table stops being a table. Columns survive
   only where both sides are short and fixed (a number and a unit, a state
   and a tick).

   Type floors hold throughout — 18px for prose, 16px for interactive text,
   14px for meta. Several of these components were under them.
   ========================================================================= */
@media (max-width: 768px) {

  /* ---- 1. Hero score card (/dashboard) ----

     Five fragments sharing a box: the product name wrapped mid-name, the
     delta wrapped and floated off to the right unattached to anything, the
     300–850 track sat away from the score it measures, and the state line
     orphaned its own bullet onto a line of its own.

     Rebuilt as the thing every credit app makes it: the number is the
     card. Product name above it, change pinned beside it, range beneath
     it, verdict last.

        VantageScore 3.0
        724   [↑ +5 this month]
        ▬▬▬▬▬▬▬▬●▬▬▬▬▬
        300               850
        ● Good range · 57 points to Excellent                          */
  .portal-hero { padding-left: var(--pm-gutter); padding-right: var(--pm-gutter); }
  .portal-hero__meta {
    font-size: var(--portal-text-caption);
    line-height: 1.45;
    /* Stops "Next refresh May 24, 2026" breaking after "May". */
    text-wrap: pretty;
  }

  .portal-hero__score {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas:
      "label  label"
      "number delta"
      "band   band"
      "scale  scale"
      "state  state";
    align-items: end;
    gap: 0;
  }
  /* `display: contents` dissolves the head wrapper so the label and the
     delta can be placed independently — the label belongs above the
     number, the delta beside it. */
  .portal-hero__score-head { display: contents; }
  .portal-hero__score-label {
    grid-area: label;
    margin-bottom: var(--pm-2);
    font-size: var(--portal-text-caption);
    letter-spacing: 0.14em;
    white-space: nowrap;
  }
  .portal-hero__score-number {
    grid-area: number;
    font-size: var(--portal-num-headline);
    line-height: 0.9;
    letter-spacing: -1.5px;
  }
  /* The change is a fact about the number, so it sits with it — as a pill,
     which also stops a two-word phrase wrapping into a two-line column. */
  .portal-hero__score-delta {
    grid-area: delta;
    justify-self: start;
    align-self: center;
    margin-left: var(--pm-3);
    padding: 5px var(--pm-3);
    border-radius: 999px;
    background: rgba(201, 162, 74, 0.16);
    font-size: var(--portal-text-caption);
    font-weight: 600;
    white-space: nowrap;
  }
  .portal-hero__score-band { grid-area: band; margin-top: var(--pm-4); }
  .portal-hero__score-band-scale {
    grid-area: scale;
    font-size: var(--portal-text-caption);
  }
  .portal-hero__score-state {
    grid-area: state;
    align-items: flex-start;
    margin-top: var(--pm-3);
    font-size: var(--portal-text-label);
    line-height: 1.45;
  }
  /* A 6px dot on a `center`-aligned flex row centres against the whole
     two-line block, which is what stranded it on its own line. Pin it to
     the first line instead. */
  .portal-hero__score-state-dot {
    flex: 0 0 6px;
    margin-top: 8px;
  }

  /* ---- 2. 12-month history card (/dashboard) ----

     `__head` is a two-column row: [title + caption] and [delta]. At 390px
     the first column measured 105px, so the title set four lines deep
     ("12- / month / score / history") and its caption five, and the chart
     — the entire point of the card — got what was left.

     Stacked: title, caption, then the change as a pill on its own line,
     then the chart at full width and enough height to show a trend. */
  .portal-history__head {
    display: block;
    margin-bottom: var(--pm-4);
  }
  .portal-history__head > div:first-child { width: auto; }
  .portal-history__head h2 {
    font-size: var(--portal-text-card-title);
    line-height: 1.25;
  }
  .portal-history__sub {
    font-size: var(--portal-text-caption);
    line-height: 1.45;
    text-wrap: pretty;
  }
  .portal-history__delta-wrap {
    display: inline-flex;
    /* The base stacks these; in a pill they read as one statement. */
    flex-direction: row;
    align-items: baseline;
    gap: var(--pm-2);
    width: fit-content;
    align-self: flex-start;
    margin-top: var(--pm-3);
    padding: 6px var(--pm-3);
    border-radius: 999px;
    background: var(--portal-olive-tint);
  }
  .portal-history__delta-label {
    font-size: var(--portal-text-caption);
    white-space: nowrap;
  }
  .portal-history__delta { font-size: var(--portal-text-card-title); }
  .portal-history__chart svg { width: 100%; height: 150px; }
  .portal-history__chart-meta { font-size: var(--portal-text-caption); }

  /* ---- 3. Alert rows (/dashboard, /alerts) ----

     The worst offender in the cluster. `.portal-alert` is a three-column
     flex row — chip · content · action — and `__content` has no
     `min-width: 0`, so the chip and the action take their natural widths
     and the text column is starved to *zero*. Measured: 0–13px. That is
     what produced

        "Apr 22, / 2026   Review inquiry →   / ·  / Equifax"

     — not a wrap, a collapse. The action was overlapping the date because
     both were competing for a column that no longer existed.

     Rebuilt as: title with its severity chip beside it, the source
     underneath, the action last. */
  .portal-alert {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "title chip"
      "meta  meta"
      "cta   cta";
    align-items: start;
    column-gap: var(--pm-3);
    row-gap: 6px;
    padding: var(--pm-4) var(--pm-gutter);
  }
  .portal-alert > * { min-width: 0; }
  .portal-alert__chip {
    grid-area: chip;
    justify-self: end;
    white-space: nowrap;
  }
  /* The wrapper around title + source has to dissolve so both can be
     placed on the row grid — it is the element that was collapsing. */
  .portal-alert__content { display: contents; }
  .portal-alert__title {
    grid-area: title;
    min-width: 0;
    font-size: var(--portal-text-card-title);
    line-height: 1.25;
  }
  .portal-alert__source {
    grid-area: meta;
    min-width: 0;
    font-size: var(--portal-text-caption);
    line-height: 1.4;
  }
  .portal-alert__cta {
    grid-area: cta;
    justify-self: start;
    min-height: var(--pm-tap);
    display: inline-flex;
    align-items: center;
    gap: var(--pm-2);
    font-size: var(--portal-text-label);
    font-weight: 600;
    white-space: nowrap;
  }

  /* ---- 4. "Continue exploring" tiles (/dashboard) ----

     Four 2-up tiles whose inner column measured 107px, so every label
     wrapped ("Credit / report", "Path to improve / your score") and the
     tiles ended up different heights. Four destinations read better as
     four full-width rows than as a ragged 2×2 grid — and it is the same
     row shape as the alerts above, so the page has one rhythm. */
  .portal-quick { grid-template-columns: minmax(0, 1fr); gap: var(--pm-2); }
  .portal-quick-tile {
    display: grid;
    grid-template-columns: 44px minmax(0, 1fr);
    grid-template-areas:
      "icon title"
      "icon sub";
    align-items: center;
    column-gap: var(--pm-4);
    row-gap: 2px;
    min-height: 76px;
    padding: var(--pm-3) var(--pm-4);
    text-align: left;
  }
  .portal-quick-tile > svg,
  .portal-quick-tile__icon { grid-area: icon; align-self: center; }
  .portal-quick-tile__title {
    grid-area: title;
    min-width: 0;
    font-size: var(--portal-text-card-title);
    line-height: 1.2;
  }
  .portal-quick-tile__sub {
    grid-area: sub;
    min-width: 0;
    font-size: var(--portal-text-caption);
    line-height: 1.4;
  }

  /* ---- 5. Data tables become cards (/report) ----

     `.vs-table` is a real <table>. The accounts table wants 668px of
     columns; at 390px it got 260, which squeezed the Type column to 65px
     and broke words down the middle — "Revol / ving", "Instal / lmen / t".
     No amount of column tuning survives that ratio, and a horizontal
     scroll box hides five of six columns behind a gesture.

     So the table stops being a table. Each row becomes a card: the first
     cell is its heading, every other cell becomes a label/value line
     carrying the column name it came from (stamped onto `data-label` from
     the real <th> by portal-mobile-nav.js), and the expandable detail
     opens underneath the card it belongs to.

     Specificity: report.html carries a page-local <style> for this table
     — `.report-page .vs-table tbody td` at (0,2,2), much of it
     `!important`, and it includes the `word-break: break-word` that is
     the literal cause of "Revol / ving". An inline <style> also wins
     ties against a linked sheet. Rather than answer a page-scoped
     selector with another one (Rule 3), the canonical class is doubled —
     `.vs-table.vs-table` — which lifts these to (0,3,x) while still
     matching nothing but the component itself. `!important` appears only
     where the declaration being replaced carries it. */
  .vs-table.vs-table,
  .vs-table.vs-table tbody,
  .vs-table.vs-table tr,
  .vs-table.vs-table td { display: block; width: auto; }
  /* The header row's job is done — its labels are inside the cells now. */
  .vs-table.vs-table thead { display: none; }
  .vs-table.vs-table {
    border-collapse: separate;
    table-layout: auto;
  }

  /* Card at rest shows what the reference apps show: the account and the
     number. Chase's list is "nothing but the name and balance of each
     account"; everything else waits behind the disclosure this table
     already has. The first build kept the table's thinking instead —
     Type, Balance, Utilization and Status as four equal label/value
     lines, so nothing led and the card was five rows tall.

     Now: name + balance on the headline row, type and last-4 as one quiet
     meta line, and utilisation as a bar, because on a credit report that
     is the one number that carries a judgement rather than a fact. The
     rest opens on tap.

        Navy Federal Credit Card        $3,480   ⌄
        Revolving · *1550
        ▬▬▬▬▬▬▬░░░░░░░░  41% used     Pays as agreed          */
  .vs-table.vs-table tbody tr.vs-table__row {
    position: relative;
    display: grid;
    /* The name gets its own full-width line. Sharing row 1 with the
       balance left it ~150px and "Navy Federal Credit Card" set four
       lines deep while "Mortgage Account" broke mid-word. The balance
       drops to the meta line, where it still leads on size and now sits
       clear of the chevron. */
    /* Third attempt, and the one that cannot break. Sharing a row with
       the balance starved the name; giving the balance a `max-content`
       track instead pushed the whole grid wider than the card and set the
       table scrolling sideways. Both were the same mistake — competing
       for one line.

       So nothing competes: name, then the amount on its own line at
       display size, then type and last-4 as a two-column meta row where
       the last-4 is the flexible one. Every track is bounded by the card,
       so no content can force an overflow. */
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas:
      "name    name"
      "balance balance"
      "type    sub"
      "util    util"
      "status  status";
    align-items: baseline;
    column-gap: var(--pm-3);
    row-gap: 2px;
    /* Room for the chevron, which is what the account name was running
       underneath. */
    padding: var(--pm-4) 52px var(--pm-4) var(--pm-4);
    /* Consecutive cards had no gap, so their 1.5px borders doubled into a
       ladder down the list. */
    margin-bottom: var(--pm-2);
    border: 1.5px solid var(--portal-hairline) !important;
    border-radius: var(--pm-r-card);
    background: var(--portal-surface);
  }

  /* Any column this sprint has not placed (Inquiries' Date / Purpose)
     keeps the label/value line — it is still the right shape there. */
  .vs-table.vs-table tbody tr td[data-label] {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: baseline;
    column-gap: var(--pm-3);
    padding: 5px 0 !important;
    border: 0;
    text-align: left !important;
    font-size: var(--portal-text-label) !important;
    overflow: visible;
    text-overflow: clip;
    word-break: normal;
    overflow-wrap: break-word;
  }
  .vs-table.vs-table tbody tr td[data-label]::before {
    content: attr(data-label);
    font-size: var(--portal-text-caption);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--portal-ink-muted);
    white-space: nowrap;
  }
  .vs-table.vs-table tbody tr td[data-label]:not(:first-child) > * {
    justify-self: end;
    text-align: right;
  }

  /* Account cell → the headline. */
  .vs-table.vs-table tbody tr td:first-child {
    display: contents;
  }
  .vs-table.vs-table tbody tr td:first-child::before { content: none; }
  .vs-table.vs-table .vs-table__primary {
    grid-area: name;
    /* Clear of the chevron parked in the corner. */
    padding-right: 4px;
    min-width: 0;
    font-size: var(--portal-text-card-title);
    font-weight: 600;
    line-height: 1.25;
    text-align: left;
  }
  /* Last-4 joins the type on the meta line rather than hanging under the
     name on a line of its own. */
  .vs-table.vs-table .vs-table__sub {
    grid-area: sub;
    justify-self: start;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--portal-text-caption);
    color: var(--portal-ink-muted);
    text-align: left;
    order: 2;
  }

  /* Balance leads: the only figure in the card at display size. */
  .vs-table.vs-table tbody tr td[data-label="Balance"] {
    grid-area: balance;
    justify-self: start;
    margin-top: 2px;
    padding: 0 !important;
    border: 0;
    font-family: var(--portal-font-display);
    font-size: var(--portal-num-data) !important;
    font-weight: 500;
    line-height: 1.1;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
  }
  .vs-table.vs-table tbody tr td[data-label="Balance"]::before { content: none; }

  /* Type sits inline with the last-4 as one quiet line. */
  .vs-table.vs-table tbody tr td[data-label="Type"] {
    grid-area: type;
    padding: 0 !important;
    border: 0;
    font-size: var(--portal-text-caption) !important;
    color: var(--portal-ink-muted);
    text-align: left !important;
  }
  .vs-table.vs-table tbody tr td[data-label="Type"]::before { content: none; }
  /* The separator that joins them, so the markup needs no new element. */
  .vs-table.vs-table tbody tr td[data-label="Type"]::after {
    content: " \00B7";
    color: var(--portal-ink-muted);
  }

  /* Utilisation as a bar. A percentage in a list of percentages says
     nothing; a filled track says "how close to the limit" at a glance. */
  .vs-table.vs-table tbody tr td[data-label="Utilization"] {
    grid-area: util;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: var(--pm-2);
    margin-top: var(--pm-3);
    padding: 0 !important;
    border: 0;
    font-size: var(--portal-text-caption) !important;
    color: var(--portal-ink-secondary);
    text-align: left !important;
  }
  .vs-table.vs-table tbody tr td[data-label="Utilization"]::before {
    content: "";
    flex: 1 1 auto;
    height: 6px;
    border-radius: 3px;
    background:
      linear-gradient(to right, var(--portal-olive) 0 var(--util, 0%),
                                var(--portal-hairline-strong) var(--util, 0%) 100%);
  }
  /* An installment account reports "—" for utilisation; a bar at 0 would
     read as "nothing used", which is wrong, so it shows no bar. */
  /* Installment and mortgage accounts report "—" for utilisation. A bar
     would misread as "none used" and a lone dash is noise, so the line
     goes entirely for those accounts. */
  .vs-table.vs-table tbody tr td[data-label="Utilization"][data-util="none"] {
    display: none;
  }

  /* Standing is an exception signal, not a headline. */
  .vs-table.vs-table tbody tr td[data-label="Status"] {
    grid-area: status;
    justify-self: start;
    margin-top: var(--pm-2);
    padding: 0 !important;
    border: 0;
    text-align: left !important;
  }
  .vs-table.vs-table tbody tr td[data-label="Status"]::before { content: none; }
  .vs-table.vs-table tbody tr td[data-label="Status"] .vs-chip {
    max-width: 100%;
    white-space: normal;
  }

  /* The expand toggle sits in the card's top-right corner rather than
     occupying a column of its own. */
  .vs-table.vs-table tbody tr td.vs-table__chev-col {
    position: absolute;
    top: var(--pm-3);
    right: var(--pm-3);
    width: auto !important;
    padding: 0 !important;
  }
  .vs-table.vs-table tbody tr td.vs-table__chev-col::before { content: none; }

  /* Detail panel — belongs to the card above it, so it is joined to it
     rather than floating as a sibling block. */
  .vs-table.vs-table tbody tr.vs-table__expand { padding: 0; border: 0 !important; }
  .vs-table.vs-table tbody tr.vs-table__expand > td {
    /* The cell keeps a width computed under the desktop `table-layout:
       fixed`, which left the detail grid 175px wide inside a 326px card. */
    display: block;
    width: auto !important;
    max-width: none !important;
    padding: var(--pm-4) !important;
    margin: calc(-1 * var(--pm-2)) 0 var(--pm-2);
    border: 1.5px solid var(--portal-hairline);
    border-top: 0;
    border-radius: 0 0 var(--pm-r-card) var(--pm-r-card);
  }
  /* Ten fields. Two columns of tracked caps at ~82px produced
     "UTILIZA / TION" and "MINIMU / M PAYMEN / T"; putting label and value
     side by side instead let the label's max-content width eat the row
     and the value printed on top of it. Keep the desktop's shape — label
     above value — and give the pair a full half of a now-full-width
     panel. With the break-word clamp lifted, labels wrap at spaces. */
  /* The panel is a flex column that was not stretching its children, so
     the grid sat at 175px inside a 227px cell — which is why the labels
     were still short of room after the column count was fixed. */
  .vs-account-detail {
    align-items: stretch;
    min-width: 0;
  }
  .vs-account-detail > * { width: 100%; min-width: 0; }
  .vs-account-detail__grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--pm-4) var(--pm-3);
  }
  .vs-account-detail__item { display: block; min-width: 0; }
  .vs-account-detail__label,
  .vs-account-detail__value {
    word-break: normal;
    overflow-wrap: break-word;
    text-align: left;
  }
  .vs-account-detail__label {
    font-size: var(--portal-text-caption);
    letter-spacing: 0.06em;
    line-height: 1.3;
  }
  .vs-account-detail__value {
    font-size: var(--portal-text-label);
    line-height: 1.3;
  }

  /* ---- 6. Document rows (/report) ----

     Was a wrapping flex row, so the RPT badge floated above the title and
     "Download" landed underneath at a random indent, detached from the
     document it downloads. Grid: badge and text on one line, the action
     directly beneath them and inside the row's own edges. */
  /* CORRECTION (2026-07-27). Moving the action to its own line stopped it
     interleaving with the meta, but left "Download" as a bare bold word
     floating under the text with no boundary — and doubled the height of
     every row in the list.

     One row instead: badge, the document and its meta, and the action at
     the far right where a list row's action belongs. The card's own copy
     already says "Click any document to download", so the whole row is
     the target and the label shrinks to a compact affordance beside it. */
  .account-row {
    /* One row of three columns left the title 104px and it set four lines
       deep. The title takes the full width beside the badge instead, and
       the action shares the meta line under it — still one visual row per
       document, still an action with a boundary, ~70px instead of 100. */
    display: grid;
    grid-template-columns: 44px minmax(0, 1fr) auto;
    grid-template-areas:
      "icon title title"
      "icon meta  act";
    align-items: center;
    column-gap: var(--pm-3);
    row-gap: 2px;
    padding: var(--pm-3) 0;
  }
  .account-row > .account-doc-icon,
  .account-row > .account-row__icon { grid-area: icon; align-self: center; }
  /* Dissolve the wrapper so title and meta can take their own rows. */
  .account-row__main { display: contents; }
  .account-row__title { grid-area: title; min-width: 0; }
  .account-row__sub { grid-area: meta; min-width: 0; }
  .account-row__title { font-size: var(--portal-text-label); line-height: 1.3; }
  .account-row__sub { font-size: var(--portal-text-caption); }
  .account-row > .portal-btn {
    grid-area: act;
    justify-self: end;
    align-self: center;
    min-height: var(--pm-tap);
    min-width: var(--pm-tap);
    padding: 0 var(--pm-2);
    white-space: nowrap;
    font-size: var(--portal-text-caption);
  }

  /* ---- 7. Card padding ----
     32px of padding each side on a 390px screen leaves 260px of content —
     which is what starved the tables, the detail panel and the split rows
     inside them. 20px is the gutter the rest of this layer uses. */
  .vs-card,
  .vs-card--lg { padding: var(--pm-5) var(--pm-gutter); }

  /* ---- 8. Interest-cost rows (/debts) ----
     Another starved column: the account block measured 88px, so its meta
     collapsed into a four-line ribbon ("*5723 / Mortgage · / 6.5% APR ·
     / $286,410 balance") while "$1,551 /month" floated off to the right,
     unattached to the account it describes. Stacked, amount underneath
     the account it belongs to and sized to lead. */
  .top-interest .top-interest__row.top-interest__row {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--pm-2);
    align-items: start;
    padding: var(--pm-4) 0;
  }
  .top-interest .top-interest__row.top-interest__row > * { min-width: 0; }
  .top-interest .top-interest__amt.top-interest__amt { font-size: var(--portal-num-data); }
  .top-interest__footer.top-interest__footer .vs-btn {
    width: 100%;
    justify-content: center;
    min-height: var(--pm-cta);
  }
  .top-interest__footer.top-interest__footer .vs-caption {
    font-size: var(--portal-text-caption);
    line-height: 1.5;
  }

  .cost-of-carry__split,
  .cost-of-carry__row {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--pm-2);
    align-items: start;
  }
  .cost-of-carry__split > *,
  .cost-of-carry__row > * { min-width: 0; }

  /* ---- 9. Identity protection (/protection) ----

     Hero: the title was clipped by the card's edge — "Protect more than
     y…" — not wrapped. Content was being lost, so it gets to wrap. */
  .vs-hero.vs-hero { padding-left: var(--pm-gutter); padding-right: var(--pm-gutter); }
  .vs-hero .vs-hero__title.vs-hero__title {
    /* protection.html sets `white-space: nowrap !important`, which is what
       cut "Protect more than y…" off at the card's edge — 439px of title
       in a 252px box. It has to be answered in kind. */
    white-space: normal !important;
    overflow: visible;
    text-overflow: clip;
    font-size: var(--portal-text-hero) !important;
    line-height: 1.15 !important;
  }
  .vs-hero .vs-hero__lead.vs-hero__lead { font-size: var(--portal-text-label); line-height: 1.5; }

  /* Monitored identifiers: text block and state pill shared one row, and
     the text block measured 132px — so "Social Security number" set three
     lines deep next to a chip with room to spare. The label spans the row;
     the value and the chip share the line under it. */
  /* Keeping the text block and the chip on one line leaves the text 132px
     of 260 — the chip alone is 116 — which is what set "Social Security
     number" three lines deep. The label takes the full width instead, and
     the value shares the line beneath it with the chip. */
  .id-row.id-row.id-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "label label"
      "value chip"
      "sub   sub";
    align-items: center;
    column-gap: var(--pm-3);
    row-gap: 4px;
    padding: var(--pm-3) 0;
  }
  /* Dissolve the wrapper so its three lines can be placed independently. */
  .id-row.id-row.id-row > div { display: contents; }
  .id-row .id-row__label.id-row__label { grid-area: label; min-width: 0; }
  .id-row .id-row__value.id-row__value { grid-area: value; min-width: 0; }
  .id-row .id-row__sub.id-row__sub { grid-area: sub; min-width: 0; }
  .id-row.id-row.id-row > .vs-chip {
    grid-area: chip;
    justify-self: end;
    white-space: nowrap;
  }
  .id-row .id-row__label.id-row__label {
    font-size: var(--portal-text-caption);
    line-height: 1.3;
    grid-area: label;
  }
  .id-row .id-row__value.id-row__value {
    font-size: var(--portal-text-label);
    grid-area: value;
    /* An email address is one unbreakable token; without this it slides
       under the chip sharing its line. */
    overflow-wrap: anywhere;
  }
  .id-row .id-row__sub.id-row__sub {
    font-size: var(--portal-text-caption);
    grid-area: sub;
  }

  /* ---- 10. Anything still pairing prose with a control ----
     A safety net for the split rows this sprint has not named
     individually. A flex row whose text child has no `min-width: 0`
     collapses that child to zero and prints it one word per line — the
     single defect behind almost every screenshot in this round. */
  .vs-card [class*="__split"] > *,
  .vs-card [class*="-row"] > *,
  .vs-card [class*="__row"] > * { min-width: 0; }

  /* ---- 11. Member home (/portal-activated) ----

     Benefits ledger. Name and amount share a row with the amount pinned
     to the first line, so a two-line name left "$519" hanging beside
     "Active member" with nothing under it. Both names wrap to two lines
     here, so centring the amount against the name block puts every
     figure on one consistent axis down the card. */
  .pact-savings__bucket-head.pact-savings__bucket-head.pact-savings__bucket-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--pm-3);
  }
  .pact-savings__bucket-name.pact-savings__bucket-name.pact-savings__bucket-name { min-width: 0; line-height: 1.25; }
  .pact-savings__bucket-sum.pact-savings__bucket-sum.pact-savings__bucket-sum { white-space: nowrap; flex: 0 0 auto; }
  .pact-savings__bucket-items.pact-savings__bucket-items.pact-savings__bucket-items {
    font-size: var(--portal-text-caption);
    line-height: 1.5;
  }

  /* Tax status. The icon had a column to itself and was centred against
     the whole body, so it floated beside a two-line title rather than
     belonging to it; the chip and the link then sat in a detached row of
     their own. Icon pinned to the title's line, and the chip and action
     indented to the text column so the card has one left edge. */
  .portal-offer-status {
    display: grid;
    grid-template-columns: 44px auto minmax(0, 1fr);
    grid-template-areas:
      "icon body body"
      ".    chip link";
    align-items: center;
    column-gap: var(--pm-3);
    row-gap: var(--pm-3);
  }
  .portal-offer-status__icon {
    grid-area: icon;
    align-self: start;
    margin-top: 2px;
  }
  .portal-offer-status__body { grid-area: body; min-width: 0; }
  .portal-offer-status__title {
    font-size: var(--portal-text-card-title);
    line-height: 1.25;
  }
  .portal-offer-status__sub {
    font-size: var(--portal-text-caption);
    line-height: 1.45;
  }
  .portal-offer-status__chip { grid-area: chip; justify-self: start; }
  .portal-offer-status__link {
    grid-area: link;
    justify-self: end;
    min-height: var(--pm-tap);
    display: inline-flex;
    align-items: center;
    gap: var(--pm-2);
    white-space: nowrap;
  }

  /* Score summary. Five loose pieces of one fact: the number, its change,
     its band, the since-joining note and the rail — none of them related
     to each other on the page. Grouped so the score and its delta share a
     baseline, the band sits directly under the number it names, and the
     note reads as a footnote rather than a button. */
  .pact-summary__score-head.pact-summary__score-head.pact-summary__score-head {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--pm-2) var(--pm-3);
  }
  .pact-summary__score-num.pact-summary__score-num.pact-summary__score-num {
    /* 96px + the delta pill is 312px in a 308px row, so the pill dropped
       to a line of its own and the two stopped reading as one statement.
       One step down the scale and they share the line. */
    font-size: var(--portal-num-headline);
    line-height: 1;
  }
  .pact-summary__score-delta.pact-summary__score-delta.pact-summary__score-delta {
    display: inline-flex;
    align-items: baseline;
    gap: 4px;
    padding: 4px var(--pm-3);
    border-radius: 999px;
    background: rgba(201, 162, 74, 0.16);
    font-size: var(--portal-text-caption);
    font-weight: 600;
    white-space: nowrap;
  }
  .pact-summary__score-band.pact-summary__score-band.pact-summary__score-band {
    margin-top: 2px;
    font-size: var(--portal-text-caption);
    letter-spacing: 0.14em;
  }
  /* The pill was shaped for one line and the sentence needs two, which is
     why it looked inflated. A quiet footnote line carries it better. */
  .pact-summary__score-since.pact-summary__score-since.pact-summary__score-since {
    display: flex;
    align-items: flex-start;
    gap: var(--pm-2);
    margin-top: var(--pm-3);
    padding: 0;
    border: 0;
    border-radius: 0;
    background: none;
    font-size: var(--portal-text-caption);
    line-height: 1.45;
  }
  .pact-summary__score-since.pact-summary__score-since > span:first-child { flex: 0 0 auto; margin-top: 2px; }

  /* Rail labels: five bands across 308px is ~46px each, so "Very good"
     broke in half and "Exceptional" took double its share and pushed the
     row out of true. Only the ends are load-bearing on a phone — the
     thumb already shows where the score sits, and the band is named in
     full directly above. */
  .pact-summary__rail-labels.pact-summary__rail-labels.pact-summary__rail-labels {
    display: flex;
    justify-content: space-between;
    font-size: var(--portal-text-caption);
  }
  .pact-summary__rail-labels.pact-summary__rail-labels > span { flex: 0 0 auto; white-space: nowrap; }
  .pact-summary__rail-labels.pact-summary__rail-labels > span:not(:first-child):not(:last-child) {
    display: none;
  }

  /* ---- 12. Score Coach (/optimal-path) ----

     Every selector here is doubled: optimal-path.html scopes this whole
     component set under `.opath-page` at (0,2,0), the same trap as
     report/debts/protection.

     Goal choices. Three options sized to their own content gave unequal
     widths — and the time-horizon row measured 277px inside a 260px card,
     so it overflowed and every label broke ("3 / months"). A three-way
     choice is one control: equal cells, one height, one row. */
  .goal-choice-options.goal-choice-options {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--pm-2);
    width: 100%;
  }
  .goal-choice-option.goal-choice-option {
    display: flex;
    align-items: center;
    justify-content: center;
    width: auto;
    min-width: 0;
    min-height: 56px;
    padding: var(--pm-2) 4px;
    text-align: center;
    line-height: 1.2;
    font-size: var(--portal-text-label);
    white-space: nowrap;
  }
  .goal-choice-legend.goal-choice-legend {
    margin-bottom: var(--pm-2);
    font-size: var(--portal-text-label);
  }
  /* Current / target were floating in 210px of a 260px card. */
  .goal-rail__heads.goal-rail__heads,
  .goal-rail__track.goal-rail__track,
  .goal-rail .vs-rail__legend { width: 100%; }
  .goal-rail__heads.goal-rail__heads {
    display: flex;
    justify-content: space-between;
    gap: var(--pm-3);
  }

  /* Pathway card. The header row carried an icon, the name and a chip, so
     the name column measured 95px and "Fastest payoff path" set three
     lines deep — then the summary was indented under it, giving the card
     two different left edges and a dead gutter down its side. Name and
     chip stack beside the icon; the summary takes the full width. */
  .pathway-head.pathway-head {
    display: grid;
    grid-template-columns: 48px minmax(0, 1fr);
    grid-template-areas:
      "icon name"
      "icon chip"
      "sum  sum";
    align-items: center;
    column-gap: var(--pm-3);
    row-gap: var(--pm-2);
  }
  .pathway-icon.pathway-icon { grid-area: icon; align-self: start; }
  .pathway-name.pathway-name {
    grid-area: name;
    min-width: 0;
    align-self: end;
    font-size: var(--portal-text-card-title);
    line-height: 1.25;
  }
  .pathway-head.pathway-head > .vs-chip {
    grid-area: chip;
    justify-self: start;
    align-self: start;
  }
  .pathway-summary.pathway-summary {
    grid-area: sum;
    margin-top: var(--pm-2);
    font-size: var(--portal-text-label);
    line-height: 1.5;
  }
  .pathway-step.pathway-step { font-size: var(--portal-text-label); }

  /* Back to Score. A bordered pill above the hero, doing navigation the
     breadcrumb and the section picker beneath it already do, and pushing
     the page title down for the privilege. Demoted to what it is — an
     "up one level" link — and pulled tight to the top of the content so
     it stops reading as a call to action. /optimal-path is not one of the
     six picker sections, so this stays the route back to Score.

     A `--variant` modifier on the canonical, not a page-scoped selector.
     The first pass qualified the canonical button with the page's own root
     class plus an href matcher, and audit-components.sh correctly flagged
     it as the page fighting the bank (CLAUDE.md Rule 3). Catalogued in
     portal-components.md. */
  .portal-btn--back {
    min-height: var(--pm-tap);
    padding: 0;
    border: 0;
    background: none;
    box-shadow: none;
    font-size: var(--portal-text-label);
    font-weight: 600;
  }

  /* ---- 13. NewDay offer perk badge ----

     `border-radius: 999px` with `align-items: center` is a one-line pill.
     At 390px "Free appraisal included · $700 value" needs two lines, so
     the shape stretched into a tall lozenge and the star centred itself
     against the whole block — sitting beside the gap between the lines
     rather than beside the sentence it belongs to.

     A two-line badge is a block, not a pill: card radius, star pinned to
     the first line, text left. Copy untouched (NewDay surface). Rendered
     by portal-prequal-surface.js, so this covers every offer product. */
  .portal-offer-panel__perk {
    display: flex;
    align-items: flex-start;
    width: 100%;
    padding: var(--pm-2) var(--pm-3);
    border-radius: var(--pm-r-input);
    text-align: left;
    line-height: 1.4;
  }
  .portal-offer-panel__perk svg {
    margin-top: 3px;
  }

  /* =====================================================================
     P3 — BENEFITS HUB (/all-benefits)

     The 3-up card grid already collapses (Sprint P2) and the cards
     themselves read well at this width — image, title, offer callout,
     checklist, full-width action. One thing was wrong: the hero's lead
     set six lines of CENTRED prose at 276px.

     Centred text is fine for a line or two of billing; past that the
     reader has to re-find the left edge on every line, which is the
     opposite of what a 65+ audience needs. Doubled to out-specify
     `.bf-page .bf-head__lead` (0,2,0).
     ===================================================================== */
  .bf-head .bf-head__lead.bf-head__lead {
    text-align: left;
    max-width: none;
  }
  .bf-head.bf-head { align-items: flex-start; text-align: left; }
  .bf-head .bf-head__title.bf-head__title { text-align: left; }
  .bf-head .bf-head__eyebrow.bf-head__eyebrow { text-align: left; }

  /* ---- Benefit detail sections: accordion, not tabs ----

     portal-benefit-tabs.js dismantles the strip below the breakpoint and
     plants each label above its own panel (see the comment there for why).
     These rules dress the result as a disclosure list. They have to
     out-specify the <style> that module injects into <head>, which wins
     ties against this file on document order — hence the doubled class. */
  .bnft-tabs--accordion.bnft-tabs--accordion {
    display: block;
    padding: 0;
  }
  .bnft-tabs--accordion.bnft-tabs--accordion .bnft-tabs__btn {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--pm-3);
    width: 100%;
    min-height: 60px;
    margin: 0;
    padding: var(--pm-3) var(--pm-gutter);
    border: 0;
    border-top: 1px solid var(--portal-hairline);
    background: none;
    text-align: left;
    font-size: var(--portal-text-card-title);
    font-weight: 600;
    line-height: 1.25;
    color: var(--portal-ink);
    cursor: pointer;
  }
  .bnft-tabs--accordion.bnft-tabs--accordion .bnft-tabs__btn:first-of-type {
    border-top: 0;
  }
  /* The underline the tab strip used to mark the selected tab has no
     meaning in a stacked list. */
  .bnft-tabs--accordion.bnft-tabs--accordion .bnft-tabs__btn::after {
    content: '';
    flex: 0 0 auto;
    width: 20px;
    height: 20px;
    position: static;
    background: currentColor;
    -webkit-mask: var(--pm-caret) center / contain no-repeat;
    mask: var(--pm-caret) center / contain no-repeat;
    /* P0R-2 — `transform: none` is load-bearing. The injected strip CSS uses
       this same ::after as the selected-tab underline and parks it at
       `scaleX(0)`; re-purposing the pseudo-element without resetting its
       transform left the caret 0px wide. It was invisible on every collapsed
       row and only ever appeared on the one row that arrived expanded, whose
       `rotate(180deg)` replaced the scale by accident. Once every panel
       arrives closed, all four affordances were gone — a disclosure row with
       no chevron is a heading, not a control (playbook §2). */
    transform: none;
    transition: transform 200ms var(--pm-ease);
  }
  .bnft-tabs--accordion.bnft-tabs--accordion .bnft-tabs__btn[aria-expanded="true"] {
    color: var(--portal-olive-deep);
  }
  .bnft-tabs--accordion.bnft-tabs--accordion .bnft-tabs__btn[aria-expanded="true"]::after {
    transform: rotate(180deg);
  }
  .bnft-tabs--accordion.bnft-tabs--accordion > [id^="bnft-panel-"] {
    padding: 0 var(--pm-gutter) var(--pm-5);
  }
  .bnft-tabs--accordion.bnft-tabs--accordion > [id^="bnft-panel-"][hidden] {
    display: none;
  }
  @media (prefers-reduced-motion: reduce) {
    .bnft-tabs--accordion.bnft-tabs--accordion .bnft-tabs__btn::after {
      transition: none;
    }
  }

  /* ---- P3: benefit panel content ----

     The feature rows inside "What's included" reuse `.portal-quick-tile`,
     the canonical I adapted for the dashboard's shortcut tiles. Two things
     did not carry over: the `--feature` variant's icon is 56px against the
     44px column that rule assumes, and the tile sits in a narrower box, so
     the text column fell to 152px. The result was an icon floating at the
     tile's middle-left with a full-width title above a body indented under
     it — two left edges again.

     Icon column sized to the icon and pinned to the title's line; text
     column gets whatever is left. */
  .portal-quick-tile {
    grid-template-columns: auto minmax(0, 1fr);
  }
  .portal-quick-tile > svg,
  .portal-quick-tile__icon { align-self: start; }
  .portal-quick-tile--feature {
    width: 100%;
    align-items: start;
  }
  .portal-quick-tile--feature .portal-quick-tile__sub {
    font-size: var(--portal-text-label);
    line-height: 1.5;
  }

  /* P0R-2 — six feature tiles inside a disclosure panel are the wall in
     miniature: each one brought its own border and fill inside the panel,
     ~200px apiece, 1200px for the list. Inside `.bnft-features` (the list, a
     component context — not a page scope) they lose the box and separate on a
     hairline, which is what a list of six specs wants on a phone. ~130px each.
     The 56px icon square comes down to 40 to match the glance's ring. */
  .bnft-features > li > .portal-quick-tile--feature {
    padding: var(--pm-4) 0;
    background: none;
    border: 0;
    border-radius: 0;
  }
  .bnft-features > li + li > .portal-quick-tile--feature {
    border-top: 1px solid var(--portal-hairline);
  }
  .bnft-features > li > .portal-quick-tile--feature .portal-quick-tile__icon {
    width: 40px;
    height: 40px;
    /* portal.css pads this square by 20px on top of `box-sizing: border-box`.
       At 56px that leaves a 16px well for the glyph; at 40px it leaves 0, and
       `img, svg { max-width: 100% }` in styles.css then clamps a 22px glyph
       down to the 12px content box — the icons rendered as specks. The square
       is a container, not a spacer: the glyph size below is the only control
       over how big the icon reads. */
    padding: 0;
    border-radius: var(--pm-r-input);
  }
  .bnft-features > li > .portal-quick-tile--feature .portal-quick-tile__icon svg {
    width: 22px;
    height: 22px;
  }
  /* P0R-2 — a panel does not re-announce its own disclosure label, where it
     really is the same string. The reader met "What's included" as a 20px
     olive row and then "WHAT'S INCLUDED" as a tracked caps eyebrow 40px
     below it: one string our own code emits twice.

     CORRECTED at P5/P6 verification (2026-07-28): this was written as a
     blanket `.bnft-tabs--accordion .bnft-section__eyebrow { display: none }`,
     which also deleted three eyebrows that are NOT duplicates —
     fox-nation's "What you'll watch" and "How it works", and
     newday-cashout's "Why NewDay". Hiding approved copy on mobile breaks the
     hard rule in components.md. `portal-benefit-tabs.js` now compares each
     eyebrow to its own disclosure label and flags only the true duplicates;
     this hides only what carries the flag. Desktop is untouched either way —
     there is no disclosure row above the eyebrow there. */
  .bnft-tabs--accordion .bnft-section__eyebrow[data-dupe-of-label] { display: none; }

  /* Section leads run to five and six centred lines on these pages, the
     same problem as the hub hero: past a couple of lines the reader has to
     re-find the left edge every time. */
  .bnft-section__lead,
  .bnft-hero__lead,
  .bnft-activate__body {
    text-align: left;
  }

  /* The benefit page's own container adds 32px of side padding, and inside
     an accordion panel that stacks on the panel's own gutter — 246px of
     content on a 390px screen, which is what starved the feature tiles.
     The panel owns the gutter; the inner container stops adding one.

     P0R-2: this rule was written at (0,1,0) and lost — `.portal-body main`
     already supplies `--pm-gutter`, and each page's inline
     `.bnft-page__inner { padding: 0 var(--portal-space-12) }` loads after
     this file and beats a longhand with its shorthand on source order. So
     the double gutter survived everywhere except inside the accordion: 52px
     of side padding, **286px of content on a 390px screen**. That is 64px of
     line length missing from every headline, lead, tile and poster on all
     nine partner pages — the single most expensive declaration on the page.

     Doubled, and inverted: `main` owns the gutter, the inner container adds
     nothing. Playbook §9a. */
  .bnft-page__inner.bnft-page__inner {
    padding-left: 0;
    padding-right: 0;
  }
  /* 40px of vertical rhythm per section is a desktop measure; inside a
     disclosure panel it reads as dead space. */
  .bnft-tabs--accordion .bnft-section { padding: var(--pm-4) 0; }

  /* ---- Identity verification (/identity-verification) ----

     Reached as the Equifax triad's activation step — `equifax-new-activate`
     redirects here, so this page IS that state rather than a page of its
     own. Two controls were under the 44px floor: the brand lock-up (an 11px
     link) and "Cancel and return to my dashboard" at 25px, which is the
     escape hatch on a screen that asks for a date of birth and an SSN. That
     is the last control that should be hard to hit. */
  .iv-page__brand {
    display: inline-flex;
    align-items: center;
    min-height: var(--pm-tap);
  }
  .iv-back {
    min-height: var(--pm-tap);
    display: inline-flex;
    align-items: center;
    text-align: left;
  }

  /* ---- P3: benefit activation wizard (*-new-activate) ----

     TaxHawk is the real template for the triad's middle state — Equifax's
     `-activate` and `-active` both redirect into the credit cluster, so it
     is the exception rather than the pattern.

     Footer: a three-item flex row — meta, Back, Continue — at 350px, which
     starved "Your progress is saved…" to 91px. Same failure and same fix
     as the profile wizard's footer: the two actions share the top row, the
     note drops beneath them. */
  /* The page's own <style> uses bare classes but sits after the stylesheet
     link in <head>, so it wins specificity ties. Doubled. */
  .bnft-wiz__footer-inner.bnft-wiz__footer-inner {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas:
      "back next"
      "meta meta";
    align-items: stretch;
    column-gap: var(--pm-3);
    row-gap: var(--pm-2);
  }
  .bnft-wiz__footer-inner.bnft-wiz__footer-inner {
    padding: var(--pm-3) var(--pm-gutter);
    padding-bottom: calc(var(--pm-3) + env(safe-area-inset-bottom, 0px));
  }
  .bnft-wiz__footer-inner > .portal-btn--ghost { grid-area: back; }
  .bnft-wiz__footer-inner > .portal-btn--primary { grid-area: next; }
  .bnft-wiz__footer-meta.bnft-wiz__footer-meta {
    grid-area: meta;
    margin-right: 0;
    min-width: 0;
    font-size: var(--portal-text-caption);
    line-height: 1.4;
  }
  .bnft-wiz__footer-inner.bnft-wiz__footer-inner > .portal-btn {
    min-height: var(--pm-cta);
    justify-content: center;
  }

  /* The consent checkbox was rendering at 13px. This is the control that
     authorises Veteran Strong to assemble a tax return — the most
     consequential tick in the product, and the smallest thing on the page.
     24px box inside a 44px row, with the whole label as the target. */
  .bnft-wiz__consent-check {
    display: flex;
    align-items: flex-start;
    gap: var(--pm-3);
    min-height: var(--pm-tap);
    padding: var(--pm-2) 0;
    cursor: pointer;
  }
  .bnft-wiz__consent-check input[type="checkbox"] {
    flex: 0 0 24px;
    width: 24px;
    height: 24px;
    margin: 0;
    accent-color: var(--portal-olive);
  }
  .bnft-wiz__consent-check > span,
  .bnft-wiz__consent-check > p {
    min-width: 0;
    font-size: var(--portal-text-label);
    line-height: 1.45;
  }

  /* ---- P3: benefit active state (*-new-active) ---- */

  /* An `inline` <a> carrying two clauses ("Where's my refund? · Track it
     at taxhawk.com") measured 40px tall — just under the floor, and the
     kind of miss that only shows up when you actually measure. */
  .bnft-active__launch-link {
    display: inline-flex;
    align-items: center;
    min-height: var(--pm-tap);
  }

  /* ---- P3: activation wizard chrome ----

     Both of these are the starved-column failure again, in the component
     every partner triad reuses.

     Bar: a four-item flex row — Save & exit, title, spacer, counter — in
     350px. Every one of them wrapped mid-phrase ("Save / & exit",
     "TaxHawk / activation", "Step 1 / of 4"). The two controls are short
     and belong together on one line; the title takes the row beneath,
     where it fits without breaking. */
  .bnft-wiz__bar-inner.bnft-wiz__bar-inner {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas:
      "back counter"
      "title title";
    align-items: center;
    column-gap: var(--pm-3);
    row-gap: 2px;
    padding: var(--pm-2) var(--pm-gutter);
  }
  .bnft-wiz__bar-inner > .bnft-wiz__back {
    grid-area: back;
    min-height: var(--pm-tap);
    display: inline-flex;
    align-items: center;
    white-space: nowrap;
  }
  .bnft-wiz__bar-inner > .bnft-wiz__counter {
    grid-area: counter;
    justify-self: end;
    white-space: nowrap;
    font-size: var(--portal-text-caption);
  }
  .bnft-wiz__bar-inner > .bnft-wiz__title {
    grid-area: title;
    min-width: 0;
    font-size: var(--portal-text-caption);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* The spacer exists to push the counter right in a flex row; the grid
     places it explicitly, so the spacer has nothing left to do. */
  .bnft-wiz__bar-inner > .bnft-wiz__spacer { display: none; }

  /* Step rail: five labels — Consent · Tax profile · Documents · Review ·
     Done — across 350px. "Tax profile" broke in half and "Done" was
     clipped at the right edge. The labels are redundant here: the step is
     named in full immediately below in the "CONSENT · STEP 1 OF 4"
     eyebrow. So the rail keeps its bars as a segmented meter and drops
     the labels, which is the same treatment the profile wizard's rail
     already uses. */
  .bnft-wiz__sections.bnft-wiz__sections {
    display: flex;
    gap: 4px;
    width: 100%;
  }
  .bnft-wiz__section.bnft-wiz__section { flex: 1 1 0; min-width: 0; }
  /* The page sets `display: block` on a bare class in an inline <style>,
     which wins the tie against this file — doubled. */
  .bnft-wiz__section-label.bnft-wiz__section-label { display: none; }
  .bnft-wiz__section-bar { width: 100%; }
}

/* ==========================================================================
   P0R — BENEFIT PRE-ACTIVATION PAGE (`*-new`), shared by nine partners
   equifax · taxhawk · fox-nation · agero · afba · adt · newzip ·
   newday-home · newday-cashout

   P3 delivered the SHARED components on these pages (the .bnft-tabs
   accordion, the .bnft-wiz footer/bar, the double-gutter fix) and repaired
   the defects it found. It never designed the page. The structural collapses
   were already there as desktop breakpoints — .bnft-hero__grid → 1 col at
   880, .bnft-features → 1 col at 560, .bnft-perk → 2 col at 480,
   .portal-video-hero__panel → 1 col at 900 — so the page reflowed and passed
   every defect check while keeping desktop's 40px section padding, centred
   section heads and auto-width CTAs at 390px. The hero measured taller than
   the viewport it was being read on.

   This block is the density and rhythm pass. Every value comes from the
   playbook's vertical-rhythm tiers: heading→content 8–16, block→block 16–24,
   section padding 32–40 with a 48 ceiling, card padding 16–20.

   P0R-2 is the composition pass on top of it, designed against a written
   pixel budget — see `mobile-blueprint-benefit-pre-activation.md`. Density
   was necessary and not sufficient: with correct rhythm the page still
   opened on 396px of nav + profile banner + crumb band, put its video CTA
   above its own poster, nested a card inside a card twice, and arrived with
   a 1296px accordion panel already open. Rules tagged `P0R-2` below are that
   pass; each one spends or reclaims a number from the budget table.

   SPECIFICITY: the bnft-* rules live in each page's inline <style>, which
   loads AFTER this file. Bare classes lose the tie, so every bnft-* selector
   here is doubled. The exceptions are .bnft-perk*, .bnft-glance__perks,
   .bnft-video and .portal-video-hero* — those live in portal.css, which loads
   BEFORE this file, so a single class wins on source order.
   ========================================================================== */
@media (max-width: 768px) {

  /* ---- Action bar: breadcrumbs only ----

     It was `position: sticky; top: 0; z-index: 20` while .portal-nav is also
     sticky at top: 0 — two bars competing for one anchor. It carries nothing
     but breadcrumbs, and a permanent 44px wayfinding rail cannot earn its
     height on a ~700px canvas when the nav and the 3-tab bar already own
     navigation. Back into flow, where it reads as page context.

     P0R-2: the crumb trail already carries the canonical 48px tap height, so
     the band's own 12px/12px padding was buying nothing but 24px — and its
     side gutter was doubling `main`'s. The band IS the crumb row: 48px. */
  .bnft-actionbar.bnft-actionbar {
    position: static;
    z-index: auto;
  }
  .bnft-actionbar__inner.bnft-actionbar__inner {
    max-width: none;
    padding: 0;
    gap: var(--pm-3);
  }
  /* The three-level crumb trail collapses to `All benefits › <this page>` via
     the canonical .portal-crumbs rule earlier in this file — nothing
     benefit-specific is needed here. */

  /* ---- Hero ---- */
  .bnft-hero.bnft-hero {
    padding: var(--pm-6) 0 var(--pm-8);
  }
  .bnft-hero__grid.bnft-hero__grid {
    gap: var(--pm-6);
    align-items: stretch;
  }
  .bnft-hero__eyebrow.bnft-hero__eyebrow { margin-bottom: var(--pm-2); }
  /* 32px is the playbook's mobile display ceiling and this is the page's
     one h1 — it keeps the size and gives back the margin instead. */
  .bnft-hero__title.bnft-hero__title { margin-bottom: var(--pm-3); }
  .bnft-hero__lead.bnft-hero__lead {
    margin-bottom: var(--pm-5);
    max-width: none;
  }
  .bnft-hero__actions.bnft-hero__actions {
    align-items: stretch;
    gap: var(--pm-3);
  }
  .bnft-hero__actions > .portal-btn {
    width: 100%;
    min-height: var(--pm-cta);
  }
  /* The trust line ("Encrypted end to end · About 5 minutes" and its per-
     partner variants) wraps to two lines at 390px. `align-items: center` then
     centres the whole block against a 14px icon, so the icon floats between
     the lines and the text looks unanchored. Top-align both and nudge the icon
     onto the first line's optical centre. Eight pages carry this line;
     equifax-new's was removed on request (2026-07-28). */
  .bnft-hero__trust.bnft-hero__trust {
    display: flex;
    align-items: flex-start;
    gap: var(--pm-2);
    line-height: 1.45;
  }
  .bnft-hero__trust > svg {
    flex: none;
    margin-top: 3px;
  }

  /* ---- At-a-glance card ---- */
  .bnft-glance.bnft-glance {
    padding: var(--pm-5);
    gap: var(--pm-4);
  }
  .bnft-glance__row.bnft-glance__row { gap: var(--pm-4); }
  .bnft-glance__row.bnft-glance__row + .bnft-glance__row {
    padding-top: var(--pm-4);
  }
  /* portal.css — single class wins on source order. */
  .bnft-glance__perks {
    padding-top: var(--pm-4);
    gap: var(--pm-2);
  }

  /* P0R-3 — the glance is a spec sheet: label, value, detail, marker.

     P0R-2 had flattened the nested box (`.bnft-perk` was bringing its own
     fill, border and 16px padding inside `.bnft-glance`'s), but the card was
     still crowded, because the content was carrying five type registers, two
     left edges and a redundant badge in 242px:

       - two tracked-caps labels, a 20px title, 14px body and a pill — and
         the pill said "$3M coverage" directly under a title that already
         said "$3M identity theft insurance";
       - the 40px icon indented the copy 52px past both labels, so the card
         had a left edge at 20 and another at 72;
       - the chip then hung under the copy column, aligned to neither edge.

     Restructured as two rows of one shape — `label` → `value` → `detail` →
     `marker`, every one of them flush to a single left edge. The only
     right-set element in the card is the partner mark, which pairs with its
     own label on one line. The icon goes: it is `aria-hidden` decoration
     marking a list of **one** item, and it was the sole cause of the second
     left edge. With the full 310px measure the title lands on one line and
     the chip reads as the row's closing value rather than a floating pill.

     SPECIFICITY: these were doubled because `newday-home-new.html` forked the
     entire component into its own inline `<style>` — a parallel copy of the
     canonical (CLAUDE.md Rule 3) that loaded after this file, so single-class
     rules lost on that one page and only that one page: its perk copy stayed
     indented to 126px while the other eight sat flush at 41. **The fork was
     deleted on 2026-07-28** (it was byte-identical to the canonical in
     portal.css, so removing it changed nothing on desktop). The doubling is
     kept anyway — it costs nothing, matches the neighbouring blocks, and is
     the cheapest guard if a page ever forks the component again. */
  .bnft-perk__icon.bnft-perk__icon { display: none; }
  .bnft-perk.bnft-perk {
    display: block;
    padding: 0;
    background: none;
    border: 0;
    border-radius: 0;
  }
  .bnft-perk__copy.bnft-perk__copy { gap: 4px; }
  .bnft-perk__title.bnft-perk__title {
    font-size: var(--portal-text-card-title);
    line-height: 1.3;
  }
  /* The chip's white fill + hairline is the card's own treatment at pill
     scale, so it read as a second card sitting inside the first. Olive tint,
     no border — the same tag treatment the feature-row icon squares use — so
     it reads as a value marker and the card is left with one box. */
  .bnft-perk__chip.bnft-perk__chip {
    display: inline-flex;
    align-items: center;
    margin-top: var(--pm-3);
    background: var(--portal-olive-tint);
    border-color: transparent;
  }
  /* A mark optically centres against its label; `align-items: baseline` with
     a replaced element aligns the image's bottom edge to the text baseline
     and sits the logo low. */
  .bnft-glance__row.bnft-glance__row { align-items: center; }

  /* The mark is a provider credit, not a banner. 150px of Equifax logotype
     beside an 89px label was the loudest ink in the card. */
  .bnft-glance__logo.bnft-glance__logo {
    max-height: 22px;
    max-width: 108px;
  }

  /* ---- Explainer video ----

     .portal-video-hero had no rule in this layer on any page. portal.css
     stacks the split panel at 900px, which is a tablet measure: 24px padding,
     a 16/10 frame and a 28px title (not a token) all survive to 390px. */
  .bnft-video { padding: 0 0 var(--pm-6); }

  /* P0R-2 — the poster comes before the button that plays it.

     `__copy` (which contains the `Watch video` CTA) is a sibling *before*
     `__frame`, so the moment the split panel stacks the page renders
     eyebrow → title → sub → [olive full-width button] → poster. The control
     arrives before the thing it controls, and the poster reads as a
     decorative footer under a button.

     `display: contents` lifts the copy's children into the panel's own flex
     column, which is what makes them orderable against the frame — a nested
     `order` inside `__copy` could never move the CTA past a sibling of its
     parent. Result: eyebrow → title → sub → poster → Watch video.

     The frame goes back to 16/9. The retro-fit had cropped it to 3/2 to buy
     height back at 252px of width; with the double gutter gone the poster is
     318px wide, and 16/9 at 318 costs the same 179px 3/2 cost at 252. */
  .portal-video-hero__panel {
    display: flex;
    flex-direction: column;
    /* portal.css centres the two desktop columns against each other. As a
       flex column that centres every child on the cross axis, and the eyebrow
       — the one shrink-to-fit item — drifted to the middle while the title and
       sub below it stayed left. Two left edges inside one card. */
    align-items: stretch;
    padding: var(--pm-4);
    gap: var(--pm-3);
  }
  .portal-video-hero__copy { display: contents; }
  .portal-video-hero__panel .portal-video-hero__frame {
    order: 1;
    aspect-ratio: 16 / 9;
    max-height: none;
    margin-left: 0;
  }
  .portal-video-hero__cta {
    order: 2;
    margin-top: var(--pm-2);
    width: 100%;
    min-height: var(--pm-cta);
  }
  /* Art direction, not a resize: the explainer character's face sits in the
     upper-centre of the plate. Default centring crops the forehead. */
  .portal-video-hero__poster { object-position: 50% 22%; }
  /* The medallion's halo is a 10px spread ring sized against the 68px desktop
     button — 15% of that diameter, 21% of this one. At 48px it stopped reading
     as a ring and started reading as a pale smudge across the presenter's
     shoulder. Scaled with the button; the drop shadow comes down with it. */
  .portal-video-hero__play {
    width: 48px;
    height: 48px;
    box-shadow:
      0 0 0 6px rgba(90, 107, 63, 0.18),
      0 12px 28px -10px rgba(0, 0, 0, 0.5);
  }
  .portal-video-hero--split .portal-video-hero__title {
    font-size: var(--portal-text-section);
  }

  /* ---- Section primitives ---- */
  .bnft-section.bnft-section { padding: var(--pm-8) 0; }
  /* Centred heads are a desktop affordance: at 390px a three-line centred
     title makes the reader re-find the left edge on every line. The lead was
     already being left-aligned further up this file — the head follows it, so
     the whole block agrees. */
  .bnft-section__head.bnft-section__head {
    max-width: none;
    margin-bottom: var(--pm-6);
    text-align: left;
  }
  .bnft-section__lead.bnft-section__lead {
    margin-top: var(--pm-3);
    max-width: none;
  }
  /* P0R-2: the rows separate on hairlines now (see `.bnft-features > li`
     above), so a flex gap would double the rhythm. */
  .bnft-features.bnft-features { gap: 0; }

  /* ---- Activate panel ----

     The body copy was already left-aligned here (see the shared lead rule
     above), which left a centred eyebrow and title sitting over left-aligned
     prose. The panel goes left as one piece. */
  .bnft-activate.bnft-activate {
    padding: var(--pm-6) var(--pm-5);
    text-align: left;
  }
  .bnft-activate__inner.bnft-activate__inner { max-width: none; }
  .bnft-activate__title.bnft-activate__title { margin-bottom: var(--pm-3); }
  .bnft-activate__body.bnft-activate__body { margin-bottom: var(--pm-5); }
  .bnft-activate__actions.bnft-activate__actions {
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    gap: var(--pm-3);
  }
  .bnft-activate__actions > .portal-btn {
    width: 100%;
    min-height: var(--pm-cta);
  }
  .bnft-activate__meta.bnft-activate__meta { margin-top: var(--pm-4); }

  /* ---- "How activation works" — canonical .portal-steps ----

     pages.css stacks the 3-column grid to one column at 900px and deletes the
     connector. That leaves a numbered sequence with nothing sequencing it: a
     48px coin sitting ABOVE each title, three near-identical blocks, ~480px
     of page. The number is doing the work of an ordered list and the reader
     has to infer the order from the digits alone.

     Rebuilt as the vertical timeline this pattern wants on a phone — coin
     beside the copy, connector restored as a rule running down between the
     coins. The item height drops to whatever the text needs (~86px rather
     than ~144px) and the sequence becomes visible instead of inferred. */
  .portal-steps { gap: var(--pm-5); }
  .portal-steps__item {
    display: grid;
    grid-template-columns: 48px minmax(0, 1fr);
    grid-template-areas:
      "num title"
      "num sub";
    column-gap: var(--pm-4);
    row-gap: 4px;
    align-items: start;
  }
  .portal-steps__num { grid-area: num; }
  .portal-steps__title { grid-area: title; }
  .portal-steps__sub { grid-area: sub; }
  /* The desktop connector is horizontal (top/left/right + height:1px). Every
     one of those axes has to be reset, or the line reappears as a stub beside
     the coin. Runs from just under the coin into the row gap below. */
  .portal-steps__item::before {
    display: block;
    top: calc(48px + 4px);
    bottom: calc(-1 * var(--pm-5) - 4px);
    left: 23px;
    right: auto;
    width: 1px;
    height: auto;
  }
  .portal-steps__item:last-child::before { display: none; }

  /* ---- Fine print ---- */
  .bnft-fine.bnft-fine {
    max-width: none;
    padding: var(--pm-5) 0 var(--pm-6);
    text-align: left;
  }
  /* The summary is the only control in this section and it was a 14px
     inline-block — well under the tap floor. */
  .bnft-fine.bnft-fine summary {
    display: flex;
    align-items: center;
    min-height: var(--pm-tap);
    padding: 0;
  }
  .bnft-fine__body.bnft-fine__body {
    margin-top: var(--pm-3);
    margin-inline: 0;
  }
}

/* ==========================================================================
   P0R-4 — BENEFIT MANAGED STATE (`*-new-active`), shared by eight partners
   fox-nation · agero · afba · adt · newzip · newday-home · newday-cashout ·
   taxhawk   (equifax-new-active is a redirect into /dashboard)

   Third and last shared template of the benefit triad. Blueprint, with the
   pixel budget and the measured before/after:
   `mobile-blueprint-benefit-managed.md`.

   These pages reported ONE untouched block in audit-mobile-coverage.sh
   (`bnft-active-body`, a body class) and were still undesigned: the hero was
   716px on an 844px screen, the Manage section 941px, the document 3812px —
   and two hard defects the coverage number cannot see. See §1 of the
   blueprint; both are fixed below.

   SPECIFICITY: every `.bnft-active__*` rule lives in each page's inline
   <style>, which loads after this file, so every selector here is doubled
   (playbook §9a). `.portal-crumbs` and `.portal-btn` are canonicals and are
   not.
   ========================================================================== */
@media (max-width: 768px) {

  /* ---- The double gutter, again ----
     `.bnft-active__inner` keeps its 32px page padding on top of the 20px
     `main` already supplies: 286px of content on a 390px screen. It is what
     printed the hero lead on five lines and what pushed the summary row past
     its own card. `main` owns the gutter. */
  .bnft-active__inner.bnft-active__inner {
    max-width: none;
    padding-left: 0;
    padding-right: 0;
  }

  /* ---- Action bar: breadcrumbs only ----
     `position: sticky; top: 0` while .portal-nav is also sticky at top: 0 —
     two bars competing for one anchor, the same bug `.bnft-actionbar` had on
     the pitch page. It carries the crumb trail and nothing else, and the
     trail already owns the 48px tap height, so the band IS the row. */
  .bnft-active__bar.bnft-active__bar {
    position: static;
    z-index: auto;
  }
  .bnft-active__bar-inner.bnft-active__bar-inner {
    max-width: none;
    padding: 0;
    gap: var(--pm-3);
  }

  /* ---- Hero: one fold, ending on the action ---- */
  .bnft-active__hero.bnft-active__hero {
    padding: var(--pm-6) 0 var(--pm-8);
  }
  .bnft-active__hero-grid.bnft-active__hero-grid {
    gap: var(--pm-6);
    align-items: stretch;
  }
  .bnft-active__eyebrow.bnft-active__eyebrow { margin-bottom: var(--pm-2); }
  .bnft-active__title.bnft-active__title { margin-bottom: var(--pm-3); }
  .bnft-active__lead.bnft-active__lead {
    margin-bottom: 0;
    max-width: none;
  }

  /* ---- The hero's second column ----

     This is where the managed state stops being one template. The eight pages
     share the bar, the hero copy, `__inner`, `__section`, `__summary`,
     `__activity-empty` and `__manage` — but the hero aside is a different
     block on almost every partner:

       fox-nation · agero · newzip  →  .bnft-active__dispatch
       afba                         →  .bnft-active__policy
       adt                          →  .bnft-active__status
       newday-cashout               →  .bnft-active__estimate
       taxhawk                      →  .bnft-active__launch (+ .bnft-active__tabs)
       newday-home                  →  .portal-offer-panel (a canonical)

     `audit-mobile-coverage.sh` groups all of them under one `bnft-active`
     namespace, which is why these pages read as 1 untouched block while five
     of the seven panels had no mobile rule at all — every one still carrying
     desktop's 32px card padding at 390px.

     Rather than name seven blocks, this keys on the aside's structural
     position in the shared grid: whatever panel a partner puts in the hero's
     second column gets phone padding and a full-width action. Component-in-
     component, so it is not a page scope. */
  .bnft-active__hero-grid.bnft-active__hero-grid > aside { padding: var(--pm-5); }
  .bnft-active__hero-grid.bnft-active__hero-grid > aside .portal-btn {
    width: 100%;
    min-height: var(--pm-cta);
  }

  /* ---- Dispatch panel: the action, and the only filled surface up here ---- */
  .bnft-active__dispatch.bnft-active__dispatch { padding: var(--pm-5); }
  .bnft-active__dispatch-eyebrow.bnft-active__dispatch-eyebrow {
    margin-bottom: var(--pm-2);
  }
  .bnft-active__dispatch-headline.bnft-active__dispatch-headline {
    margin-bottom: var(--pm-2);
  }
  .bnft-active__dispatch-sub.bnft-active__dispatch-sub {
    margin-bottom: var(--pm-4);
  }
  .bnft-active__dispatch .portal-btn {
    width: 100%;
    min-height: var(--pm-cta);
  }
  .bnft-active__dispatch-meta.bnft-active__dispatch-meta {
    margin-top: var(--pm-3);
  }
  /* Agero's dispatch number is `1-800-123-4567` at 48px with
     `white-space: nowrap` — 289px inside a 280px card at 320px, clipped by the
     card's own `overflow: hidden`. **The number a stranded Veteran calls was
     cut off.** It cannot wrap (a phone number broken mid-digits is worse than
     a small one), so it takes the next display step down on the narrowest
     phones. Applies to newzip's member code in the same block for
     consistency; both stay display numbers, and both fit. */
  @media (max-width: 380px) {
    .bnft-active__dispatch-num.bnft-active__dispatch-num {
      font-size: var(--portal-num-stat);
    }
  }

  /* ---- Sections ---- */
  .bnft-active__section.bnft-active__section { padding: var(--pm-8) 0; }
  .bnft-active__section-head.bnft-active__section-head {
    max-width: none;
    margin-bottom: var(--pm-5);
    text-align: left;
  }

  /* ---- Plan summary — a data list, not a table (playbook §4a) ----

     The card is `overflow: hidden` (it clips the row hairlines to its radius)
     and its content measured 322px inside a 284px box, so the tail of
     `james.holloway@veteranstrong.us` was cut off the card — not truncated
     with an ellipsis, silently gone. The row was padded 28/40 inside a 286px
     card, and an email address is one unbreakable token.

     The gutter fix gives the card 350px; the rest is the row: tighter
     padding, `min-width: 0` on every text child, and `overflow-wrap` on the
     one value guaranteed to be longer than its container. The card keeps its
     clipping — the fix belongs on the content. */
  .bnft-active__summary-row.bnft-active__summary-row {
    padding: var(--pm-4) var(--pm-5);
    gap: var(--pm-2);
  }
  .bnft-active__summary-row.bnft-active__summary-row > * { min-width: 0; }
  .bnft-active__summary-value.bnft-active__summary-value {
    min-width: 0;
    overflow-wrap: anywhere;
  }
  .bnft-active__summary-sub.bnft-active__summary-sub {
    min-width: 0;
    overflow-wrap: anywhere;
  }
  /* The row's only control, and on one row it signs the member out of the
     service. It was a 24px-tall text link. It already stacks under the value
     at the page's own 640px breakpoint; what it lacked was height. */
  .bnft-active__summary-edit.bnft-active__summary-edit {
    display: inline-flex;
    align-items: center;
    align-self: start;
    /* Both axes. "Edit" is 30px of text at 16px, so height alone left a
       30×48 target — the label is what sets the box when it hugs its text. */
    min-height: var(--pm-tap);
    min-width: var(--pm-tap);
    justify-self: start;
  }

  /* ---- Activity: an absence should not cost 300px to report ---- */
  .bnft-active__activity-empty.bnft-active__activity-empty {
    padding: var(--pm-6) var(--pm-5);
  }
  .bnft-active__activity-empty-icon.bnft-active__activity-empty-icon {
    width: 48px;
    height: 48px;
    margin-bottom: var(--pm-4);
  }

  /* ---- Status timeline (adt · newday-home · newday-cashout) ----

     `grid-template-columns: 40px 1fr auto` where the third column is a
     nowrap sentence — playbook §4a, and at 320px it was the one thing on
     these two pages that broke the no-horizontal-overflow rule: 40px icon +
     a 139px "Within 1 business day" + two 24px gaps needed 227px inside a
     224px card, and the `auto` track refuses to shrink, so the page grew to
     337px. The time is metadata about the row, so it goes under the row's
     body where §4a puts an action. */
  .bnft-active__status.bnft-active__status { padding: var(--pm-5); }
  .bnft-active__status-row.bnft-active__status-row {
    grid-template-columns: 40px minmax(0, 1fr);
    column-gap: var(--pm-4);
    row-gap: 4px;
    padding: var(--pm-4) 0;
  }
  .bnft-active__status-row.bnft-active__status-row > * { min-width: 0; }
  .bnft-active__status-row.bnft-active__status-row > *:nth-child(3) {
    grid-column: 2;
    white-space: normal;
  }

  /* ---- Manage: action-led cards, not paragraphs with a link after them ---- */
  .bnft-active__manage.bnft-active__manage { gap: var(--pm-4); }
  .bnft-active__manage-card.bnft-active__manage-card { padding: var(--pm-5); }
  .bnft-active__manage-title.bnft-active__manage-title {
    margin-bottom: var(--pm-2);
  }
  .bnft-active__manage-body.bnft-active__manage-body {
    margin-bottom: var(--pm-5);
  }
  .bnft-active__manage-card .portal-btn {
    width: 100%;
    min-height: var(--pm-cta);
  }
}

/* ==========================================================================
   P0R-5 — BENEFIT ACTIVATION FLOW (`*-new-activate`) chrome
   agero · afba · adt · newzip · newday-home · newday-cashout · taxhawk
   (fox-nation's is a single confirm screen with no form; equifax's redirects
   to /identity-verification)

   P1/P3 built the wizard's bar, footer and section progress and verified the
   controls. Three things were never looked at, all visible in one screenshot
   at 390px:

   1. The bar and footer are chrome, but they sat INSIDE `main`'s 20px gutter,
      so both rendered as white bands with a cream gap down each side — a
      sticky top bar and a fixed bottom bar that don't reach the edges read as
      a broken layout.
   2. `.bnft-wiz__progress` reserved 68px and drew NOTHING. `.bnft-wiz` is a
      flex column and the progress block carries `margin: 0 auto`; on a flex
      item the cross-axis `auto` margin cancels `stretch`, so the block sized
      to its content instead of the container. Its content is four
      `.bnft-wiz__section` segments at `flex: 1 1 0` — which, with no space to
      distribute, computed to **0px wide each**. Four invisible segments in a
      76px box: the whole step indicator was gone on every partner wizard,
      and "Step 1 of 3" survived only as text in the bar.
   3. Left edges disagreed: bar content at 40px (main 20 + inner 20), step
      content at 52px (main 20 + step 32).

   `main` stops owning the gutter here — the chrome bleeds, and the step and
   the bar inners each inset by one gutter, so every left edge is 20px.
   ========================================================================== */
@media (max-width: 768px) {

  /* Chrome goes full-bleed; the inners already carry --pm-gutter. */
  main.bnft-wiz.bnft-wiz {
    padding-left: 0;
    padding-right: 0;
  }

  /* Cross-axis auto margin is what collapsed this. `margin-inline: 0` restores
     `align-items: stretch`, the segments get a container to divide, and the
     indicator exists again. */
  .bnft-wiz__progress.bnft-wiz__progress {
    max-width: none;
    margin-inline: 0;
    padding: var(--pm-3) var(--pm-gutter) var(--pm-4);
  }

  /* The step owns one gutter, and 32/40 of desktop vertical padding on top of
     an already-tall form is 72px the form needs more than the whitespace
     does. */
  .bnft-wiz__step.bnft-wiz__step {
    max-width: none;
    padding: var(--pm-5) var(--pm-gutter) var(--pm-8);
  }
  /* 40px between the lead and the first field is a desktop measure; the step
     is the page's one region and it already scrolls. */
  .bnft-wiz__step-lead.bnft-wiz__step-lead {
    margin-bottom: var(--pm-6);
    max-width: none;
  }
  .bnft-wiz__form.bnft-wiz__form { gap: var(--pm-6); }

  /* P5 verification, 2026-07-28: a prefilled value the member is being asked
     to CONFIRM must be readable without scrolling inside the field.
     `#prop-address` ("412 Magnolia Lane, Richmond, VA 23220") measured 341px
     of text in a 318px box at 360px wide — the state and ZIP were cut off.
     16px is the input floor, so the width has to come from the padding.
     Buys ~8px on every wizard field. Below 340px the longest values still
     scroll natively, which is accepted: the value is complete and editable,
     and no component change is worth three characters at the narrowest
     supported width. */
  @media (max-width: 400px) {
    .bnft-wiz__input.bnft-wiz__input,
    .bnft-wiz__select.bnft-wiz__select { padding-inline: var(--pm-3); }
  }

  /* ---- Wizard review rows (all partner wizards) ----
     `1fr auto auto` with the value in the middle: at 390px the value column
     measured 125–150px against a much wider row — playbook §4a. Label above
     value, action underneath, one left edge. The Edit link was also a 46×37
     text link; it is the row's only control. */
  .bnft-wiz__review-row.bnft-wiz__review-row {
    grid-template-columns: minmax(0, 1fr);
    gap: 4px;
    padding: var(--pm-4) 0;
  }
  .bnft-wiz__review-row.bnft-wiz__review-row > * { min-width: 0; }
  .bnft-wiz__review-value.bnft-wiz__review-value { text-align: left; }
  .bnft-wiz__review-edit.bnft-wiz__review-edit {
    display: inline-flex;
    align-items: center;
    justify-self: start;
    min-height: var(--pm-tap);
    min-width: var(--pm-tap);
    padding-inline: 0;
  }

  /* ---- Extracted-value review cards (taxhawk-new-activate, REQ-006 V6.1) ----
     The head is `40px 1fr auto` with a nowrap "Replace document" button in the
     auto track — the same starved-row shape as above. The button takes its own
     row at full width, which also gets it off the 41px height it had as an
     inline text button. */
  .bnft-wiz__doc-head.bnft-wiz__doc-head {
    grid-template-columns: 40px minmax(0, 1fr);
    gap: var(--pm-3) var(--pm-4);
    padding: var(--pm-4) var(--pm-5);
  }
  .bnft-wiz__doc-head.bnft-wiz__doc-head > * { min-width: 0; }
  .bnft-wiz__doc-replace.bnft-wiz__doc-replace {
    grid-column: 1 / -1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: var(--pm-tap);
    white-space: normal;
    border: 1px solid var(--portal-hairline);
    border-radius: var(--pm-r-input);
  }
  .bnft-wiz__doc-fields.bnft-wiz__doc-fields {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--pm-5);
    padding: var(--pm-5);
  }
}

/* ==========================================================================
   P0R-6 — ACCOUNT (/account)

   The page reported 22 untouched namespaces and zero defects — the failure
   was composition. Headline: the five-section tab rail was the horizontal-
   rail trap a FOURTH time ("Billing" and "Membership" fully off-screen at
   390px, no peek, no fade). portal-mobile-nav.js dismantles the rail into a
   disclosure list (same transform as the benefit-detail tabs); these rules
   dress it. One section stays open at all times — that is the page's own
   model, not an invention.

   SPECIFICITY: account.html's inline <style> qualifies everything with
   `.acct-page` (0,2,0) and carries !important on the very properties this
   block must change (hero/panel/tab padding, tab borders). Contested
   declarations here are (0,3,0) + !important — playbook §9a's ugliest case.
   ========================================================================== */
@media (max-width: 768px) {

  /* Stacked gutters: the page's own shell rule sets `main` to 32px sides
     (with !important), and the panel added 32px more — blocks inside
     measured 260px wide on a 390px screen. `main.account` (0,1,1) beats the
     inline `.acct-page` (0,1,0) at equal importance. */
  main.account.acct-page {
    padding: var(--pm-4) var(--pm-gutter) var(--pm-10) !important;
  }
  .account-tabs__panel.account-tabs__panel.account-tabs__panel {
    padding: var(--pm-5) var(--pm-4) !important;
    gap: var(--pm-5);
  }
  /* 40px between every sibling section is a desktop measure. */
  .account__inner.account__inner > * + * { margin-top: var(--pm-4) !important; }

  /* ---- Hero: a settings page doesn't open on a 292px prose card ---- */
  .account-hero.account-hero.account-hero {
    padding: var(--pm-5) !important;
    margin-top: var(--pm-4) !important;
  }
  /* pages.css drops the body to 16px at this width — under the 18px floor. */
  .account-hero__body.account-hero__body { font-size: var(--portal-text-body); }

  /* ---- Accordion rows (built by portal-mobile-nav.js) ---- */
  .account-tabs--accordion .account-tabs__tab.account-tabs__tab {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--pm-3);
    width: 100%;
    min-height: 56px;
    padding: var(--pm-3) var(--pm-4) !important;
    border: 0 !important;
    border-top: 1px solid var(--portal-hairline) !important;
    font-size: var(--portal-text-card-title) !important;
    font-weight: 600 !important;
    color: var(--portal-ink) !important;
    text-align: left;
  }
  .account-tabs--accordion .account-tabs__tab.account-tabs__tab:first-of-type {
    border-top: 0 !important;
  }
  .account-tabs--accordion .account-tabs__tab.account-tabs__tab::after {
    content: '';
    flex: 0 0 auto;
    width: 20px;
    height: 20px;
    background: currentColor;
    -webkit-mask: var(--pm-caret) center / contain no-repeat;
    mask: var(--pm-caret) center / contain no-repeat;
    transition: transform 200ms var(--pm-ease);
  }
  .account-tabs--accordion .account-tabs__tab.account-tabs__tab[aria-selected="true"] {
    color: var(--portal-olive-deep) !important;
  }
  .account-tabs--accordion .account-tabs__tab.account-tabs__tab[aria-selected="true"]::after {
    transform: rotate(180deg);
  }
  @media (prefers-reduced-motion: reduce) {
    .account-tabs--accordion .account-tabs__tab.account-tabs__tab::after { transition: none; }
  }
  .account-tabs--accordion .account-tabs__panel.account-tabs__panel {
    border-top: 1px solid var(--portal-hairline);
  }
  /* Log out: an account action, not a section — its own quiet row at the
     end of the list, full tap height. */
  .account-tabs--accordion .account-tabs__logout.account-tabs__logout {
    display: flex;
    align-items: center;
    justify-content: center;
    width: calc(100% - 32px);
    margin: var(--pm-4) !important;
    min-height: var(--pm-tap);
    border-radius: var(--pm-r-input) !important;
  }

  /* ---- Rows and toggles ----
     The switch renders 44×24 — under the 44px floor on its short axis. The
     visuals are the track/thumb children, so padding grows the hit box
     without touching them. */
  .account-row.account-row { min-height: var(--pm-tap); align-items: center; }
  .account-toggle.account-toggle {
    padding: 10px 2px !important;
    background: transparent;
  }

  /* ---- Cards that stacked desktop padding ---- */
  .account-family-card.account-family-card { padding: var(--pm-4) var(--pm-5) !important; }
  .account-verify.account-verify { padding: var(--pm-4) var(--pm-5) !important; }
}

/* ==========================================================================
   P0R-7 — REFERRAL (/referral) + SUPPORT (/support)

   Both pages were structurally sound at 390 — single-column, no rail traps —
   and both were still desktop-dense: referral's shell holds 32px side
   padding (!important) and every card another 32–36px, so copy ran at
   252px; support's hero and card panels carried 28–36px pads. Rhythm pass
   only; the video rail keeps its dots + peek (playbook §4 allows a carousel
   for secondary content with a visible affordance).
   Referral's inline styles are `.ref-page`-scoped with !important — same
   (0,3,0)+!important treatment as /account. Support's live in pages.css at
   (0,1,0), so plain doubled selectors win there.
   ========================================================================== */
@media (max-width: 768px) {

  /* ---- Referral ---- */
  main.ref.ref-page {
    padding: var(--pm-4) var(--pm-gutter) var(--pm-10) !important;
  }
  .ref-hero.ref-hero.ref-hero { padding: var(--pm-5) !important; }
  .ref-how.ref-how.ref-how { padding: var(--pm-5) !important; }
  .ref-start__copy.ref-start__copy.ref-start__copy { padding: var(--pm-5) !important; }
  .ref-tracker.ref-tracker.ref-tracker,
  .ref-premium.ref-premium.ref-premium { padding: var(--pm-5) !important; }
  .ref__inner.ref__inner > * + * { margin-top: var(--pm-4) !important; }
  /* The share link is one unbreakable token inside a readonly input — it
     clips by nature; the ellipsis says "there's more" instead of cutting
     mid-character. */
  .ref-link-form__input.ref-link-form__input { text-overflow: ellipsis; }

  /* ---- Support ---- */
  .support-hero.support-hero { padding: var(--pm-5); }
  .support-card.support-card { padding: var(--pm-4); }
  .support-card__panel.support-card__panel { padding: var(--pm-5); }
  /* Video titles were single-line ellipsis — a 65+ reader loses half the
     title ("Reading your credit re…"). Cards are fixed-width rail items, so
     the title wraps instead; the row below it absorbs the height. */
  .support-video__title.support-video__title {
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
    line-height: 1.3;
  }
}

/* ==========================================================================
   P0R-8 — CREDIT CLUSTER SHELL (/credit /optimal-path /alerts /debts
   /report /protection)

   The P2c/P2d passes designed the data components; what was never decided
   was the shell: all six pages run on `.vs-container`, whose --page-pad is
   a desktop 32px — 326px of content on a 390px screen while every other
   portal page gets 350. One rule returns the 24px.

   And one §4a row that P2 missed because it renders from JS:
   `.recommendation__row` pairs the title with an impact chip in a
   space-between flex row, which starved the title to 121px ("Pay down
   / revolving / balances", one word per line, beside an untouchable chip).
   Content with a marker stacks — title first, chip beneath.
   ========================================================================== */
@media (max-width: 768px) {
  main.vs-main.vs-container { padding-left: var(--pm-gutter); padding-right: var(--pm-gutter); }

  /* optimal-path's inline styles are `.opath-page`-scoped (0,2,0) and load
     after this file — tripled per §9a. */
  .recommendation__row.recommendation__row.recommendation__row {
    flex-wrap: wrap;
  }
  .recommendation__row.recommendation__row.recommendation__row > .recommendation__title {
    flex: 1 1 100%;
    min-width: 0;
  }

  /* /all-benefits: the category filter is the page's only control under the
     tap floor (145×40). Page-local `.bf-page`-scoped (0,2,0) → tripled. */
  .bf-filter__select.bf-filter__select.bf-filter__select { min-height: var(--pm-tap); }
}

/* ==========================================================================
   P7 — MY ASSETS (/my-assets)

   Blueprint: `mobile-blueprint-my-assets.md`. Defect sweep was clean; the
   failure was composition. "My home" measured **1665px on an 844px screen** —
   two viewports for one asset — and the four-number glance strip took 549px
   because the page's own 720px block stacks it one cell per row. Document
   6067px, 7.2 viewports.

   Playbook §4a applied to a detail block: lead with the figure that carries a
   judgement, open the rest on tap. `.mah-val` (the valuation) stays visible;
   the loan side panel, the partner offer and the document chips go behind a
   disclosure built in portal-mobile-nav.js. Nothing is deleted.

   SPECIFICITY: the page's inline <style> is `.massets-page`-scoped (0,2,0)
   inside its own `@media (max-width: 720px)` block, and loads after this
   file — so contested selectors here are tripled. Its shell padding carries
   !important.
   ========================================================================== */
@media (max-width: 768px) {

  /* 16px gutter was the odd one out across the portal. */
  main.ma2.massets-page { padding: var(--pm-4) var(--pm-gutter) var(--pm-10) !important; }

  /* ---- Glance: 2×2, not four rows ----
     Four stat cells at 137px each is 549px to show four numbers. The page's
     own 1080px breakpoint already has a 2-col version with the right border
     logic — it is only the 720px block that flattens it to one column. */
  .mah-glance__grid.mah-glance__grid.mah-glance__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .mah-glance__cell.mah-glance__cell.mah-glance__cell {
    padding: var(--pm-4);
    border-left: 0;
  }
  .mah-glance__cell.mah-glance__cell.mah-glance__cell:nth-child(even) {
    border-left: 1px solid var(--portal-hairline);
  }
  .mah-glance__cell.mah-glance__cell.mah-glance__cell:nth-child(n + 3) {
    border-top: 1px solid var(--portal-hairline);
  }
  .mah-glance__cell.mah-glance__cell.mah-glance__cell + .mah-glance__cell {
    border-top: 0;
  }
  .mah-glance__cell.mah-glance__cell.mah-glance__cell:nth-child(n + 3) {
    border-top: 1px solid var(--portal-hairline);
  }
  .mah-glance__foot.mah-glance__foot { padding-top: var(--pm-2); }
  /* Floor for the 2-up glance. `--portal-num-stat` is 32px and a value like
     "$439,000" needs 150px; two-up at 320 leaves 139px per cell and the
     number is `nowrap`, so it overflowed its own cell. Below 360 the strip
     goes back to one column — a display number that doesn't fit is worse
     than a taller strip. */
  @media (max-width: 359px) {
    .mah-glance__grid.mah-glance__grid.mah-glance__grid { grid-template-columns: 1fr; }
    .mah-glance__cell.mah-glance__cell.mah-glance__cell:nth-child(even) { border-left: 0; }
    .mah-glance__cell.mah-glance__cell.mah-glance__cell + .mah-glance__cell { border-top: 1px solid var(--portal-hairline); }
  }

  /* The insurances invite CTA — "Set my insurance information" — is a nowrap
     `.portal-btn` in a flex row with no `min-width: 0`, so at 320px it forced
     itself 325px wide inside a 246px container and pushed the whole document
     42px past the viewport. Pre-existing; found in the P7 320px sweep. The
     label wraps and the button fills its row instead. */
  .mah-invite.mah-invite { flex-wrap: wrap; }
  .mah-invite.mah-invite > .portal-btn {
    flex: 1 1 100%;
    min-width: 0;
    white-space: normal;
    text-align: center;
  }

  /* ---- Section head: 209px for an icon, a title and a meta line ----
     The page wraps the head at 720px and pushes the meta to a third row with
     a 64px indent — a second left edge. Icon on the title's line, everything
     else flush. */
  .mah-section.mah-section.mah-section { padding: var(--pm-5) var(--pm-4); }
  .mah-section__head.mah-section__head.mah-section__head {
    align-items: flex-start;
    gap: var(--pm-4);
    padding-bottom: var(--pm-4);
    margin-bottom: var(--pm-4);
  }
  .mah-section__meta.mah-section__meta.mah-section__meta { margin-left: 0; }
  .mah-section__icon.mah-section__icon { flex: none; }
  /* The heading was taking the full content width, so `flex-wrap: wrap` put
     the 48px icon on a row of its own ABOVE the title — three rows and a
     second left edge, 213px of head. Let it share the icon's row. */
  .mah-section__heading.mah-section__heading.mah-section__heading {
    flex: 1 1 0;
    min-width: 0;
  }

  /* ---- The disclosure control ---- */
  .mah-disclose {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--pm-2);
    width: 100%;
    min-height: var(--pm-tap);
    margin: var(--pm-4) 0 0;
    padding: 0 var(--pm-4);
    appearance: none;
    background: var(--portal-surface);
    border: 1px solid var(--portal-hairline-strong);
    border-radius: var(--pm-r-input);
    font-family: var(--portal-font-body);
    font-size: var(--portal-text-label);
    font-weight: 600;
    color: var(--portal-olive-deep);
    cursor: pointer;
  }
  .mah-disclose:active { transform: scale(0.99); }
  .mah-disclose::after {
    content: '';
    width: 18px;
    height: 18px;
    background: currentColor;
    -webkit-mask: var(--pm-caret) center / contain no-repeat;
    mask: var(--pm-caret) center / contain no-repeat;
    transition: transform 200ms var(--pm-ease);
  }
  .mah-disclose[aria-expanded="true"]::after { transform: rotate(180deg); }
  @media (prefers-reduced-motion: reduce) {
    .mah-disclose::after { transition: none; }
  }
  /* Desktop never sees it — the sections are not collapsed there. */
  @media (min-width: 769px) { .mah-disclose { display: none; } }

  /* ---- Collapsed state ----
     Keyed off the class on `.mah-section`, which `innerHTML` on the content
     host cannot destroy (see the comment in portal-mobile-nav.js).

     TRIPLED, and that is not cosmetic: at (0,2,0) this lost to the page's own
     `.massets-page .mah-panel` / `.mah-offer` display rules, which load after
     this file — the offer panel and the loan side panel both stayed visible
     and the "collapse" saved only 214px. Only `.mah-docs` worked, because
     nothing else declares its display. */
  .mah-section--collapsed.mah-section--collapsed.mah-section--collapsed .mah-side,
  .mah-section--collapsed.mah-section--collapsed.mah-section--collapsed .mah-offer,
  .mah-section--collapsed.mah-section--collapsed.mah-section--collapsed .mah-docs { display: none !important; }

  /* ---- Panels and cards: desktop padding ---- */
  .mah-panel.mah-panel { padding: var(--pm-4); }
  .mah-offer.mah-offer { padding: var(--pm-4); }
  .mah-how__card.mah-how__card { padding: var(--pm-4); }
}

/* ==========================================================================
   P7 — CONCIERGE (/concierge)

   Structurally sound at 390 — three stacked sections (hero 427, call 535,
   messages 709), no overflow, no starved columns, no clipping. Two real
   findings, one of them the important kind.

   1. NESTED SCROLL. `.con-thread` is `max-height: 380px; overflow-y: auto`
      with two 245px messages in it — 490px of content in a 380px box. On a
      desktop that is a tidy transcript pane; on a phone it is a scroll
      container inside the page scroll, which is the one interaction a 65+
      member cannot be expected to discover or operate. The page already
      scrolls; the thread flows into it.
   2. Inline controls in prose, under the tap floor — see below.
   ========================================================================== */
@media (max-width: 768px) {

  /* The transcript is not a pane on a phone. */
  .con-thread.con-thread {
    max-height: none;
    overflow-y: visible;
  }

  /* Avatar pairs with the START of its message. `align-items: flex-end` reads
     fine on desktop where a bubble is two lines; at 390 a bubble runs six and
     the face ends up floating beside the timestamp, detached from the message
     it belongs to. */
  .con-msg.con-msg { align-items: flex-start; }

  /* `.con-call__note` reads "Need a different time? [Request a new time], or
     call Kara at [(855) 555-1111]." — a button and a tel: link inline in a
     sentence, measured 131×22 and 95×14.

     An inline control inside prose cannot reach 48×48 without destroying the
     paragraph, and breaking the sentence into stacked fragments would be
     worse than the small target. So this is a deliberate, recorded exception
     rather than a fix pretending to be one:
       - the PRIMARY call path is unaffected and correct — the hero carries a
         56px `tel:` button ("Call <name>") plus Message, so nothing here is
         the only route to the concierge;
       - the note goes 14 → 16px (instructional copy, not metadata — the same
         call made for the referral and support sublabels), and its controls
         get vertical padding inside a roomier line box, which roughly
         doubles the hit area to ~31px without colliding between lines.
     If this ever becomes the only way to reschedule, it needs to leave the
     sentence and become a real button. */
  .con-call__note.con-call__note {
    font-size: var(--portal-text-label);
    line-height: 1.9;
  }
  .con-call__note.con-call__note a,
  .con-call__note.con-call__note .con-link {
    display: inline-block;
    padding: 6px 0;
    font-size: var(--portal-text-label);
  }

  /* Desktop card padding on the two content sections. */
  .con-section.con-section { padding: var(--pm-5) var(--pm-4); }

  /* ---- Hero: a contact card, not a profile page (blueprint §2a) ----
     A 96px avatar centred above a centred name is a PROFILE composition —
     right for a page about someone. This page is about *reaching* someone.
     Measured: the member's own "Call Robert: (855) 555-1111" button sat at
     y=626 of a 705px canvas, because 266px of shared onboarding bar, a 96px
     avatar row and a centred 265px identity block came first.

     Avatar inline at 64px, identity left-aligned beside it, the way every
     messaging app on a phone presents a person. The two actions keep their
     full width and 56px height — they are the point of the page. */
  /* Grid, not flex, because the actions have to span the whole card while the
     identity sits in a column beside the avatar — and they live one level
     down, inside `.con-hero__id`. `display: contents` lifts that wrapper's
     children into this grid so all four can be placed independently; the same
     pattern P0R-2 used to pair the onboarding meter with its counter and to
     put the video poster before its own play button. A first attempt gave the
     actions `flex: 1 1 100%` inside `__id`, which only made them fill the
     column — the screenshot showed both buttons indented 80px, hanging off
     the identity block instead of anchoring the card. */
  .con-hero.con-hero {
    display: grid;
    grid-template-columns: 64px minmax(0, 1fr);
    column-gap: var(--pm-4);
    row-gap: var(--pm-1);
    align-items: start;
    padding: var(--pm-5) var(--pm-4);
    text-align: left;
  }
  .con-hero__avatar-wrap.con-hero__avatar-wrap {
    grid-column: 1;
    grid-row: 1 / span 3;
    width: 64px;
    height: 64px;
  }
  .con-hero__avatar.con-hero__avatar { width: 64px; height: 64px; }
  .con-hero__id.con-hero__id { display: contents; }
  .con-hero__eyebrow.con-hero__eyebrow { grid-column: 2; grid-row: 1; text-align: left; }
  .con-hero__name.con-hero__name { grid-column: 2; grid-row: 2; text-align: left; }
  .con-hero__credential.con-hero__credential { grid-column: 2; grid-row: 3; text-align: left; }
  /* Full card width, under both columns — a 56px target must not be squeezed
     into whatever the identity block leaves over. */
  .con-hero__actions.con-hero__actions {
    grid-column: 1 / -1;
    grid-row: 4;
    margin-top: var(--pm-3);
  }
  .con-hero__actions.con-hero__actions > .portal-btn { flex: 1 1 100%; }
}

/* ==========================================================================
   P7b — ASSET SHEETS (.ma2-sheet, .ma2-detail)

   Blueprint: `mobile-blueprint-asset-sheets.md`. The my-assets page body was
   designed in P7; the sheet — the surface every add and edit flow actually
   happens in — was not opened once. Measured at 390: a 344px centred dialog
   floating with ~95px of dead backdrop above and below, rounded on all four
   corners, 38x38 close button, and three buttons crammed into one 288px
   footer row with the flow's primary action at 116px wide. Zero mobile rules
   existed for either surface.

   Note `100vh`: on iOS Safari that is the LARGE viewport (URL bar hidden), so
   `max-height: calc(100vh - 32px)` permits a panel taller than the visible
   area and the footer holding Continue can sit under the browser chrome.
   `dvh` is the whole point.
   ========================================================================== */
@media (max-width: 768px) {

  /* ---- Bottom sheet, not a centred dialog ----
     Matches `.portal-menu` from P1 — the portal's established sheet pattern —
     and puts the footer in the thumb zone instead of mid-screen. */
  .ma2-sheet__panel.ma2-sheet__panel,
  .ma2-detail__panel.ma2-detail__panel {
    top: auto;
    bottom: 0;
    left: 0;
    width: 100%;
    max-width: none;
    max-height: 88vh;   /* iOS <15.4 fallback; dvh line wins where supported */
    max-height: 92dvh;
    border-radius: var(--pm-r-card) var(--pm-r-card) 0 0;
    /* NO entrance transform, in either state, deliberately.

       The desktop rule centres the panel with
       `transform: translate(-50%, -50%) scale(0.96)` and `.is-open` swaps in
       `scale(1)`, so a bottom sheet has to cancel that transform — and both
       states must be cancelled, or the panel jumps back to the centre the
       moment the class lands.

       It must also be correct WITHOUT the class. `my-assets.js` adds
       `is-open` inside `requestAnimationFrame` at four call sites, and rAF
       does not fire while the document is not painting — a background tab, or
       a phone that locks mid-open. A first attempt here rested the panel at
       `translateY(12px)` and relied on `.is-open` to clear it; measured
       against a real open, the class never arrived and the sheet sat 12px
       below the bottom edge. Position now holds in both states; the existing
       opacity transition is the reveal. */
    transform: none;
  }
  .ma2-sheet.is-open .ma2-sheet__panel.ma2-sheet__panel,
  .ma2-detail.is-open .ma2-detail__panel.ma2-detail__panel {
    transform: none;
  }

  /* Head pinned, body scrolls — a wizard that scrolls its own step counter
     away loses the member's place. */
  .ma2-sheet__head.ma2-sheet__head,
  .ma2-detail__head.ma2-detail__head {
    flex: none;
    padding: var(--pm-5) var(--pm-5) var(--pm-3);
  }
  .ma2-sheet__body.ma2-sheet__body,
  .ma2-detail__body.ma2-detail__body {
    padding-inline: var(--pm-5);
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
  }

  /* ---- Close button: 38 -> 44 on both surfaces ---- */
  .ma2-sheet__close.ma2-sheet__close,
  .ma2-detail__close.ma2-detail__close,
  .ma2-sheet .portal-iconbtn,
  .ma2-detail .portal-iconbtn {
    width: var(--pm-tap);
    height: var(--pm-tap);
  }

  /* ---- Footer: the primary action gets its own row ----
     DOM order is Back, "Need help? Call concierge", Continue. Reordered so
     Continue is the full-width row on top and the two secondaries share the
     row beneath. Safe-area inset so the sheet clears the home indicator. */
  /* `.ma2-sheet__foot` is `display: grid` with `auto 1fr auto` — Back | skip |
     Continue. Flex properties are inert against it, so a first attempt here
     set `order` and `flex` and changed nothing: visual QA showed "Done" still
     165px wide against the left edge, sitting in the first auto track.
     (`.ma2-detail__foot` is already flex.) */
  .ma2-sheet__foot.ma2-sheet__foot,
  .ma2-detail__foot.ma2-detail__foot {
    display: flex;
    flex: none;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--pm-3);
    padding: var(--pm-4) var(--pm-5) calc(var(--pm-4) + env(safe-area-inset-bottom));
  }
  .ma2-sheet__foot.ma2-sheet__foot > .ma2-btn--primary,
  .ma2-detail__foot.ma2-detail__foot > .ma2-btn--primary {
    order: -1;
    flex: 1 1 100%;
    min-height: var(--pm-tap);
    justify-content: center;
  }
  .ma2-sheet__foot.ma2-sheet__foot > .ma2-btn--ghost {
    flex: 0 0 auto;
    min-height: var(--pm-tap);
  }
  .ma2-sheet__foot.ma2-sheet__foot > .ma2-sheet__skip {
    flex: 1 1 auto;
    display: inline-flex;
    align-items: center;
    justify-content: flex-end;
    min-height: var(--pm-tap);
    text-align: right;
  }
  /* The single-button footer ("Done") has nothing to share a row with. */
  .ma2-sheet__foot--single.ma2-sheet__foot--single > .ma2-btn { flex: 1 1 100%; }

  /* Below 360 the concierge link no longer fits beside Back and wraps to a
     row of its own, where right-aligned reads as orphaned. Centre it there. */
  @media (max-width: 380px) {
    .ma2-sheet__foot.ma2-sheet__foot > .ma2-sheet__skip {
      flex: 1 1 100%;
      justify-content: center;
      text-align: center;
    }
  }
}
