/* Points & Prompts v2 — Clean & premium */
/* === WS-THEME ===
   Pass 1 of Fable's chat-first spec v2 §3 (points-relay repo,
   mockups/FABLE-CHATFIRST-SPEC.md). ONE semantic token layer, two
   palettes, switched via [data-theme="dark"|"light"] on <html> -- set
   pre-paint by the inline script in index.html's <head>, before this
   stylesheet is even requested, so there is never a flash of the wrong
   theme. Every color in this file now flows through the tokens below:
   the legacy --ink/--bg/--brand/--ask-.../--shell-... names (defined right
   after) are kept as ALIASES onto these semantic tokens -- so every
   existing rule in this file re-themes with zero further edits -- while
   new rules should reference the semantic names (--bg-surface, --text-1,
   --accent-text, etc.) directly. This retires body.theme-ask-dark (the
   old WS-CLAUDE p2 / WS-CONTRAST dark-only #/ask surface, below) as a
   special case: the assistant view is just the app, in whichever theme
   is active, like every other route now. Contrast ratios measured per
   spec §3.3 (WCAG method). */
:root, :root[data-theme="light"]{
  --bg-canvas:#faf8f1; --bg-deep:#f2eee3; --bg-surface:#ffffff; --bg-surface-2:#f6f2e8; --bg-bubble:#e7ecf2;
  --line:rgba(30,40,51,.10); --line-strong:rgba(30,40,51,.20);
  --text-1:#1e2833;   /* body/headings on surface -- 14.9:1 */
  --text-2:#46525e;   /* secondary prose -- 8.0:1 */
  --text-3:#5c6a77;   /* muted labels, still reading text -- 5.6:1 */
  --text-4:#75828f;   /* faint: >=12px, decorative/non-essential only -- 3.9:1 */
  --accent:#c79a3b;               /* gold fills/borders/icons -- never text */
  --accent-text:#7d5b1d;          /* gold TEXT (bronze in light mode) -- 6.2:1 */
  --accent-soft:rgba(199,154,59,.14);
  --accent-on:#14202b;            /* text on gold fills -- 6.4:1 */
  /* strategy-matrix-redesign: same fill-vs-text split as --accent/--accent-text
     above, applied to --good. --good itself (#0f766e, defined once below,
     not per-theme) is a fill/border color -- it's already used as text at
     19px/800-weight elsewhere and passes there, but the matrix's
     best-in-category number is 13.5px/800, which needs 4.5:1, not 3:1.
     --good-text is that same green, per theme, at a weight that clears
     4.5:1 on --bg-surface: 5.32:1 in light (== --good itself already
     passes), 8.13:1 in dark (a brighter emerald -- --good's own 0f766e
     only manages ~2.9:1 on the dark surface). */
  --good-text:#0f766e;            /* == --good; already 5.32:1 on light --bg-surface */
  /* JOB4 (spend-view.js banners): --good-soft/--good-line/--warn-soft/--warn-line/
     --warn-ink were referenced all over this file (obSecCard, obNeedsBal, obHubTag,
     plaidConnected, .st2Pill.good etc.) only as `var(--x, <hex>)` fallbacks -- never
     actually declared -- so every one of those rules was silently running on its
     fallback. Declaring them here at exactly the values the fallbacks already used
     means light mode renders byte-identical to before; only dark mode (below) changes,
     because that's where the bug was: --text-1 flips to near-white in dark but these
     backgrounds stayed pinned to their light-mode hex. */
  --good-soft:#eaf6f1; --good-line:rgba(15,118,110,.30);
  --warn-soft:#fdf3dd; --warn-line:#e6b34d; --warn-ink:#b7791f;
  --danger-bg:#fdf2f2; --danger-border:#f3c9c9; --danger-border-strong:#e4a6a2;
  --danger-ink:#b3261e; --danger-bg-hover:#fdeceb;
  --bg-surface-translucent:rgba(255,255,255,.92);
  --shadow:0 1px 2px rgba(20,32,43,.06),0 10px 30px rgba(20,32,43,.08);
  --dock-shadow:rgba(20,32,43,.18);  /* WS-DOCK contrast: upward lift off the canvas */
  color-scheme:light;
}
:root[data-theme="dark"]{
  --bg-canvas:#101820; --bg-deep:#0b1219; --bg-surface:#16212c; --bg-surface-2:#1c2a38; --bg-bubble:#20303f;
  --line:rgba(242,237,227,.12); --line-strong:rgba(242,237,227,.18);  /* #19: .09 hairlines were near-invisible on the dark surface -> .12 */
  --text-1:#f2ede3;   /* 13.9:1 */
  --text-2:#cfc9bb;   /* 9.9:1 */
  --text-3:#a4b0bc;   /* muted -- 7.4:1 (was 5.6:1 on the old --ask-muted) */
  --text-4:#8593a1;   /* faint -- 5.2:1 (was the 3.6:1 --ask-faint culprit) */
  --accent:#c79a3b;
  --accent-text:#e5b964;          /* 8.9:1 */
  --accent-soft:rgba(199,154,59,.14);
  --accent-on:#14202b;            /* 6.4:1 on gold */
  --good-text:#34d399;            /* brighter emerald for dark: 8.13:1 on --bg-surface, 9.06:1 on --bg-canvas */
  /* JOB4: dark counterparts of the light-mode fallback hexes above. Solid dark
     fills (not translucent) so the contrast math is exact rather than depending on
     whatever sits underneath. --text-1 dark is #f2ede3 (L=.8499); --good-soft dark
     #163a2c has L=.0339 -> (.8499+.05)/(.0339+.05) = 10.7:1. --warn-soft dark #3a2c12
     has L=.0274 -> (.8499+.05)/(.0274+.05) = 11.6:1. Both clear 4.5:1 by a wide
     margin -- this was the near-white-on-pale-mint bug the job called out. --warn-ink
     is bumped to a lighter amber (was the light-mode #b7791f, too dark to read on the
     new dark warn-soft) -- #f0c775 on #3a2c12 is L=.6067 -> (.6067+.05)/(.0274+.05)
     = 8.5:1. */
  --good-soft:#163a2c; --good-line:rgba(52,211,153,.35);
  --warn-soft:#3a2c12; --warn-line:rgba(230,179,77,.35); --warn-ink:#f0c775;
  --danger-bg:rgba(224,134,104,.14); --danger-border:rgba(224,134,104,.35); --danger-border-strong:rgba(224,134,104,.5);
  --danger-ink:#e08668; --danger-bg-hover:rgba(224,134,104,.22);
  --bg-surface-translucent:rgba(22,33,44,.92);
  --shadow:0 10px 34px rgba(0,0,0,.45);
  --dock-shadow:rgba(0,0,0,.5);  /* WS-DOCK contrast: upward lift off the canvas */
  color-scheme:dark;
}
:root{
  /* legacy sitewide tokens -> semantic pass-throughs (old->new map, spec §3.4) */
  --ink:var(--text-1); --ink2:var(--text-2);
  --bg:var(--bg-surface); --bg2:var(--bg-canvas);
  --brand:#142343; --brand-ink:var(--text-1); --brand-soft:var(--accent-soft);
  --accent-yellow:#ffeb99; --accent-yellow-ink:#7a5c00; /* constant both themes: a solid highlight chip, not a page surface */
  --good:#0f766e; --step:#0f8a6a; --radius:14px; --maxw:1080px; --marketing-maxw:1180px;
  /* Orange accent (wallet.js "Edit points & programs" CTA, Jeremy: "make it
     orange so it stands out"). Constant across themes like --good/--step --
     it's always a solid fill with white text, so it doesn't need a
     per-theme text variant. #c2410c on white text is ~5.2:1 (AA for normal
     text in both themes); the darker hover shade stays comfortably >4.5:1
     too. */
  --orange:#c2410c; --orange-hover:#9a3412;
  /* PP #30 refinement (backlog P0): the centered content-column width the
     Ask-AI composer, thread, and Pass-4 answer panel all share -- so the
     composer reads as a CONTAINED Claude/ChatGPT-style box instead of a
     full-width bar spanning --maxw. See .dockInner/.chatHistoryInner/
     .askHomeCenter below, all of which now key off this one token. */
  --chatcol:760px;
  --font:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  /* Public explainers and the demo use these semantic aliases. Keep them
     attached to the established site stacks so a missing alias cannot make
     an entire font shorthand (including its size) invalid. */
  --ui:var(--font);
  --display:var(--font-display);

  /* legacy #/ask dark-surface tokens (WS-CLAUDE p2, below) -> pass-throughs.
     No longer scoped to body.theme-ask-dark -- the assistant view reads
     the active [data-theme] like everything else now. */
  --ask-canvas:var(--bg-canvas); --ask-surface:var(--bg-surface); --ask-surface-2:var(--bg-surface-2);
  --ask-line:var(--line); --ask-line-strong:var(--line-strong);
  --ask-ink:var(--text-1); --ask-ink-dim:var(--text-2); --ask-muted:var(--text-3); --ask-faint:var(--text-4);
  --ask-gold:var(--accent); --ask-gold-bright:var(--accent-text); --ask-gold-soft:var(--accent-soft); --ask-gold-ink:var(--accent-on);
  --ask-user-bubble:var(--bg-bubble); --ask-wallet-bg:var(--bg-surface); --ask-wallet-ink:var(--text-2);
  --ask-bottom-grad-1:var(--bg-canvas); --ask-bottom-grad-2:var(--bg-deep);
  --ask-chip-border:var(--line-strong); --ask-chip-bg:var(--bg-surface-2); --ask-chip-bg-hover:var(--accent-soft); --ask-chip-ink:var(--text-2);
  --ask-composer-shadow:rgba(0,0,0,.35); --ask-balance-ink:var(--text-4); --ask-error:var(--danger-ink);
  --ask-serif:'Iowan Old Style','Palatino Linotype',Palatino,Georgia,'Times New Roman',serif;

  /* legacy sidebar-shell tokens (WS-SHELL, below) -> pass-throughs. The
     sidebar is no longer a fixed-dark frame independent of the page --
     it follows [data-theme] too, so the whole app (chrome included) is
     one coherent theme. */
  --shell-w:248px;
  --shell-bg:var(--bg-deep); --shell-surface:var(--bg-surface);
  --shell-line:var(--line); --shell-line-strong:var(--line-strong);
  --shell-ink:var(--text-1); --shell-ink-dim:var(--text-2); --shell-muted:var(--text-3); --shell-faint:var(--text-4);
  --shell-gold:var(--accent);
}
/* === end WS-THEME tokens === */
*{box-sizing:border-box}
html{scroll-behavior:smooth;overflow-x:clip;overscroll-behavior-x:none;scroll-padding-top:calc(var(--topnav-h) + 10px)}
/* Lock horizontal drift on mobile: overscroll-behavior-x:none kills the iOS
   sideways rubber-band bounce (the "slightly off-center" scroll), and
   overflow-x:clip catches any sub-pixel overflow WITHOUT creating a scroll
   container (so the sticky topnav keeps working, unlike overflow-x:hidden).
   Intentional horizontal scrollers (.tabs, .matrixScroll) have their own
   overflow-x:auto and are unaffected. */
body{margin:0;font:16px/1.55 var(--font);color:var(--ink);background:var(--bg2);overflow-x:clip;overscroll-behavior-x:none;max-width:100%}
.wrap{max-width:var(--maxw);margin:0 auto;padding:0 20px}
/* Marketing routes gain a wider signed-out canvas; the signed-in app keeps
   the narrower widths that suit comparison and reading tasks. */
#view-points .ppMarketingCanvas{max-width:1100px}
#view-strategy .ppMarketingCanvas{max-width:1020px}
#view-pricing .ppMarketingCanvas{max-width:1100px}
body:not(.ppSignedIn) #view-points .ppMarketingCanvas,
body:not(.ppSignedIn) #view-strategy .ppMarketingCanvas,
body:not(.ppSignedIn) #view-pricing .ppMarketingCanvas{max-width:var(--marketing-maxw)}
a{color:var(--brand-ink)}
h1,h2,h3,h4{line-height:1.14;letter-spacing:-.01em;margin:0 0 .4em}
h1{font-size:clamp(31px,5.2vw,50px);font-weight:800}
h2{font-size:clamp(22px,3.4vw,31px);font-weight:800}
h3{font-size:19px;font-weight:700}
.muted{color:var(--ink2)}
.btn{display:inline-flex;align-items:center;gap:8px;border:0;border-radius:999px;padding:13px 22px;font-weight:650;font-size:15px;cursor:pointer;text-decoration:none;transition:transform .05s,box-shadow .15s;font-family:var(--font)}
.btn:active{transform:translateY(1px)}
.btn.primary{background:var(--brand);color:#fff;box-shadow:var(--shadow)}
.btn.ghost{background:var(--bg-surface);color:var(--brand-ink);border:1px solid var(--line)}
.btn.block{width:100%;justify-content:center}
.btn.sm{padding:9px 15px;font-size:14px}
.btn.accent{background:var(--accent);color:var(--accent-on);box-shadow:0 3px 12px rgba(212,166,81,.4);font-weight:750}
.btn.accent:hover{background:#c79646}
/* Branded "Continue with Google" button (backlog #74). Deliberately NOT
   themed off --brand/--accent -- Google's sign-in button guidelines call
   for a fixed neutral surface so the mark reads as an authentic Google
   control, with dedicated light/dark surface colors (Google publishes
   both) rather than following the app's own light/dark palette. */
.btn.google{background:#fff;color:#3c4043;border:1px solid #dadce0;box-shadow:none;font-weight:600}
.btn.google:hover{background:#f8f9fa;box-shadow:0 1px 2px 0 rgba(60,64,67,.30),0 1px 3px 1px rgba(60,64,67,.15)}
.btn.google:focus-visible{outline:2px solid #4285f4;outline-offset:2px}
.btn.google[disabled]{opacity:.55;cursor:default;pointer-events:none}
.btn.google .googleGIcon{flex-shrink:0}
.btn.google .googleGLabel{font-family:'Roboto',var(--font);letter-spacing:.15px}
[data-theme="dark"] .btn.google{background:#131314;color:#e3e3e3;border-color:#8e918f}
[data-theme="dark"] .btn.google:hover{background:#1e1f20}
[data-theme="dark"] .btn.google:focus-visible{outline-color:#a8c7fa}
/* "Continue with Apple" / "Continue with GitHub" (backlog #18). Same base
   .btn shape/sizing as .btn.google; fixed brand surfaces (not themed off the
   app palette) -- Apple's brand guidance requires the black button not be
   re-tinted, so both keep identical light/dark colours. The marks use
   currentColor, so the white text colour paints the logo too. */
.btn.apple{background:#000;color:#fff;border:1px solid #000;box-shadow:none;font-weight:600}
.btn.apple:hover{background:#1a1a1a}
.btn.apple:focus-visible{outline:2px solid #000;outline-offset:2px}
.btn.apple[disabled]{opacity:.55;cursor:default;pointer-events:none}
.btn.apple .appleLogo{flex-shrink:0}
.btn.apple .appleLabel{letter-spacing:.15px}
.btn.github{background:#24292f;color:#fff;border:1px solid #24292f;box-shadow:none;font-weight:600}
.btn.github:hover{background:#2f363d}
.btn.github:focus-visible{outline:2px solid #24292f;outline-offset:2px}
.btn.github[disabled]{opacity:.55;cursor:default;pointer-events:none}
.btn.github .githubMark{flex-shrink:0}
.btn.github .githubLabel{letter-spacing:.15px}
/* Dark mode: the Apple pill's own black already reads on the dark surface, so
   lift its focus ring / hover for contrast; GitHub gets a hairline so its dark
   fill doesn't blend into the dark panel. */
[data-theme="dark"] .btn.apple{background:#000;color:#fff;border-color:#3a3a3c}
[data-theme="dark"] .btn.apple:focus-visible{outline-color:#a8c7fa}
[data-theme="dark"] .btn.github{border-color:#444c56}
[data-theme="dark"] .btn.github:focus-visible{outline-color:#a8c7fa}
/* In the save modal the branded buttons stack directly (no flex-gap wrapper
   like the chat upsell has), so give Apple/GitHub a little breathing room
   under whatever branded button precedes them. */
#saveScrim .modal .btn.apple.block,#saveScrim .modal .btn.github.block{margin-top:10px}
.eyebrow{display:inline-block;font-size:12.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--brand-ink);background:var(--brand-soft);padding:5px 11px;border-radius:999px;margin-bottom:16px}

header.nav{position:sticky;top:0;z-index:20;background:var(--bg-surface-translucent);backdrop-filter:saturate(1.2) blur(8px);border-bottom:1px solid var(--line)}
.nav .wrap{display:flex;align-items:center;gap:16px;height:62px}
.logo{font-weight:800;letter-spacing:-.02em;font-size:18px;color:var(--ink);cursor:pointer;display:flex;align-items:center;gap:9px}
.logo .dot{width:22px;height:22px;border-radius:7px;background:linear-gradient(135deg,var(--brand),var(--accent))}
nav.links{margin-left:auto;display:flex;align-items:center;gap:4px}
nav.links a{padding:9px 12px;border-radius:9px;font-size:14.5px;font-weight:600;color:var(--ink2);text-decoration:none;cursor:pointer;white-space:nowrap}
nav.links a:hover{background:var(--bg2);color:var(--ink)}
nav.links a.active{color:var(--brand-ink)}

.view{display:none}.view.on{display:block;animation:fade .25s ease}
@keyframes fade{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
section{padding:34px 0}
.lead{max-width:650px;margin-bottom:22px}
.back{cursor:pointer;font-size:14px;color:var(--ink2)}

/* Reusable page-subheading -- replaces the repeated inline
   style="margin-top:-6px" subheads that used to sit under every page's
   <h2> title, one page at a time, with slightly different values each
   time. One class, one place to tune it. */
.pageSub{font-size:13.5px;color:var(--text-3);margin:-2px 0 14px;max-width:720px;line-height:1.5}

.hero{background:linear-gradient(180deg,var(--bg-surface),var(--bg2));border-bottom:1px solid var(--line)}

/* Live "data health" chip -- small status pill for the homepage hero (e.g.
   "Rates last checked 2h ago"). Hidden by default; shown only once the
   health check actually has something to report. */
.dataHealthChip{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:700;
  color:var(--accent-text);background:var(--accent-soft);border-radius:999px;padding:4px 10px;
  line-height:1.4;white-space:nowrap}
.dataHealthChip[hidden]{display:none}
.dataHealthChip .dot{width:6px;height:6px;border-radius:50%;background:var(--good);flex:none}

.save-strip{background:var(--brand);color:#fff;border-radius:var(--radius);padding:22px 24px;display:flex;align-items:center;gap:18px;flex-wrap:wrap}
.save-strip[hidden]{display:none}
.save-strip p{margin:0;flex:1;min-width:220px}
.save-strip .btn.primary{background:#fff;color:var(--brand)}

/* assistant */
.askbox{background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:22px}
.askrow{display:flex;gap:10px;align-items:center;border:2px solid var(--brand-soft);border-radius:999px;padding:8px 8px 8px 22px;background:var(--bg-surface);box-shadow:0 2px 10px rgba(20,32,43,.08);transition:border-color .15s,box-shadow .15s}
.askrow:focus-within{border-color:var(--brand);box-shadow:0 0 0 5px rgba(31,58,95,.12)}
.askrow input{flex:1;border:0;background:none;font-size:17px;font-weight:600;outline:none;padding:12px 0;color:var(--ink);font-family:var(--font)}
/* Ask button must clear the 44px touch-target minimum on its own — don't
   rely on @media(pointer:coarse), which some mobile browsers/emulators
   don't match even on a touch device (Brief #42). */
.askrow .btn{min-height:44px}
@media(max-width:480px){
  .askrow{flex-wrap:wrap;border-radius:22px}
  .askrow input{flex-basis:100%;width:100%}
  .askrow .btn{flex:1;justify-content:center}
}
.starters{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}
.starter{border:1px solid var(--line);background:var(--bg-surface);border-radius:999px;padding:8px 14px;font-size:13.5px;font-weight:600;cursor:pointer;color:var(--ink)}
.starter:hover{border-color:var(--brand);color:var(--brand-ink)}
.chat{max-width:760px}
.bubble{padding:14px 16px;border-radius:16px;margin:10px 0;max-width:88%;font-size:15px;line-height:1.5}
.bubble.me{background:var(--brand);color:#fff;margin-left:auto;border-bottom-right-radius:5px}
.bubble.ai{background:var(--bg-surface);border:1px solid var(--line);border-bottom-left-radius:5px;box-shadow:var(--shadow)}
.bubble.ai strong{color:var(--brand-ink)}
.plan{margin:10px 0 2px;padding:0;list-style:none}
.plan li{padding:8px 0;border-bottom:1px dashed var(--line);display:flex;gap:10px}
.plan li:last-child{border-bottom:0}
.plan .step{width:22px;height:22px;border-radius:50%;background:var(--brand-soft);color:var(--brand-ink);font-weight:800;font-size:12px;display:grid;place-items:center;flex:none}
.srcnote{font-size:12px;color:var(--ink2);margin-top:9px;display:flex;align-items:center;gap:6px}
/* Mobile audit P0 #1 (2026-07-19): the Strategy matrix's "✓ Data verified
   as of… — see recent updates · how we verify → · helpful resources →"
   line reuses .srcnote's flex row, which is fine for its OTHER uses (a
   single short icon+text run, e.g. the citations disclosure) but wrong
   here — this is one long sentence containing 3 inline links, and a
   nowrap flex row with no basis control squeezes each link/text-run down
   toward its own min-content width instead of letting the whole thing
   wrap as a paragraph, so on a narrow phone it collapses into ~4 rigid
   near-equal columns that each word-wrap internally (e.g. "how we verify
   →" stacked one word per line in a ~40px column) — reads as a broken
   render, not cramped text. .stratMetaRow drops back to plain block flow:
   the <a> tags stay inline and the whole line wraps normally at word
   boundaries at any width, same as it would in a plain <p>. */
.stratMetaRow{display:block;line-height:1.6}
/* Brief #61 item 6: the "✓ Answered in-site..." line is now a <summary>
   (see srcnoteHTML/citationsHTML in app.js) when the wallet has data to
   show -- a real disclosure affordance, not static text. */
summary.srcnote{cursor:pointer;list-style:none;user-select:none}
summary.srcnote::-webkit-details-marker{display:none}
summary.srcnote::after{content:"›";display:inline-block;margin-left:2px;transform:rotate(90deg);transition:transform .15s;font-size:13px}
.citationsBlock[open] summary.srcnote::after{transform:rotate(270deg)}
.citationsList{margin-top:8px;padding:10px 12px;background:var(--bg2);border-radius:10px;font-size:13px;display:flex;flex-direction:column;gap:7px}
.citationRow{display:flex;align-items:center;justify-content:space-between;gap:10px}
.citationRow span{color:var(--ink2)}
.ai3-status{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--ink2)}
.ai3-spinner{width:14px;height:14px;border-radius:50%;border:2px solid var(--line);border-top-color:var(--brand);animation:ai3spin .8s linear infinite;flex:none}
@keyframes ai3spin{to{transform:rotate(360deg)}}
.ai3-answer{margin:10px 0 2px}

/* explore */
.tabs{display:flex;gap:6px;border-bottom:1px solid var(--line);margin-bottom:20px;overflow-x:auto}
/* Edge fade hints there's more to scroll when tabs overflow the viewport
   (Brief #42) — a mask fades the buttons themselves so it works over any
   background color, unlike a solid gradient overlay. */
@media(max-width:600px){
  .tabs{-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 28px),transparent 100%);mask-image:linear-gradient(to right,#000 calc(100% - 28px),transparent 100%)}
}
/* Mobile audit P0 #2 (2026-07-19): the mask-image fade above is real but
   subtle enough on a photo/screenshot that testers read it as "zero scroll
   affordance" — the Strategy matrix's own overflow row pairs its identical
   mask with a persistent "Swipe for more →" caption (.matrixSwipeHint)
   that leaves no doubt, so the tab strip gets the same caption here.
   Hidden by default/on wide screens; exTabsUpdateOverflow() (explore.js)
   toggles .on only when the tab row's content genuinely doesn't fit,
   mirroring stratMatrixUpdateOverflow()'s scrollWidth>clientWidth check
   for the matrix so this never shows on a viewport wide enough to fit
   every tab. */
.tabsSwipeHint{display:none;align-items:center;gap:4px;font-size:12px;color:var(--ink2);margin:-14px 0 14px}
.tabsSwipeHint.on{display:flex}
.tab{padding:12px 15px;border:0;background:none;font-size:15px;font-weight:650;color:var(--ink2);cursor:pointer;border-bottom:2.5px solid transparent;white-space:nowrap;font-family:var(--font)}
.tab.on{color:var(--brand-ink);border-bottom-color:var(--brand)}
.tabpane{display:none}.tabpane.on{display:block}
.panel{background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--radius);padding:8px 22px;box-shadow:var(--shadow)}
/* Card-cell consistency (2026-07-22): every logo/monogram in a .row list sits
   in the SAME fixed-width slot (--logoSlot), so the name column starts at one
   x across all rows even when brand marks are mixed sizes (22px program logos,
   28px bank logos, 40px monogram tiles). The slot is the .ico tile size; real
   logos center inside it. */
.row{display:flex;align-items:center;gap:14px;padding:14px 4px;border-bottom:1px solid var(--line);--logoSlot:40px}
.row:last-child{border-bottom:0}
.row .ico{width:var(--logoSlot);height:var(--logoSlot);border-radius:10px;background:var(--bg2);display:grid;place-items:center;font-weight:800;color:var(--brand-ink);flex:none;font-size:12px}
.row .grow{flex:1;min-width:0}
/* Names wrap at word boundaries only — never mid-word — and the value column
   never wraps or shrinks, so a long card name can't push the layout. */
.row .grow strong{overflow-wrap:break-word;word-break:normal;hyphens:none}
.row .val{font-weight:750;color:var(--ink);font-size:14px;text-align:right;white-space:nowrap;flex:none}
.row.hasExtra{flex-wrap:wrap}
.row .rowExtra{flex-basis:100%;display:flex;align-items:center;gap:8px;margin-top:10px;padding-left:54px}
.pill{display:inline-block;font-size:11px;font-weight:700;padding:2px 8px;border-radius:999px;background:var(--accent-soft);color:var(--accent-text);margin-left:6px}
/* beta-tester badge (#118) -- distinct from a plain .pill: gold-bordered,
   uppercase micro-label with a star, so a beta user reads as a deliberate
   status, not an afterthought. --lg for the profile page's larger context. */
.betaBadge{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;padding:2px 8px;border-radius:999px;background:var(--accent-soft);color:var(--accent-text);border:1px solid var(--accent);margin-left:6px;line-height:1.45;vertical-align:middle;white-space:nowrap}
.betaBadge::before{content:"\2726";font-size:9px;line-height:1}
.betaBadge--lg{font-size:11px;padding:3px 11px;gap:5px}
.pill.good{background:var(--brand-soft);color:var(--brand-ink)}
/* "In wallet" (Item #3, Brief #34) gets its own real color, not the same
   washed-out brand-soft/brand-ink combo as "★ Preferred" — built on --good
   (#0f766e) with white text, ~5.5:1 contrast, so "you have this" reads at a
   glance whether it's the ghost-button form (has a balance field to expand)
   or the plain pill form (no currency to expand). */
/* .btn.ghost (background:#fff, 2 classes) outranks a bare .inWalletBtn (1
   class) on specificity regardless of source order — qualify with .btn so
   the override actually wins. */
.btn.inWalletBtn{background:var(--good);color:#fff;border-color:var(--good)}
.btn.inWalletBtn:hover{background:#0b5c55}
.pill.inWallet{background:var(--good);color:#fff}
/* "Edit points & programs" (wallet hub summary, Jeremy: "make it orange so
   it stands out") -- same .btn-qualified-for-specificity trick as
   .btn.inWalletBtn above, since it starts life as a .btn.ghost. Solid
   --orange fill + white text reads clearly against both the light cream
   canvas and the dark navy one, unlike the old plain ghost outline which
   blended into the card in light mode. */
.btn.walletEditBtn{background:transparent;color:var(--orange);border:2px solid var(--orange);display:inline-flex;align-items:center}
.btn.walletEditBtn:hover{background:var(--orange);color:#fff}
.btn.walletDoneBtn{background:transparent;color:var(--good-text);border:2px solid var(--good-text);display:inline-flex;align-items:center;justify-content:center}
.btn.walletDoneBtn:hover{background:var(--good-text);color:#fff}
/* Bug 2 (2026-07-28, Codex round 3): a persistent header Add action beside Edit.
   Filled orange = primary intent (adding is what most people came to do); Edit
   stays the outline secondary. 44px min touch target on all three; the header
   wraps the action group beneath the title at phone widths (hubSummaryHead is
   already flex-wrap). */
.hubHeadActions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end}
.btn.walletAddBtn{background:var(--orange);color:#fff;border:2px solid var(--orange);display:inline-flex;align-items:center;justify-content:center;min-height:44px}
.btn.walletAddBtn:hover{filter:brightness(1.06)}
.btn.walletEditBtn,.btn.walletDoneBtn{min-height:44px}
.hubAddPanelFoot{margin-top:10px;display:flex;justify-content:flex-end}
.btn .btnIc{width:15px;height:15px;margin-right:6px;flex:0 0 auto}

/* Unified badge system -- three modifiers replacing 7 ad-hoc pill variants
   scattered across wallet/matrix/strategy views (.pill.inWallet, .walletPill,
   .exGroupWalletBadge, .adminStatusPill--done/--active, etc). New call sites
   should reach for .badge--held/--best/--meta; the legacy pill rules above
   are left in place for existing markup rather than mass-renamed. */
.badge{display:inline-flex;align-items:center;font-size:10px;font-weight:800;letter-spacing:.03em;text-transform:uppercase;padding:2px 8px;border-radius:999px;line-height:1.4;white-space:nowrap}
.badge--held{background:var(--good,#1c7d4d);color:#fff}
.badge--best{background:var(--accent);color:var(--accent-on)}
.badge--meta{background:var(--bg-surface-2);color:var(--text-3);border:1px solid var(--line)}

/* ---- Points Strategy: full multiplier matrix (Brief #36) ----
   .matrixScroll is the ONLY element that scrolls horizontally — the page
   itself never does, even though the table inside it is wider than any
   phone viewport. The card-name column stays visible via position:sticky
   while the 7 category columns scroll underneath it. */
.matrixScroll{overflow-x:auto;-webkit-overflow-scrolling:touch;border:1px solid var(--line);border-radius:var(--radius);background:var(--bg-surface)}
/* Brief #48: below 1024px the 8-column matrix (1126px) overflows its
   container with no visual cue that it scrolls. Reuse .tabs's mask-image
   edge-fade (Brief #42) so the last column visibly trails off, plus a
   persistent caption — the fade alone reads as too subtle against this
   denser grid of numbers. Scoped to <1024px per brief; see COWORK-STATUS.md
   for a desktop-overflow finding out of this brief's stated scope. */
.matrixSwipeHint{display:none}
@media(max-width:1023px){
  .matrixScroll{-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 28px),transparent 100%);mask-image:linear-gradient(to right,#000 calc(100% - 28px),transparent 100%)}
  .matrixSwipeHint{display:flex;align-items:center;gap:4px;font-size:12px;color:var(--ink2);margin:0 0 6px}
}
/* Brief #50: widened the Strategy section so the matrix fits without
   scrolling at 1440x900+, but a viewport in the ~1024-1439px gap (or any
   width where it genuinely still can't fit) fell through both the media
   query above and the old "desktop never overflows" assumption, getting
   no affordance at all. stratMatrixUpdateOverflow() in app.js measures
   real scrollWidth>clientWidth after render/resize and toggles
   .isOverflowing on .stratMatrix so the same mask+hint show up whenever
   the table truly overflows, at any width. */
.stratMatrix.isOverflowing .matrixScroll{-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 28px),transparent 100%);mask-image:linear-gradient(to right,#000 calc(100% - 28px),transparent 100%)}
.stratMatrix.isOverflowing .matrixSwipeHint{display:flex;align-items:center;gap:4px;font-size:12px;color:var(--ink2);margin:0 0 6px}
/* Brief #60: table-layout:fixed makes column widths come from each column's
   own declared width instead of its widest cell's content — the fix for
   "8x" and "8.5x" value cells rendering at different widths. th[data-sort-key]
   (the category headers) and td (the value cells) share one column each, so
   giving both the same explicit width keeps every value cell identical while
   letting a long category label (e.g. "Online shopping") wrap to two lines
   rather than force its column wider. */
/* strategy-matrix-redesign: text-size-adjust:100% turns off the browser's
   automatic narrow-viewport font boosting for this table specifically —
   measured (Playwright) the exact same 158px-wide, 2-line card name cell
   rendering 2 lines at 1440px but silently wrapping to 3 at 390px with no
   CSS change of ours between them, purely from that auto-boost inflating
   the effective font size on the "mobile" viewport. Left uncontrolled,
   that would undercut the one explicit, deliberate row-height rule right
   below it (uniform row height is the whole point of this redesign) in a
   way no fixed pixel value could fully outrun. */
.matrixTable{border-collapse:collapse;table-layout:fixed;width:max-content;min-width:100%;font-size:13.5px;-webkit-text-size-adjust:100%;text-size-adjust:100%}
.matrixTable th,.matrixTable td{padding:9px 14px;text-align:center;white-space:nowrap;border-bottom:1px solid var(--line)}
.matrixTable th[data-sort-key]:not(.matrixCardCol),.matrixTable td{width:110px}
.matrixTable thead th{font-weight:750;color:var(--ink2);font-size:12px;text-transform:uppercase;letter-spacing:.02em;background:var(--bg2);white-space:normal}
.matrixTable tbody tr:last-child th,.matrixTable tbody tr:last-child td{border-bottom:0}
/* .matrixTable th,.matrixTable td (0,1,1) beats a bare .matrixCardCol (0,1,0)
   regardless of source order, so the nowrap/white-space override below must
   match that specificity or the sticky column silently stays unwrapped and
   its intrinsic width overlaps the scrolling columns next to it. */
.matrixTable th.matrixCardCol{position:sticky;left:0;text-align:left;font-weight:650;background:var(--bg-surface);box-shadow:1px 0 0 var(--line);vertical-align:middle;white-space:normal;width:158px;max-width:158px;font-size:12.5px;line-height:1.25}
.matrixCardCol .plogo{vertical-align:middle;margin-right:6px}
.matrixTable thead .matrixCardCol{background:var(--bg2);z-index:2}
.matrixTable tbody .matrixCardCol{z-index:1}
/* strategy-matrix-redesign: every data row is forced to the same explicit
   height — tall enough for the WORST case (a 2-line-or-more card name plus
   its wallet pill) at the NARROWEST column width the sticky name column
   ever actually renders at — so every row, held or not, long name or
   short, is the same height everywhere. Measured empirically (Playwright):
   (1) min-height on <th>/inline-block name spans is NOT reliably honored
   by Chromium's table layout under border-collapse (a short row's cell
   rendered shorter than its own declared min-height) -- an explicit
   `height` on <tr> in tbody is. (2) The name column's rendered width is
   NOT constant across breakpoints even though its CSS width is: Brief
   #50's .matrixTable{min-width:100%} deliberately stretches the whole
   table to fill .matrixScroll on wide screens, and table-layout:fixed
   distributes that extra width proportionally across every column
   (158px -> ~188px at 1440px; stays a true 158px once .matrixScroll can't
   offer more room than the table needs, e.g. <=390px) -- that's Brief
   #50's own intended desktop behavior, not a bug to undo. So the same
   "American Express Gold" + wallet pill wraps to 2 lines in the wider
   desktop column but 3 lines in the true-158px mobile column. 80px is
   sized for that narrow-column 3-line worst case (measured 78.25px);
   wider desktop columns just get a little header-room on a short name. */
.matrixTable tbody tr{height:80px}
/* strategy-matrix-redesign: wallet membership used to be a row-level green
   background tint + box-shadow frame (removed here) on top of the
   best-in-category cell's fill; now it's just the .walletPill next to the
   card's name (stratMatrixRowHTML), so a held row renders identically to
   any other row except for that one pill — the goal being every row reads
   as plainly as possible, with color reserved for the two signals that
   still carry meaning (best-in-category text, the `*` detail marker). */
.matrixCardCol .pill.walletPill{display:inline-flex;align-items:center;gap:3px;font-size:8px;padding:1px 5px 1px 4px;margin-left:2px;font-weight:700;background:var(--good);color:#fff;vertical-align:middle}
.walletPillIcon{width:9px;height:9px;flex:none}
/* Sortable column headers (Brief #41) — a real <button> fills the <th> so
   Enter/Space activation and focus rings come from the browser, not
   hand-rolled keydown code. aria-sort lives on the <th> per the ARIA spec. */
.matrixSortBtn{all:unset;box-sizing:border-box;display:block;width:100%;cursor:pointer;font:inherit;font-weight:inherit;color:inherit;text-transform:inherit;letter-spacing:inherit;text-align:center}
.matrixTable th.matrixCardCol .matrixSortBtn{text-align:left}
.matrixSortBtn:hover{color:var(--brand-ink)}
.matrixSortBtn:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:4px}
.matrixSortArrow{display:inline-block;width:.9em;color:var(--accent)}
/* Top-earner highlight (strategy-matrix-redesign): was a filled yellow box
   (Brief #41/#50); now a plain bold green NUMBER, no cell fill at all, so
   the grid reads as clean multipliers with color doing the signaling
   instead of a checkerboard of filled boxes. --good-text (not --good) is
   the per-theme text-safe variant of the same green — see its definition
   near the top of this file for the contrast math on why a straight
   var(--good) fails on the dark surface. */
/* #121: best-in-category is lens-specific — the two lenses can crown
   different cards (four cards share "2x other" but only one is 4.62¢/$1),
   so .matrixTop marks the multiplier-lens top and .matrixTopT the
   true-value-lens top; the container's .lensTrue class picks which one
   paints. */
.stratMatrix:not(.lensTrue) .matrixTable td.matrixTop,
.stratMatrix.lensTrue .matrixTable td.matrixTopT{color:var(--good-text);font-weight:800}
/* Brief #44's data-integrity requirement (a cell can earn its headline
   number two different ways — a channel split like CSR's 8x portal / 4x
   direct, or a product_type split like Amex Gold's 5x hotels / 2x cars)
   used to render as a permanent second line in the cell (.matrixSub,
   removed) that made row heights vary. That same real breakdown, plus any
   DB notes (caps, "via portal", etc.), now lives behind this `*` marker's
   data-note attribute and opens on tap in #matrixNoteScrim (see
   PP.showMatrixNote, router.js) — nothing about the underlying numbers
   changed, only where the detail is shown. ::before is an absolutely
   positioned (out of flow) hit-slop so the tap target is comfortably
   larger than the visible glyph without adding to the cell's — and so the
   row's — rendered height. */
.matrixNoteTrig{all:unset;box-sizing:border-box;position:relative;display:inline-flex;align-items:flex-start;justify-content:center;width:10px;height:13px;margin-left:1px;font-size:12px;font-weight:800;line-height:1;color:var(--accent-text);cursor:pointer;vertical-align:top}
/* Mobile audit P1 #5 (2026-07-19): the visible 10x13px glyph is correct —
   dozens of these sit inline in dense matrix cells and a visibly bigger
   button per cell would wreck the grid's alignment — but the old -10px
   inset only grew the actual (invisible) hit target to 30x33px, still
   under the 44px floor. -17px on every side adds 34px in each dimension
   (44x47), clearing 44px on both axes without moving a single visible
   pixel; ::before is absolutely positioned/out of flow so this never
   affects the cell's own layout width or the uniform 80px row height. */
.matrixNoteTrig::before{content:'';position:absolute;inset:-17px}
.matrixNoteTrig:hover{color:var(--brand-ink)}
.matrixNoteTrig:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:50%}
.matrixLegendMarker{display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;font-size:12.5px;font-weight:800;color:var(--accent-text);flex:none}
/* Brief #50: legend for the two matrix highlight signals. flex-wrap lets the
   two items drop to their own line on narrow screens instead of forcing
   horizontal scroll — .matrixLegend sits above .matrixScroll, outside the
   contained scroller, so it must never itself need to scroll. */
/* Brief #51: the legend and the print button share one row so the button
   doesn't just get tacked on below/above with its own stray margin — wraps
   to its own line on narrow screens same as the legend items already do. */
.matrixToolbar{display:flex;flex-wrap:wrap;gap:10px;align-items:center;justify-content:space-between;margin:0 0 10px}
.matrixToolbar .matrixLegend{margin:0}
.matrixLegend{display:flex;flex-wrap:wrap;gap:8px 18px;font-size:12.5px;color:var(--ink2);margin:0 0 10px}
.matrixLegendItem{display:inline-flex;align-items:center;gap:7px;white-space:nowrap}
/* strategy-matrix-redesign: the legend's "best in category" sample used to
   be a plain colored swatch standing in for the cell's yellow fill; now
   that the real cell is just bold green text, the sample is that same
   text treatment ("5x") instead of an abstract box, so the legend and the
   grid literally match. */
.matrixLegendSwatch.matrixLegendTop{width:auto;height:auto;flex:none;color:var(--good-text);font-weight:800}

/* ---- Explore: landing area + grouped/collapsible blocks (Items #6, #10) ---- */
.exIntro{max-width:720px;margin-bottom:26px}
.exYourWrap{margin-bottom:30px}
.exYourWrap>h3{font-size:13px;font-weight:750;color:var(--ink2);text-transform:uppercase;letter-spacing:.03em;margin:0 0 12px}
.exYourBlock{margin-bottom:16px}
.exYourBlock:last-child{margin-bottom:0}
.exYourBlock h4{font-size:14.5px;font-weight:750;color:var(--ink);margin:0 0 8px}
/* Wallet quick-jump strip (Item #6, Brief #34) — navigation only, no detail;
   deliberately smaller/plainer than .chip so it doesn't read as "add" or
   "toggle" controls used elsewhere on these tabs. */
.exWalletQuick{margin-bottom:18px}
.exWalletQuick h4{font-size:13px;font-weight:750;color:var(--ink2);text-transform:uppercase;letter-spacing:.03em;margin:0 0 8px}
.exQuickRow{display:flex;flex-wrap:wrap;gap:8px}
.exQuickChip{display:inline-flex;align-items:center;gap:7px;border:1px solid var(--line);background:var(--bg-surface);border-radius:999px;padding:7px 14px 7px 8px;font-size:13.5px;font-weight:650;color:var(--ink);cursor:pointer;font-family:var(--font);min-height:44px}
.exQuickChip:hover{border-color:var(--brand);background:var(--bg2)}
/* Brief highlight after a quick-jump lands on a row, so it's obvious what
   just happened — an inset ring rather than a background swap so it reads
   clearly against both the plain row background and .exGroupBody's. */
.exJumpHighlight{box-shadow:0 0 0 3px var(--accent) inset;border-radius:10px;transition:box-shadow .3s}
.exGroups{display:flex;flex-direction:column;gap:10px}
.exGroup{background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);overflow:hidden}
.exGroup summary{list-style:none}
.exGroup summary::-webkit-details-marker{display:none}
.exGroupHead{display:flex;align-items:center;gap:12px;padding:15px 18px;cursor:pointer;font-weight:700;color:var(--ink);user-select:none}
.exGroupHead:hover{background:var(--bg2)}
/* Hamburger, not a chevron or "+" — once "+" means "add to wallet" elsewhere
   on the row (exCardAddHTML/exProgAddHTML), reusing it for expand/collapse
   here would collide with that meaning right next to it (Brief #32). A
   rotating chevron/triangle tested as unclear for "tap to reveal a list", so
   Brief #34 swapped it for a hamburger — a symbol people already read as
   "there's a list in here" — and dropped the rotation since spinning a
   hamburger 90° doesn't communicate anything; open state is instead shown by
   swapping to a "×"-style close mark. */
.exGroupToggle{width:22px;height:22px;border-radius:50%;background:var(--brand-soft);color:var(--brand-ink);display:grid;place-items:center;flex:none;font-size:13px;font-weight:800}
.exGroupToggle::before{content:"☰"}
.exGroup[open] .exGroupToggle::before{content:"×"}
.exGroupTitle{flex:1}
/* Wallet-held count on a still-collapsed group header (Item #2, Brief #34) —
   visible before the group is ever expanded, only rendered when >0. */
.exGroupWalletBadge{font-size:11.5px;font-weight:700;color:#fff;background:var(--good);border-radius:999px;padding:2px 9px;white-space:nowrap}
.exGroupCount{font-size:12.5px;font-weight:700;color:var(--ink2);background:var(--bg2);border-radius:999px;padding:2px 10px}
.exGroupBody{padding:0 18px;border-top:1px solid var(--line)}
/* Airlines-only flat/grouped toggle (Item #5, Brief #34) */
.exAirlinesToggleRow{margin-bottom:10px}
/* === REDESIGN-B PASS 3: Programs Precision restyle ===
   Direction B programs.html — calm hairline rows, tabular numerals, and
   the typical-value bar cell. Scoped to #view-explore so the generic .row
   used elsewhere (modals, updates, marketing wallet card) is untouched. */
.valBarCell{display:inline-flex;align-items:center;gap:10px;justify-content:flex-end;vertical-align:middle}
.valBar{width:110px;height:6px;border-radius:99px;background:var(--bg2);overflow:hidden;flex:none;border:1px solid var(--line)}
.valBar i{display:block;height:100%;border-radius:99px;background:var(--accent)}
.valBar i.hi{background:var(--good-text)}
.valBarCell b{font-weight:750;font-size:13.5px;color:var(--ink);white-space:nowrap}
@media(max-width:640px){.valBar{width:56px}}
/* calm rows: tighter vertical rhythm + hairline separators only */
#view-explore .row{padding:11px 4px;gap:12px;--logoSlot:34px}
#view-explore .row .ico{font-size:11px}
/* the +Add / ✓ In wallet control sits inline at the row's right edge on
   desktop instead of opening a second line under every row */
@media(min-width:760px){
  #view-explore .row.hasExtra{flex-wrap:nowrap}
  #view-explore .row .rowExtra{flex-basis:auto;margin-top:0;padding-left:0;order:5;flex:none}
}
/* === end PASS 3 Programs === */
.searchbar{width:100%;padding:12px 14px;border:2px solid var(--line-strong);border-radius:12px;font-size:16px;margin-bottom:8px;box-shadow:0 1px 3px rgba(20,32,43,.06);transition:border-color .15s,box-shadow .15s;font-family:var(--font);min-height:44px}
.searchbar:focus{outline:none;border-color:var(--brand);box-shadow:0 0 0 4px rgba(31,58,95,.12)}
.fieldLabel{display:block;font-size:13px;font-weight:750;color:var(--ink2);margin:0 0 8px;letter-spacing:.01em}
/* Prominent variant of .searchbar for input bars outside Explore (Item 5) —
   Explore's own #exSearch keeps the plain .searchbar look untouched. */
.searchbarLg{padding:16px 18px;font-size:16.5px;font-weight:600;border:2px solid var(--line-strong);border-radius:14px;box-shadow:0 2px 10px rgba(20,32,43,.08)}
.searchbarLg::placeholder{color:var(--ink2);opacity:.85}
.searchbarLg:focus{border-color:var(--brand);box-shadow:0 0 0 5px rgba(31,58,95,.14)}

/* your points / holdings */
.holdgrid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.hold{border:1px solid var(--line);border-radius:12px;padding:14px;background:var(--bg-surface)}
.hold label{font-size:13px;font-weight:700;display:block;margin-bottom:6px}
.hold input{width:100%;padding:9px 11px;border:1px solid var(--line);border-radius:9px;font-size:15px}
/* JOB3 (iOS focus-zoom, cont'd -- see the full "iOS focus zoom" comment at
   EOF): the sitewide guard there is a bare `input` selector, (0,0,1). This
   rule is `.hold input`, (0,1,1), which outranks it, so .hold input still
   computed 15px and still zoomed on focus. Matched at equal specificity
   here so it wins on source order instead. */
@media(max-width:899px){ .hold input{font-size:16px} }
.total{font-size:15px;margin-top:14px}
.total b{font-size:20px;color:var(--brand-ink)}

.scrim{display:none;position:fixed;inset:0;background:rgba(20,32,43,.5);z-index:50;align-items:center;justify-content:center;padding:20px}
.scrim.on{display:flex}
.modal{background:var(--bg-surface);border-radius:20px;max-width:460px;width:100%;padding:26px;box-shadow:0 20px 60px rgba(0,0,0,.3)}
.modal .x{float:right;cursor:pointer;color:var(--ink2);font-size:20px;border:0;background:none;
  min-width:44px;min-height:44px;display:inline-flex;align-items:center;justify-content:center}
.note{font-size:12.5px;color:var(--ink2);margin-top:10px}
.banner{background:var(--accent-soft);border:1px solid var(--accent);color:var(--accent-text);font-size:13px;padding:9px 14px;border-radius:10px;margin:14px auto 0;max-width:var(--maxw)}
/* Brief: sitewide "Beta" banner (formerly a full-width row under
   header.nav, see the retired .betaPill/.betaDot it replaces) is now a
   small pill next to the logo in .sidebarBrand instead -- same signal,
   without permanently eating a row of vertical space on every route.
   Sized to sit comfortably beside the 22px-tall .sidebarBrandImg;
   --accent-soft/--accent-text are the same gold-accent token pair used
   elsewhere for subtle emphasis (e.g. .ask-gold-*), already tuned per
   theme for readable contrast (6.2:1 light / 8.9:1 dark on their
   respective surfaces), so this reads clearly without competing with the
   logo or nav in either theme. */
.sidebarBetaPill{display:inline-flex;align-items:center;margin-left:8px;padding:2px 8px;border-radius:999px;background:var(--accent-soft);color:var(--accent-text);border:1px solid var(--line);font-size:10px;font-weight:750;letter-spacing:.04em;text-transform:uppercase;line-height:1.5;flex:none}
@media(max-width:660px){.holdgrid{grid-template-columns:1fr}.hubgrid{grid-template-columns:1fr}}

/* wizard (Your points) */
/* ONBOARD-V3: the classic wizard's stepper (.stepper/.stepPill/.stepArrow/
   .wizStepperRow/.wizTopNavBtn), the sticky stepper+hero stack
   (.wizStickyStack) and the big mid-screen .wizHero card are all GONE with
   the classic form. Their replacement is the wallet-progress TRAY below:
   one slim bar pinned to the very top of the scroll region
   (position:sticky; top:0 inside #viewsScroll — the site header lives
   outside that scroller, so top:0 is the true top of the content area),
   collapsed by default, expandable via its chevron. Small enough that the
   current step's heading, input, and primary action all stay on screen at
   390x844. */
/* THEMING-CONTRAST (Jeremy: "users can't tell their action registered"):
   the "2 cards" progress readout is the ONLY feedback that adding a card
   worked, but it rendered at 12.5px/750 on a washed-out gradient behind a
   hairline 10%-alpha border — easy to miss entirely. Now: a stronger
   border (--line-strong, 2x the alpha), 14px text, and the live COUNT
   NUMBERS themselves in accent gold-bronze (--accent-text: 6.2:1 light /
   8.9:1 dark) + 850 weight so the "2" visibly ticks up when a card lands.
   The ≈$ value moves from --good (only ~2.9:1 on the dark bar) to its
   per-theme --good-text variant (5.3:1 light / 8.1:1 dark) — that token
   exists for exactly this fill-vs-text split. */
.wizTray{position:sticky;top:var(--topnav-h);z-index:16;margin:0 0 16px;background:var(--bg-surface);border:1px solid var(--line-strong);border-radius:12px;box-shadow:0 4px 14px rgba(20,32,43,.1)}
.wizTrayBar{display:flex;align-items:center;gap:10px;width:100%;min-height:44px;padding:5px 12px;background:linear-gradient(135deg,var(--brand-soft),var(--bg-surface));border:0;border-radius:11px;cursor:pointer;font-family:var(--font);text-align:left}
.wizTray.isOpen .wizTrayBar{border-radius:11px 11px 0 0}
.wizTrayCounts{flex:1;min-width:0;font-size:14px;font-weight:800;color:var(--text-1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.wizTrayCounts span:not([hidden]){color:var(--accent-text);font-weight:850}
.wizTrayVal{flex:none;font-size:14.5px;font-weight:800;color:var(--good-text);white-space:nowrap}
.wizTrayChevron{flex:none;width:16px;height:16px;color:var(--ink2);transition:transform .18s}
.wizTray.isOpen .wizTrayChevron{transform:rotate(180deg)}
.wizTrayDetail{display:flex;flex-direction:column;gap:6px;padding:8px 12px 10px;border-top:1px solid var(--line)}
/* display:flex above would silently defeat the [hidden] attribute the
   collapse toggle relies on — re-assert it. */
.wizTrayDetail[hidden]{display:none}
.wizTrayPts{font-size:12.5px;color:var(--ink2);font-weight:650}
.wizTrayLogos{display:flex;gap:5px;flex-wrap:wrap}
.wizTrayLogos:empty{display:none}
.wizTrayLogos .plogo img,.wizTrayLogos .plogo .mono{width:20px;height:20px}
@media(prefers-reduced-motion:reduce){ .wizTrayChevron{transition:none} }
/* The intake's step-change scroll target (scrollChatActiveIntoView) must
   land BELOW the pinned tray, not underneath it. */
.chatOnboard .chatBubble{scroll-margin-top:64px}
/* ONBOARD-V3 (Jeremy): the Ask-AI dock is hidden while the first-run
   intake is on screen — it was eating the bottom of the phone screen
   mid-setup. #dock stays in the DOM (see renderWizard in router.js and the
   documented onboarding exception in verify-ws-dock.mjs). */
body.ppOnboarding .dock{display:none}
.issuerGroup{margin-bottom:18px}
.issuerGroup h4{font-size:13px;font-weight:750;color:var(--ink2);margin:0 0 8px;text-transform:uppercase;letter-spacing:.03em}
.chipgrid{display:flex;flex-wrap:wrap;gap:9px}
.chip{display:inline-flex;align-items:center;gap:7px;border:1px solid var(--line);background:var(--bg-surface);border-radius:999px;padding:8px 15px 8px 8px;font-size:13.5px;font-weight:650;cursor:pointer;color:var(--ink);font-family:var(--font);box-shadow:0 1px 2px rgba(20,32,43,.05);transition:border-color .12s,box-shadow .12s,transform .08s,background .12s}
.chip:not(.on) .plogo{background:transparent}
.chip:hover{border-color:var(--brand);box-shadow:0 3px 10px rgba(20,32,43,.1);transform:translateY(-1px)}
.chip.on{background:var(--brand);border-color:var(--brand);color:#fff;box-shadow:0 3px 10px rgba(31,58,95,.28)}
.chip .plogo{margin-left:1px}

/* ---- Alliance partner grid (Brief #45): grid-template-columns with 1fr
   tracks makes every column the same width regardless of a chip's own
   content, and the fixed height + 2-line clamp keeps every row the same
   height too — the uniform-cell constraint Brief #43's matrix column
   missed for wallet-card badges. */
.allyPartnerGrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(92px,1fr));gap:9px;margin:10px 0}
.allyChip{box-sizing:border-box;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;height:82px;padding:8px 6px;border:1px solid var(--line);border-radius:12px;background:var(--bg-surface);font-family:var(--font);cursor:pointer;box-shadow:0 1px 2px rgba(20,32,43,.05);transition:border-color .12s,box-shadow .12s,transform .08s}
.allyChip:hover{border-color:var(--brand);box-shadow:0 3px 10px rgba(20,32,43,.1);transform:translateY(-1px)}
.allyChip .plogo{margin:0}
.allyChipName{width:100%;font-size:12px;font-weight:650;color:var(--ink);text-align:center;line-height:1.25;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.allyPartnerNote{font-size:12.5px;margin-top:8px}

/* ---- Points-entry fields: a distinct, tinted, unmissable "enter your points" panel ---- */
.inlineBalWrap{display:flex;flex-direction:column;gap:10px;margin-top:12px}
.inlineBalRow{display:flex;align-items:center;gap:12px;background:linear-gradient(135deg,var(--accent-soft),var(--bg-surface));border:1.5px solid var(--accent);border-radius:14px;padding:11px 14px;flex-wrap:wrap;box-shadow:0 2px 6px rgba(192,138,30,.08)}
.inlineBalLead{display:flex;align-items:center;gap:10px;flex:1;min-width:160px}
/* Card-cell consistency (2026-07-22): fixed 28px logo slot, name shrinks
   cleanly instead of pushing the input off the row. */
.inlineBalLead .plogo{flex:0 0 28px;width:28px;justify-content:center}
.inlineBalLabel{font-weight:700;font-size:13.5px;color:var(--ink);min-width:0;overflow-wrap:break-word;word-break:normal}
.inlineBalField{display:flex;align-items:center;gap:8px}
.balInput,.inlineBalRow input{width:132px;max-width:100%;padding:11px 12px;border:1.5px solid var(--line);border-radius:10px;font-size:15px;font-weight:650;font-family:var(--font);background:var(--bg-surface);box-shadow:0 1px 3px rgba(20,32,43,.06);transition:border-color .15s,box-shadow .15s;min-height:44px}
.balInput:focus,.inlineBalRow input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 4px rgba(192,138,30,.18)}
/* JOB3 (iOS focus-zoom, cont'd -- see EOF): the sitewide guard already
   covers plain `.balInput` (it's in the EOF selector list explicitly), but
   `.inlineBalRow input` is (0,1,1), one class+type step above the guard's
   bare `input` (0,0,1), so that half of this shared-declaration rule kept
   winning and kept zooming. Matched here, same specificity. */
@media(max-width:899px){ .inlineBalRow input{font-size:16px} }
.inlineBalUnit{font-size:12.5px;color:var(--ink2);font-weight:600;padding:0 2px;pointer-events:none}
.balHelperText{font-size:12.5px;margin:8px 0 0}

/* ONBOARD-V3: the classic wizard's selected-programs panel styles
   (.wizSelectedPanel/.wizSelectedTitle/.wizPrefHelp/.wizAdded/.wizAddedRow/
   .prefchk) are gone with the classic form. .wizRemoveBtn stays — the hub's
   bank-card chips still render it. */
.wizRemoveBtn{border:0;background:none;color:var(--ink2);font-size:19px;line-height:1;cursor:pointer;padding:6px 8px;border-radius:8px;min-height:32px}
.wizRemoveBtn:hover{background:var(--line);color:var(--ink)}
/* ONBOARD-V3: .wiznav (the classic form's bottom Back/Next row) is gone. */
.hubgrid{display:grid;grid-template-columns:repeat(2,1fr);gap:16px;margin-top:16px}
.hubcard{background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--radius);padding:20px;cursor:pointer;text-align:left;font-family:var(--font);display:block;width:100%}
.hubcard:hover{border-color:var(--brand);box-shadow:var(--shadow);transform:translateY(-2px)}
.hubcard.soon{cursor:default;opacity:.55}
.hubcard.soon:hover{border-color:var(--line);box-shadow:none;transform:none}
/* Mobile round 2 (c) fix: the shared @media(max-width:660px) block up at
   ~line 640 (.holdgrid/.hubgrid{grid-template-columns:1fr}) was written
   back when .hubgrid's own base rule lived ABOVE it in the file. Since
   then .hubgrid's base rule (repeat(2,1fr), directly above) moved/landed
   AFTER that media query, so at equal specificity the later, unconditional
   2-column rule always won the cascade -- the mobile override never
   actually applied. Five .hubcard tiles were rendering as a cramped
   2-column grid (with the fifth stranded alone next to empty space) at
   EVERY width, including 390px, instead of stacking single-column on
   mobile. Redeclared here, after the base rule, so it wins again -- desktop
   (>660px) is untouched, this only fires under the same 660px breakpoint
   the rule always intended. */
@media(max-width:660px){.hubgrid{grid-template-columns:1fr}}

/* ---- Expanded hub summary (Your points -> "what do you want to do") ---- */
.hubSummary{background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:18px 20px;margin:18px 0}
.hubSummaryHead{display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:6px 14px;margin-bottom:6px;border-bottom:1px solid var(--line);padding-bottom:12px}
/* WS-METRIC (backlog #102): points<->$ display toggle for the wallet
   summary. A segmented pill matching the existing filter-toggle idiom
   (see .updatesFilterToggle usage in wallet.js) rather than a plain
   checkbox switch, so a glance shows both options and which is active.
   44px min-height keeps it clear of the #138 tap-target floor; on narrow
   phones the pill grows to fill the row rather than the label crowding it. */
.walletMetricRow{display:flex;align-items:center;gap:10px;margin:2px 0 14px;flex-wrap:wrap}
.walletMetricLabel{font-size:13px;font-weight:650}
.walletMetricToggle{display:inline-flex;border:1.5px solid var(--line-strong);border-radius:999px;padding:3px;background:var(--bg-surface-2);gap:2px}
.walletMetricBtn{display:inline-flex;align-items:center;justify-content:center;border:0;background:transparent;color:var(--ink2);font-family:var(--font);font-size:13.5px;font-weight:700;padding:0 18px;min-height:44px;border-radius:999px;cursor:pointer;transition:background .15s,color .15s}
.walletMetricBtn.isOn{background:var(--brand);color:#fff}
.walletMetricBtn:not(.isOn):hover{color:var(--ink)}
@media(max-width:480px){.walletMetricRow{width:100%}.walletMetricToggle{flex:1}.walletMetricBtn{flex:1}}

/* Auto-sync onboarding card (Rec 4) — a calm, honest offer to connect
   accounts, mirroring the site's other wallet cards (same surface/border/
   radius/shadow idiom as .tierCard/.instantReadCard). Deliberately NOT a loud
   new style: a quiet accent-soft reassurance chip, one primary action, and a
   low-key "Maybe later" text button. Tokens carry both themes. */
.autoSyncCard{background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--radius);
  padding:18px 20px;box-shadow:var(--shadow)}
.autoSyncHead{margin:0 0 6px;font-size:16px;font-weight:700;color:var(--text-1);line-height:1.35}
.autoSyncBody{margin:0 0 12px;font-size:13.5px;color:var(--text-2);line-height:1.5;max-width:520px}
.autoSyncReassure{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:650;
  color:var(--accent-text);background:var(--accent-soft);border-radius:999px;padding:5px 12px;margin:0 0 14px}
.autoSyncActions{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.autoSyncLater{border:none;background:none;color:var(--text-3);font:600 13.5px var(--font);cursor:pointer;
  padding:8px 4px;min-height:44px;transition:color .12s}
.autoSyncLater:hover{color:var(--text-1)}
@media(max-width:480px){.autoSyncCard{padding:16px}.autoSyncActions{gap:10px}}
.hubGrandLine{font-size:14.5px;color:var(--ink2);font-weight:650;white-space:nowrap}
.hubGrandLine b{font-size:18px;color:var(--brand-ink)}
.hubBucket{margin-top:16px}
.hubBucket:first-of-type{margin-top:14px}
.hubBucketHead{display:flex;align-items:center;gap:10px;margin-bottom:8px;flex-wrap:wrap}
.hubBucketHead h4{margin:0;font-size:13px;font-weight:750;color:var(--ink2);text-transform:uppercase;letter-spacing:.03em;flex:1}
.hubBucketSub{font-size:14px;font-weight:750;color:var(--brand-ink)}
.hubEmpty{font-size:13.5px;padding:4px 0 2px}
/* REMOVED (mobile audit P0 #3, 2026-07-19): .hubRow{display:flex;...} was a
   leftover from the pre-Brief-#56 div-based wallet hub rows, dead ever
   since that redesign moved to a real <table> — except its class name got
   reused on the new <tr class="hubRow"> body rows (hubBucketRowsHTML,
   wallet.js), which was silently catastrophic: an author `display:flex`
   on a <tr> blockifies its <td> children per the flex spec, so every data
   row rendered as a stack of full-width blocks instead of table cells —
   nothing lined up under the PROGRAM/BALANCE/VALUE headers — and because
   the browser's column-width algorithm was then only informed by the one
   real table-row left (<thead>), a real column (BALANCE) could end up
   sized far narrower than its own header text on a narrow phone, clipping
   it (the audit's "BALA" truncation). Fix was two-part: the <tr>s now use
   a non-colliding class ("hubTableRow", which needs no rule of its own —
   plain <tr>/<td> table layout is exactly what's wanted here), and this
   now-fully-unused rule (confirmed no remaining class="hubRow" anywhere in
   the app) is deleted outright rather than left as a dangling trap for the
   next refactor to collide with again. */
.hubRowVal{font-weight:750;color:var(--brand-ink);font-size:14px;min-width:58px;text-align:right}
/* BUGFIX (Hyatt-$0): an unresolved valuation renders as a muted "—" (not a
   receipt-trigger button, since there's no AWVALS source to show) rather
   than a same-weight/same-color "$0" that reads as a real, verified value. */
.hubRowValUnknown{font-weight:600;color:var(--ink2);cursor:help;display:inline-block}
.hubBalText{font-size:14.5px;font-weight:650;color:var(--ink);white-space:nowrap}
/* Balance + $ value travel together as one flex item so they can only wrap
   as a unit, never split onto separate lines (Brief #42, view mode only). */
.hubRowMeta{display:flex;align-items:center;gap:8px;flex:none;margin-left:auto}
.inlineBalField.sm .balInput{width:100px;padding:8px 10px;font-size:13.5px;min-height:38px}
/* JOB3 (iOS focus-zoom, cont'd -- see EOF): three classes deep, (0,3,0),
   easily clearing the sitewide guard's bare `input` (0,0,1) -- computed
   13.5px and zoomed on focus. Matched at (0,3,0) here so it wins on source
   order. `.wCell--bal .inlineBalField.sm .balInput` (line ~1205, (0,4,0))
   doesn't set its own font-size, so it inherits this fix too. */
@media(max-width:899px){ .inlineBalField.sm .balInput{font-size:16px} }
/* Per-card removal chips under a bank row in hub edit mode (Brief #34) — a
   currency row can back more than one held card, so removal happens per
   chip, not per row. .hubRemoveBtn overrides .wizRemoveBtn's 32px min-height
   to clear the 44px touch-target bar used everywhere else in edit mode. */
.hubCardChips{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
.hubCardChip{display:inline-flex;align-items:center;gap:2px;background:var(--bg2);border:1px solid var(--line);border-radius:999px;padding:4px 4px 4px 12px;font-size:13px;font-weight:600}
.hubRemoveBtn{min-height:44px;min-width:44px}
/* Grouped "add another" triggers (Brief #30) — one row instead of trailing each bucket */
.hubAddGroup{display:flex;gap:10px;flex-wrap:wrap;margin:2px 0 16px}
.hubAddBtn{margin-top:0;min-height:44px}
/* Per-row "+ Add" pills in the wizard/hub/Explore add flows (Brief #43) — set
   unconditionally rather than relying on @media(pointer:coarse), which Brief
   #42 found some mobile browsers/emulators don't reliably match. */
.addRowBtn{min-height:44px}
/* Inline "add another card/airline/hotel" panel on the hub (Item 3) */
.hubAddPanel{margin-top:10px;padding:14px;border:1.5px solid var(--line);border-radius:12px;background:var(--bg2)}
.hubAddSelect{width:100%;padding:12px 14px;border:1.5px solid var(--line);border-radius:10px;font-size:15px;font-family:var(--font);background:var(--bg-surface);min-height:44px}
.hubAddSelect:focus{outline:none;border-color:var(--brand);box-shadow:0 0 0 4px rgba(31,58,95,.12)}
.hubAddResults{max-height:260px;overflow:auto}
@media(max-width:430px){
  .hubRowMeta{flex-basis:100%;justify-content:flex-end;margin-left:0}
  .hubGrandLine{white-space:normal}
}

/* ---- Finish celebration: count-up totals (kept) + a modern glow-burst,
   confetti burst, and shimmer-text reveal, shown briefly between the wizard
   and the hub. Purely decorative — no data dependency, so it can't break
   with an empty profile — and entirely skipped, never rendered, when the
   user prefers reduced motion (see prefersReducedMotion() in app.js). ---- */
.finishAnim{position:relative;text-align:center;padding:30px 10px 6px;overflow:hidden}
.finishBurst{position:relative;height:130px;display:flex;align-items:center;justify-content:center;margin-bottom:2px}
.burstGlow{position:absolute;width:150px;height:150px;border-radius:50%;background:radial-gradient(circle,rgba(192,138,30,.4),rgba(31,58,95,.22) 55%,transparent 74%);animation:burstPulse 1.6s ease-out forwards}
.burstRing{position:absolute;width:64px;height:64px;border-radius:50%;border:2px solid var(--accent);opacity:0;animation:ringPulse 1.5s ease-out forwards}
.burstRing.r2{border-color:var(--brand);animation-delay:.16s}
.burstRing.r3{border-color:var(--step);animation-delay:.32s}
@keyframes burstPulse{0%{transform:scale(.25);opacity:0}30%{opacity:1}100%{transform:scale(1.25);opacity:0}}
@keyframes ringPulse{0%{transform:scale(.4);opacity:0}22%{opacity:.85}100%{transform:scale(3.4);opacity:0}}
.confettiWrap{position:absolute;inset:0;pointer-events:none;overflow:hidden}
.confetti{position:absolute;top:-12px;width:7px;height:13px;border-radius:2px;opacity:0;animation:confettiFall 1.4s ease-in forwards}
@keyframes confettiFall{0%{transform:translateY(0) rotate(0deg);opacity:0}10%{opacity:1}100%{transform:translateY(150px) rotate(320deg);opacity:0}}
.confetti.c1{left:6%;background:var(--accent);animation-delay:.02s}
.confetti.c2{left:16%;background:var(--brand);animation-delay:.2s}
.confetti.c3{left:26%;background:var(--step);animation-delay:.08s}
.confetti.c4{left:36%;background:var(--accent);animation-delay:.32s}
.confetti.c5{left:46%;background:var(--brand);animation-delay:.14s}
.confetti.c6{left:56%;background:var(--step);animation-delay:.4s}
.confetti.c7{left:66%;background:var(--accent);animation-delay:.06s}
.confetti.c8{left:76%;background:var(--brand);animation-delay:.26s}
.confetti.c9{left:86%;background:var(--step);animation-delay:.18s}
.confetti.c10{left:94%;background:var(--brand);animation-delay:.36s}
.confetti.c11{left:44%;background:var(--step);animation-delay:.46s}
.confetti.c12{left:20%;background:var(--accent);animation-delay:.44s}
.finishHeadline{margin:0 0 18px;position:relative;display:inline-block;color:var(--brand-ink);background:linear-gradient(90deg,var(--brand-ink) 0%,var(--accent) 45%,var(--step) 55%,var(--brand-ink) 100%);background-size:220% auto;-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;animation:shimmerText 1.7s ease-in-out forwards}
@supports not (background-clip:text){.finishHeadline{background:none;-webkit-text-fill-color:var(--brand-ink);color:var(--brand-ink)}}
@keyframes shimmerText{0%{background-position:220% 0}100%{background-position:0 0}}
.finishTotals{display:flex;justify-content:center;gap:22px;flex-wrap:wrap;margin-bottom:4px}
.finishTotal{display:flex;flex-direction:column;align-items:center;min-width:88px;opacity:0;animation:totalPop .5s ease-out forwards}
.finishTotal:nth-child(2){animation-delay:.14s}
.finishTotal:nth-child(3){animation-delay:.28s}
@keyframes totalPop{from{opacity:0;transform:translateY(10px) scale(.92)}to{opacity:1;transform:translateY(0) scale(1)}}
.finishTotalNum{font-size:26px;font-weight:800;color:var(--step);text-shadow:0 0 16px rgba(15,138,106,.32)}
.finishTotalLbl{font-size:12px;color:var(--ink2);font-weight:650;margin-top:2px;text-align:center}
@media(max-width:430px){
  .finishBurst{height:104px}
  .finishTotals{gap:12px}
  .finishTotal{min-width:74px}
  .finishTotalNum{font-size:20px}
}
@media(prefers-reduced-motion:reduce){
  .finishAnim *{animation:none!important}
}

/* ---- Brand logos (ported from points-compass) ---- */
.plogo{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center}
.plogo img{width:22px;height:22px;object-fit:contain;background:#fff;padding:2px;border-radius:5px;box-sizing:border-box;display:block}
.plogo .mono{width:22px;height:22px;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:800;letter-spacing:.02em;line-height:1;color:#fff;background:var(--mc,#888);border-radius:5px}
.plogo .mono[hidden]{display:none}
.plogo.lg img,.plogo.lg .mono{width:28px;height:28px}
.plogo.lg .mono{font-size:10.5px}
/* Real logos occupy the same fixed slot as the .ico monogram tile, centered,
   so plogo-rows and ico-rows share one name-column x. (Replaces the old
   margin-right:2px nudge, which made the name start drift with logo size.) */
.row .plogo{flex:0 0 var(--logoSlot,40px);width:var(--logoSlot,40px);justify-content:center}

/* ---- Canonical card cell (cross-surface, 2026-07-22) ----
   ONE pattern for every "brand mark + name (+ value)" line on the site:
   [fixed logo slot][name, shrinkable][value, hard-right]. The logo slot is a
   constant-width column (--logoSlot, default 28px) with the mark centered in
   it, so the NAME column starts at the same x in every row of a list even when
   marks are mixed sizes. The name lives in a min-width:0 item (the classic
   flex-refuses-to-shrink fix) and either truncates on one line with an
   ellipsis (.cardName--1ln, for rows that keep a value on the same line) or
   wraps to at most two clean word-boundary lines (.cardName--2ln, for stacked
   cells) — it can never push the layout or overflow. The value (.cardVal)
   never wraps, sits hard-right via margin-left:auto, and uses tabular
   numerals so figures align down the column. Tokens only; light/dark safe. */
.cardCell{display:flex;align-items:center;gap:10px;min-width:0}
.cardCell .plogo{flex:0 0 var(--logoSlot,28px);width:var(--logoSlot,28px);justify-content:center}
.cardCell .ico{flex:0 0 var(--logoSlot,28px);width:var(--logoSlot,28px);height:var(--logoSlot,28px);border-radius:7px;background:var(--bg2);display:grid;place-items:center;font-weight:800;color:var(--brand-ink);font-size:10px}
.cardName{min-width:0;flex:1 1 auto}
.cardName--1ln{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cardName--2ln{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;overflow-wrap:break-word;word-break:normal;hyphens:none}
.cardVal{flex:none;margin-left:auto;text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}

/* mobile nav: hamburger + slide-down panel */
.menuBtn{display:none;width:44px;height:44px;border:1px solid var(--line);border-radius:10px;background:var(--bg-surface);cursor:pointer;flex:none;padding:0;align-items:center;justify-content:center;gap:4px;flex-direction:column}
.menuBtn span{display:block;width:20px;height:2px;background:var(--ink);border-radius:2px;transition:transform .15s,opacity .15s}
.menuBtn[aria-expanded="true"] span:nth-child(1){transform:translateY(6px) rotate(45deg)}
.menuBtn[aria-expanded="true"] span:nth-child(2){opacity:0}
.menuBtn[aria-expanded="true"] span:nth-child(3){transform:translateY(-6px) rotate(-45deg)}
.menuScrim{display:none;position:fixed;inset:0;background:rgba(20,32,43,.35);z-index:19}
.menuScrim.on{display:block}
.mobileMenu{display:none;flex-direction:column;position:absolute;top:100%;left:0;right:0;background:var(--bg-surface);border-bottom:1px solid var(--line);box-shadow:var(--shadow);z-index:21;padding:8px 20px 18px}
.mobileMenu.on{display:flex}
.mobileMenu a{padding:12px 6px;font-size:16px;font-weight:650;color:var(--ink);text-decoration:none;border-bottom:1px solid var(--line);min-height:44px;display:flex;align-items:center}
.mobileMenu a:not(.btn):last-of-type{border-bottom:0}
.mobileMenu a.active{color:var(--brand-ink)}
.mobileMenu a.btn{border-bottom:0;margin-bottom:0}

@media(max-width:780px){
  nav.links{display:none}
  .menuBtn{display:inline-flex;margin-left:auto}
  .nav .wrap{padding-right:4px}
}
/* Mobile audit (2026-07-19): this whole block used to be gated on
   `pointer:coarse` alone. That's correct for a real touch device, but the
   audit's headless-Chromium pass (plain viewport resize to 390x844/430x932,
   no touch/mobile device emulation) reports `pointer:fine` regardless of
   viewport width -- so every one of these 44px floors silently never
   applied during that audit, and the live findings (24x25 modal ×, 130x16
   matrix sort pills, etc) are exactly what's left when this block doesn't
   fire. Narrow viewport width is at least as reliable a "this is a phone"
   signal as pointer type for a site with no separate desktop layout past
   this point, so the same rules now also apply under max-width:768px
   regardless of how the pointer reports -- belt and suspenders, matches
   the width-based breakpoints already used elsewhere in this file
   (e.g. @media(max-width:780px) just above). */
@media(pointer:coarse),(max-width:768px){
  nav.links a{min-height:44px;display:inline-flex;align-items:center}
  .mobileMenu a{min-height:44px}
  .starter{min-height:44px;display:inline-flex;align-items:center}
  .tab{min-height:44px;display:inline-flex;align-items:center}
  .chip2,.chip2browse,.replay{min-height:44px;display:inline-flex;align-items:center}
  .btn{min-height:44px}
  .btn.sm{min-height:44px}
  .x{min-width:44px;min-height:44px;display:inline-flex;align-items:center;justify-content:center}
  .prefchk{min-height:44px;padding:4px 2px}
  .matrixSortBtn{min-height:44px;display:flex;align-items:center;justify-content:center}
  .matrixTable th.matrixCardCol .matrixSortBtn{justify-content:flex-start}
  .stepPill{min-height:44px}
  /* PP-100 A13 (Codex M2 / Fable F18): name-map nav + source/CSV/JSON links.
     .x (28x28 closes) and .btn.sm (Got-it) already floor to 44px above via
     the .btn base's inline-flex + this block's min-height -- verified, not
     re-declared. These three are new: .nmJump a is a standalone per-line nav
     link (safe to inline-flex like nav.links a above). .nmMeta a and
     #nmDownloads a sit inline inside running prose (citation/source text,
     CSV+JSON download sentence) where forcing min-height would distort line
     height, so they get an invisible expanded hit area instead -- enlarges
     the tappable region without changing visible layout. */
  .nmJump a{min-height:44px;display:inline-flex;align-items:center}
  .nmMeta a,#nmDownloads a{position:relative}
  .nmMeta a::after,#nmDownloads a::after{content:"";position:absolute;top:-12px;bottom:-12px;left:-6px;right:-6px}
}

/* interactive explore rows + detail modal */
.row.rclick{cursor:pointer;border-radius:10px;transition:background .12s}
.row.rclick:hover{background:var(--bg2)}
.row .val{color:var(--brand-ink)}
#detailBody h3{margin-bottom:2px}
.kv{display:flex;justify-content:space-between;gap:14px;padding:8px 0;border-bottom:1px solid var(--line);font-size:14px}
.kv:last-child{border-bottom:0}
.kv span{color:var(--ink2)}
.kv b{text-align:right}
#detailScrim .modal{max-height:82vh;overflow:auto}

/* brand logo */
.logoImg{height:28px;width:auto;display:block}
.logo{gap:0}
@media(max-width:400px){ .logoImg{height:24px} }

/* status-tier benefits */
.statusProgHead{display:flex;align-items:center;gap:14px;margin:14px 0 20px;flex-wrap:wrap}
.tierCard{background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--radius);padding:16px 18px;margin-bottom:14px;box-shadow:var(--shadow)}
.tierReq{font-size:13px;color:var(--ink2);margin:2px 0 10px;font-weight:600}
.tierPerks{margin:0;padding-left:20px;font-size:14.5px;line-height:1.6}
.tierPerks li{margin:3px 0}
.tierGain{margin-top:12px;padding:10px 14px;border-radius:10px;background:var(--brand-soft);border-left:3px solid var(--step)}
.tierGainLabel{font-size:12px;font-weight:750;text-transform:uppercase;letter-spacing:.03em;color:var(--brand-ink)}
.tierGainList{margin:6px 0 0;padding-left:18px;font-size:13.5px;line-height:1.55}
.tierGainList li{margin:2px 0}
@media(max-width:430px){ .tierCard{padding:14px} }

/* === WS-B onboarding === */
/* Conversational "AI interview" intake for #view-points (wizWrap). Chip/
   search/balance-input styling itself is inherited from the existing .chip,
   .searchbar, .inlineBalRow etc rules above — only new, onboarding-specific
   layout lives here. ONBOARD-V3: the chat/classic mode-switch bar
   (.chatModeBar/.chatModeLabel/.classicModeBar) is gone with the classic
   wizard. */
.chatOnboard{display:flex;flex-direction:column}
.chatBubble{display:flex;align-items:flex-start;gap:10px;margin:0 0 16px;max-width:100%}
.chatAvatar{flex:none;width:32px;height:32px;border-radius:999px;background:var(--brand);color:#fff;display:flex;align-items:center;justify-content:center;font-size:15px;box-shadow:var(--shadow)}
.chatBubble.assistant .chatBubbleText{background:var(--bg2);border:1px solid var(--line);border-radius:4px 16px 16px 16px;padding:12px 16px;font-weight:650;max-width:min(560px,100%)}
.chatBubble.user{justify-content:flex-end;flex-wrap:wrap;padding-left:42px}
.chatBubble.user .chatBubbleText{background:var(--brand-soft);border:1px solid var(--line);border-radius:16px 4px 16px 16px;padding:10px 14px;font-size:14px;color:var(--ink2);max-width:min(480px,100%)}
.chatEditLink{display:block;margin:-10px 0 16px auto}
.linkish{border:0;background:none;padding:4px 2px;color:var(--brand-ink);font-weight:700;font-size:13px;cursor:pointer;text-decoration:underline;font-family:var(--font);min-height:44px}
.chatStepBody{margin:0 0 22px 42px;max-width:640px}
.chatSubLabel{margin:14px 0 6px;font-size:14px}
.chatStepNav{display:flex;justify-content:space-between;gap:10px;margin-top:14px;flex-wrap:wrap}
.chatStepNav .btn{min-height:44px}
/* ONBOARD-CUE (Jeremy 2026-07-20, "the continue button requires you to
   scroll down in the onboarding and find it... maybe we can tell them to
   go down to hit the continue button"): a floating "Scroll down to
   continue ↓" pill, fixed to the bottom of the screen (free space — the
   Ask-AI dock is hidden during onboarding). wireChatContinueCue
   (router.js) toggles .on from an IntersectionObserver on the active
   step's primary button, so the pill exists ONLY while Continue is
   off-screen and vanishes the moment it scrolls into view — zero clutter
   whenever Continue is already findable. visibility:hidden (not
   display:none) so the fade can run; pointer-events:none while hidden so
   it can never intercept a tap it isn't visually making. z-index 30: over
   page content, under every scrim (50). */
#chatScrollCue{position:fixed;left:50%;bottom:calc(16px + env(safe-area-inset-bottom,0px));z-index:30;
  transform:translateX(-50%) translateY(6px);display:inline-flex;align-items:center;gap:7px;
  /* THEMING-CONTRAST (Jeremy: "far too little contrast — users can't tell
     what to do next"): the pill used to be --bg-surface on --bg-surface
     result cards (~1.0:1 — literally invisible except for its shadow) in
     BOTH themes. It is now an INVERTED pill — brand navy with white text
     in light mode, warm-cream with near-black text in dark mode (see the
     [data-theme="dark"] override below) — so it reads as the single
     highest-contrast element on the screen, which is exactly what a
     "here's what to do next" affordance must be. */
  padding:10px 18px;border-radius:999px;border:1px solid rgba(255,255,255,.28);background:var(--brand);
  /* nowrap: a fixed element anchored at left:50% shrink-to-fits against
     the half-viewport and would wrap this short label onto two lines. */
  white-space:nowrap;
  color:#fff;font:inherit;font-family:var(--font);font-size:14px;font-weight:750;cursor:pointer;
  box-shadow:0 8px 24px rgba(20,32,43,.35);opacity:0;visibility:hidden;pointer-events:none;
  transition:opacity .22s ease,transform .22s ease,visibility 0s linear .22s}
:root[data-theme="dark"] #chatScrollCue{background:var(--text-1);color:#14202b;
  border-color:rgba(16,24,32,.25);box-shadow:0 8px 24px rgba(0,0,0,.55)}
#chatScrollCue.on{opacity:1;visibility:visible;pointer-events:auto;
  transform:translateX(-50%) translateY(0);transition:opacity .22s ease,transform .22s ease}
#chatScrollCue .chatScrollCueArr{font-weight:800;display:inline-block;animation:cueNudge 1.6s ease-in-out infinite}
@keyframes cueNudge{0%,100%{transform:translateY(-1px)}50%{transform:translateY(2px)}}
@media(prefers-reduced-motion:reduce){ #chatScrollCue,#chatScrollCue.on{transition:none;transform:translateX(-50%)}
  #chatScrollCue .chatScrollCueArr{animation:none} }
@media(max-width:520px){
  .chatBubble.user{padding-left:0}
  .chatStepBody{margin-left:0}
  .chatEditLink{margin-right:auto;margin-left:0}
}
/* Chip/searchbar tap targets inside the chat flow specifically — scoped to
   .chatOnboard so it never touches .chip/.searchbar usage elsewhere (Explore,
   the classic wizard, etc), which are owned outside this workstream. */
@media(max-width:768px){
  .chatOnboard .chip{min-height:44px;padding-top:10px;padding-bottom:10px}
  .chatOnboard .searchbar{min-height:44px}
}
/* === end WS-B === */

/* === WS-A ask-bar + assistant ===
   The top #askBar banner (persistent "Ask P&P AI" bar shown on every view
   except home) is retired by WS-DOCK (Fable chat-first spec v2 pass 2,
   Decision 4) -- the ONE composer is docked in #dock sitewide now (see
   near the end of this file), never a second ask surface above the fold. */

/* Ask AI page orientation copy (Brief #55) */
.aiExplainer{background:var(--bg2);border:1px solid var(--line);border-radius:var(--radius);padding:14px 18px;margin:14px 0 18px}
.aiExplainer p{margin:0 0 8px;font-size:14.5px;line-height:1.5}
.aiExplainer p:last-child{margin-bottom:0}
/* === end WS-A === */

/* === WS-58 delete controls === */
/* Brief #58 part 2: the three x delete controls (wizard "added program" row
   at wizSelectedPanel, hub bank-card chip at hubBankCardChipsHTML, hub
   air/hotel program row at hubRowsHTML) all share the .wizRemoveBtn /
   .hubRemoveBtn classes already, but the base .wizRemoveBtn rule above was
   only a 32px min-height (no min-width) while .hubRemoveBtn overrode it to
   44px -- so the plain wizard-row control (no .hubRemoveBtn class) landed
   below the project's 44px touch-target standard used everywhere else.
   Converge every use of .wizRemoveBtn on the same 44px square target, same
   contrast, same centered glyph -- no fourth style. Position consistency
   (trailing edge of its row/chip) is handled via markup order in app.js. */
.wizRemoveBtn{min-height:44px;min-width:44px;display:inline-flex;align-items:center;justify-content:center;border-radius:10px}
/* === end WS-58 === */

/* ===================== WS-MOBILE wallet (supersedes WS-56) =====================
   THE LAYOUT PRIMITIVE, and why it is not a grid on mobile.

   The previous attempt drove all three buckets from one rigid multi-column
   system at every width. Below ~430px the name column's track collapsed to
   near-zero, and because the name also carried overflow-wrap:break-word it
   dutifully wrapped ONE CHARACTER PER LINE; the fixed header row collapsed
   the same way and ran "Program" and "Balance" together into "PROGBALANCE".
   Both failures share one root cause: asking a fixed column system to lay
   out in a width it cannot fit.

   So mobile does not use a column system at all. A wallet row is a stacked
   CARD: the program name owns a full-width line of its own (~330px at
   390px), with balance and value as a labelled pair beneath it. There is no
   header strip on mobile either -- it is display:none, not merely narrow --
   so there is nothing that CAN collide. Every failure mode above is
   structurally impossible here rather than tuned around.

   At >=760px the same markup becomes a real aligned table via one shared
   grid track list (--wCols) applied identically to .wHead and .wRow in all
   three buckets, which is what actually makes Credit cards / Airlines /
   Hotels line up with each other. The name track carries a hard 190px floor
   (minmax(190px,1fr), never minmax(0,1fr)) so it cannot collapse there
   either, at any width where the grid is active. */
/* Balance track is 196px, not 168px: at 168px the Edit-mode input plus the
   inline "points"/"miles" unit left the field too narrow and it clipped its
   own value to "742,00". */
:root{ --wCols: minmax(190px,1fr) 196px 104px 48px; }

.wTable{width:100%}
.wRows{display:flex;flex-direction:column;gap:10px}
/* Column headers: desktop-only, by construction (see block comment). */
.wHead{display:none}

/* WS-JITTER (#119): contain:layout scopes this row's own layout/reflow to
   itself -- a row's internal content change (e.g. its balance field
   swapping between .hubBalTap and .balInput) can no longer trigger a
   layout recalculation of anything outside this box, which is one more
   guard against the wallet "jitter" alongside the box-match fix below and
   the targeted single-row DOM refresh in wallet.js. */
.wRowWrap{position:relative;overflow:hidden;border:1px solid var(--line);border-radius:12px;background:var(--bg-surface);contain:layout}
.wRowWrap--confirm{padding:12px 14px}

/* --- Mobile row: stacked card. Name spans the full width on its own line;
   balance/value sit beneath as a labelled pair; the action slot is ALWAYS
   present (44px) so revealing the delete in Edit mode never reflows the
   row. --- */
.wRow{
  position:relative;z-index:1;background:var(--bg-surface);
  /* Balance needs more of the line than Value ("742,000 points" vs
     "$14,840"), hence 1.25fr rather than an even split. */
  display:grid;grid-template-columns:1.25fr 1fr 44px;
  grid-template-areas:"name name name" "bal val act";
  /* stretch (not end) so the Balance and Value cells are the same height
     and their captions sit on one shared baseline. */
  gap:10px 12px;align-items:stretch;padding:12px 14px;
  transition:transform .22s cubic-bezier(.22,.61,.36,1);
  touch-action:pan-y;
}
.wRow.isDragging{transition:none}
.wCell{min-width:0}
.wCell--name{grid-area:name;display:flex;align-items:flex-start;gap:10px}
.wCell--bal{grid-area:bal;display:flex;flex-direction:column;gap:3px;align-items:flex-start}
.wCell--val{grid-area:val;display:flex;flex-direction:column;gap:3px;align-items:flex-end;text-align:right}
/* PASS 3: Typical-value column exists only at the desktop table breakpoint. */
.wCell--rate{display:none}
.wCell--act{grid-area:act;display:flex;align-items:center;justify-content:flex-end}

.wArt{display:inline-flex;align-items:center;gap:6px;flex:none;padding-top:1px}
/* Card-cell consistency (2026-07-22): the brand logo inside the row art sits
   in the same fixed 28px slot used site-wide (.cardCell), so 22px program
   marks and 28px bank marks start the name column at one x. */
.wArt .plogo{flex:0 0 28px;width:28px;justify-content:center}
.wNameBlock{flex:1 1 auto;min-width:0;display:block}
/* Names never wrap character-by-character: word-break stays `normal`
   (break-all is what shatters words) and overflow-wrap only breaks a word
   that genuinely cannot fit its own line. Card-cell consistency pass
   (2026-07-22) additionally caps the name at TWO clean lines (line-clamp) so
   a long name can't stack messily or stretch the row on narrow screens. */
.wName{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;
  font-weight:700;font-size:15px;line-height:1.35;color:var(--ink);
  overflow-wrap:break-word;word-break:normal;hyphens:none}
.wBadges{display:block;margin-top:4px}
/* One card name per line. An inline "A · B" list reads fine on desktop but
   wraps badly in a phone-width column -- the separator ends up orphaned at
   the start of the next line. A stacked list has no separator to orphan. */
.wRowSub{margin-top:4px;font-size:12.5px;color:var(--ink2);display:flex;flex-direction:column;gap:2px}
.wRowCardName{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;overflow-wrap:break-word;word-break:normal}
.wRowSub--noCard{font-style:italic}
.wRow .hubRowUpdated{margin-top:4px;font-size:12px}

/* Per-cell captions replace the hidden column headers on mobile. They live
   INSIDE their own cell, so unlike a shared header row they have no
   neighbour to collide with. */
.wCellLbl{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--ink2)}

/* Balance: read-only tap target and live input are pinned to the same box,
   so switching between them cannot change the row's height or width.
   max-width + flex-wrap are load-bearing, not cosmetic: .hubBalTap is an
   inline-flex whose intrinsic width ("742,000 points") exceeds its grid
   track on a phone, and without them it overflowed straight into the Value
   column and printed "742,000 points$14,840". */
.wCell--bal{position:relative}
.wCell--bal .hubBalTap,.wCell--bal .inlineBalField{width:100%;max-width:100%;min-width:0;min-height:44px;box-sizing:border-box}
.wCell--bal .inlineBalField{display:flex;align-items:center;gap:5px;flex-wrap:nowrap}
/* The input must SHRINK, not hold a fixed width: .inlineBalField.sm gives it
   width:100px and it sits in a flex row beside the unit and the "Saved ✓"
   tag, which together overran the Balance track in Edit mode and bled into
   Value. flex:1 1 auto + width:auto + min-width:0 lets it give way.
   WS-JITTER (#119): selector bumped from `.wCell--bal .balInput` to
   `.wCell--bal .inlineBalField.sm .balInput` (4 classes, was 2) so this
   min-height:44px actually WINS the cascade over .inlineBalField.sm
   .balInput's own min-height:38px (line ~798, 3 classes -- higher
   specificity than the old 2-class selector regardless of source order).
   Losing that fight meant .balInput rendered 6px shorter than its
   44px-tall .inlineBalField wrapper and got vertically centered inside it
   (align-items:center) -- a 3px top offset vs. the read-state .hubBalTap,
   which gets its own 44px min-height directly. */
.wCell--bal .inlineBalField.sm .balInput{flex:1 1 auto;width:auto;min-width:118px;min-height:44px;box-sizing:border-box}
.wCell--bal .inlineBalUnit{flex:none}
/* "Saved ✓" is a transient confirmation that is opacity:0 almost always —
   but it still reserved width in the flex row. Float it out of flow, into
   the free space beside the BALANCE caption. */
.wCell--bal .balSavedTag{position:absolute;top:0;right:2px;font-size:11px}
/* WS-JITTER (#119): border-box-identical to its .balInput counterpart
   (.inlineBalField.sm .balInput: 1.5px border, 8px/10px padding, 10px
   radius) -- was border:0 + padding-left:0, so the digits themselves sat
   ~11.5px further left than they did the instant the row flipped into
   Edit. A transparent border of the same width plus matching padding
   keeps the box (and the digits inside it) in exactly the same place
   whichever state is showing. */
.wCell--bal .hubBalTap{justify-content:flex-start;gap:5px;text-align:left;white-space:nowrap;
  border:1.5px solid transparent;padding:8px 10px;border-radius:10px}
/* Unit lives in the caption on mobile (see hubRowsHTML); it comes back
   inline at the table breakpoint, where there is room for it. */
.wCell--bal .inlineBalUnit{display:none}
.wCell--val,.wCell--bal{justify-content:flex-start}
/* .hubRowVal.receipt-trig sets width:100% globally (for the old table cell);
   here that stretched the dotted receipt underline across the whole cell,
   trailing off past the number. Shrink it back to the figure itself. */
.wRow .wCell--val>.hubRowVal,.wRow .wCell--val>.hubRowVal.receipt-trig{
  width:auto;max-width:100%;min-width:0;flex:0 0 auto;align-self:flex-end;text-align:right}
/* The ✎ hint next to a read-only balance is a desktop affordance: on mobile
   the swipe tray carries an explicit "Edit" button, and at this size the
   glyph reads as a stray artifact rather than a pencil. */
.wCell--bal .hubBalEditIcon{display:none}
@media(min-width:760px){ .wCell--bal .hubBalEditIcon{display:inline} }

/* One delete control per row. Always in the DOM and always occupying its
   slot; only its interactivity changes, so Edit mode never reflows a row. */
.wDelBtn{display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;flex:none;
  border:1.5px solid transparent;border-radius:10px;background:transparent;color:var(--ink2);cursor:pointer;
  transition:opacity .15s ease,color .15s ease,border-color .15s ease}
.wDelBtn svg{width:18px;height:18px}
.wDelBtn:disabled{opacity:0;pointer-events:none}
.wDelBtn.isOn{color:var(--bad-text,#b42318);border-color:var(--line)}
.wDelBtn.isOn:hover,.wDelBtn.isOn:focus-visible{background:rgba(180,35,24,.09);border-color:currentColor}

/* --- Swipe tray (mobile): sits behind the row, revealed by a right-to-left
   drag. See wireWalletSwipe in router.js for the gesture itself. --- */
.wRowTray{position:absolute;top:0;right:0;bottom:0;width:140px;display:flex;align-items:stretch}
.wTrayBtn{flex:1 1 50%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;
  border:0;cursor:pointer;font:inherit;font-size:11.5px;font-weight:700;color:#fff;min-width:44px}
.wTrayBtn svg{width:17px;height:17px}
.wTrayBtn--edit{background:var(--brand,#1f3a5f)}
.wTrayBtn--del{background:#b42318}
.wRowWrap.isSwiped .wRow{transform:translateX(-140px)}
@media(prefers-reduced-motion:reduce){ .wRow{transition:none} }

/* --- Edit-mode visibility (Jeremy 2026-07-20: "you can't tell which
   fields are editable"). A rendered .balInput inside a wallet row IS the
   editable thing (read-only rows render .hubBalTap text instead), so every
   such input gets an unmistakable treatment: gold accent border + the
   accent-soft tinted fill, in both themes — visibly a form field at a
   glance, not just on focus. The existing :focus ring rule still layers
   the stronger glow on top while typing. */
.wCell--bal .balInput{border-color:var(--accent);background:linear-gradient(135deg,var(--accent-soft),var(--bg-surface));box-shadow:0 1px 3px rgba(192,138,30,.18)}
/* Re-assert the focus ring at equal-or-higher specificity than the rule
   above (which would otherwise win the cascade tie and flatten it). */
.wCell--bal .balInput:focus{box-shadow:0 0 0 4px rgba(192,138,30,.18)}

/* --- Swipe discoverability (Jeremy 2026-07-20) --- */
/* Persistent affordance: a quiet chevron hugging the row's right edge on
   mobile, hinting "this slides left". Hidden in Edit mode (the row's real
   controls are visible there), while the tray is open/being dragged, and
   entirely at the desktop breakpoint (no swipe there). Absolutely
   positioned, so it is not a grid item and can never reflow the row. */
.wSwipeHint{display:none;position:absolute;right:2px;top:50%;transform:translateY(-50%);color:var(--ink2);opacity:.45;pointer-events:none}
.wSwipeHint svg{width:14px;height:14px;display:block}
@media(max-width:759px){
  .wRow:not(.isEditing) .wSwipeHint{display:block}
  .wRowWrap.isSwiped .wSwipeHint,.wRow.isDragging .wSwipeHint{display:none}
}
/* One-time peek: the top row slides left to reveal the tray, then settles
   back — fired once per browser from maybeSwipeHintPeek (router.js), which
   also checks prefers-reduced-motion in JS; the media query here is
   belt-and-suspenders. */
/* WS-WALLET3 (Jeremy 2026-07-20, "the little swipe over animation happens
   so fast you miss it"): was 1.1s total with the hold only 0.27s. Now the
   row eases out over ~0.7s, HOLDS the revealed position for ~1.3s, and
   eases back over ~0.8s -- long enough to genuinely register, still one
   quiet nudge rather than a looping tutorial. Firing is also gated on the
   row being visible in the viewport now (IntersectionObserver in
   maybeSwipeHintPeek, router.js). */
/* WS-WALLET4 (Jeremy 2026-07-20, "it only slides over to show the delete
   button and not edit... The preview animation should show both the edit
   and delete buttons are there"): the old -64px apex only uncovered the
   Delete half of the 140px tray, so the one-time preview taught Delete but
   never Edit. The apex now matches the real gesture's resting position
   exactly (-140px == .wRowWrap.isSwiped .wRow == .wRowTray width), so the
   hold shows BOTH buttons. Timing (2.8s in/hold/return), one-time gating,
   and reduced-motion suppression all unchanged. */
@keyframes wSwipePeek{0%{transform:none}25%{transform:translateX(-140px)}72%{transform:translateX(-140px)}100%{transform:none}}
.wRow.swipeHintPeek{animation:wSwipePeek 2.8s cubic-bezier(.22,.61,.36,1) 1}
@media(prefers-reduced-motion:reduce){ .wRow.swipeHintPeek{animation:none} }

/* --- Desktop: the same markup, now a genuinely aligned table --- */
@media(min-width:760px){
  .wRows{gap:0}
  .wHead{display:grid;grid-template-columns:var(--wCols);gap:12px;align-items:center;
    padding:9px 14px;background:var(--bg2);border:1px solid var(--line);border-radius:10px 10px 0 0}
  .wH{font-size:12px;font-weight:750;color:var(--ink2);text-transform:uppercase;letter-spacing:.02em;min-width:0}
  .wH .matrixSortBtn{text-align:left;justify-content:flex-start}
  .wH--val{text-align:right;justify-content:flex-end}
  /* PASS 3: the Typical-value column only exists at >=1000px (below that
     the tablet grid needs every pixel for the Program name) — hide its
     header here so the 4 visible headers map onto the 4 tracks. */
  .wH--rate{display:none}
  .wRow{grid-template-columns:var(--wCols);grid-template-areas:"name bal val act";align-items:center;gap:12px;padding:10px 14px}
  .wRowWrap{border-radius:0;border:1px solid var(--line);border-top:0}
  .wRows>.wRowWrap:last-child{border-radius:0 0 10px 10px}
  .wCell--bal,.wCell--val{flex-direction:row;align-items:center}
  .wCell--bal{justify-content:flex-start}
  .wCell--val{justify-content:flex-end}
  .wCell--bal .inlineBalUnit{display:inline;font-size:12.5px;font-weight:600;color:var(--ink2)}
  .wCell--bal .balSavedTag{position:static}
  /* Enough room for a 3-comma-group balance ("1,234,567") without clipping. */
  .wCell--bal .balInput{min-width:104px}
  .wCellLbl{display:none}
  .wRowTray{display:none}
  .wCell--name{align-items:center}
}
/* PASS 3: at >=1000px there's room for the Direction B wallet table's
   "Typical value" (¢/pt) column — the honest middle term of the math
   (balance × typical value = worth). Balance hands back 24px to pay for
   part of it; the name track keeps its 190px floor. Below 1000px the
   cell and its header stay display:none and the 4-track grid above rules. */
@media(min-width:1000px){
  :root{ --wCols: minmax(180px,1fr) 200px 108px 104px 48px; }
  .wH--rate{display:flex;text-align:right;justify-content:flex-end}
  .wRow{grid-template-areas:"name bal rate val act"}
  .wCell--rate{display:flex;grid-area:rate;align-items:center;justify-content:flex-end;text-align:right}
  .wRate{font-size:12.5px;font-weight:600;color:var(--ink2);white-space:nowrap}
}

.wGhostRow{padding:10px 14px;border:1px solid var(--line);border-radius:12px;background:var(--bg2)}
@media(min-width:760px){ .wGhostRow{border-radius:0;border-top:0} }

.srOnly{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}

/* ============ WS-UNISEARCH: the one "add anything" search ============
   Rendered identically in the wallet's Edit mode and in onboarding -- one
   box over cards + airlines + hotels together, results grouped and labelled
   by type, one tap to add. Sized mobile-first: the input clears 44px, every
   result row's Add button clears 44px, and nothing here uses a fixed
   multi-column track, so it degrades to a plain vertical list at 390px. */
.uniSearch{margin:14px 0 18px;padding:14px;border:1.5px solid var(--line);border-radius:14px;background:var(--bg2)}
.uniTitle{margin:0 0 4px;font-size:13px;font-weight:750;color:var(--ink2);text-transform:uppercase;letter-spacing:.03em}
.uniLede{margin:0 0 10px;font-size:13px}
.uniLabel{display:block;margin:0 0 7px;font-size:14.5px;font-weight:650;color:var(--ink);line-height:1.35}
.uniInputWrap{position:relative;display:flex;align-items:center}
.uniIcon{position:absolute;left:13px;width:18px;height:18px;color:var(--ink2);pointer-events:none}
.uniInput{width:100%;min-height:48px;box-sizing:border-box;padding:12px 44px 12px 40px;
  border:1.5px solid var(--line);border-radius:12px;background:var(--bg-surface);
  font:inherit;font-size:15.5px;color:var(--ink)}
.uniInput:focus{outline:none;border-color:var(--brand);box-shadow:0 0 0 4px rgba(31,58,95,.12)}
.uniInput::placeholder{color:var(--ink2);opacity:.85}
.uniClear{position:absolute;right:4px;width:40px;height:40px;display:flex;align-items:center;justify-content:center;
  border:0;background:none;color:var(--ink2);font-size:22px;line-height:1;cursor:pointer;border-radius:10px}
.uniClear:hover,.uniClear:focus-visible{background:var(--bg2);color:var(--ink)}
.uniResults{margin-top:10px;max-height:min(52vh,420px);overflow-y:auto;-webkit-overflow-scrolling:touch}
.uniHint{margin:8px 2px;font-size:13.5px;color:var(--ink2)}
.uniNoMatch{color:var(--ink)}
.uniGroup+.uniGroup{margin-top:12px}
.uniGroupLabel{margin:0 0 6px;font-size:11.5px;font-weight:750;text-transform:uppercase;letter-spacing:.04em;color:var(--ink2)}
.uniList,.uniAddedList{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}
.uniRow,.uniAddedItem{display:flex;align-items:center;gap:10px;padding:8px 10px;
  border:1px solid var(--line);border-radius:11px;background:var(--bg-surface);min-height:56px}
.uniRow.isHeld{opacity:.72}
.uniArt{display:inline-flex;align-items:center;gap:5px;flex:none}
.uniText{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:2px}
/* Same anti-shatter contract as .wName -- a result name never breaks
   mid-word, and it always has the full row width to lay out in. */
.uniName{font-weight:650;font-size:14.5px;line-height:1.3;color:var(--ink);
  overflow-wrap:break-word;word-break:normal;hyphens:none}
.uniMeta{display:flex;align-items:center;gap:7px;flex-wrap:wrap}
.uniType{font-size:10.5px;font-weight:800;text-transform:uppercase;letter-spacing:.045em;
  padding:2px 7px;border-radius:999px;white-space:nowrap}
.uniType--card{background:rgba(31,58,95,.13);color:var(--brand-ink)}
.uniType--airline{background:rgba(15,118,110,.14);color:var(--good-text)}
.uniType--hotel{background:rgba(167,139,250,.20);color:var(--ink)}
[data-theme="dark"] .uniType--card{background:rgba(147,197,253,.18)}
[data-theme="dark"] .uniType--hotel{background:rgba(167,139,250,.26)}
.uniSub{font-size:12px;color:var(--ink2);min-width:0;overflow-wrap:break-word;word-break:normal}
.uniAddBtn{flex:none;min-height:44px;white-space:nowrap;padding:0 16px;font-size:14px}
.uniHeld{flex:none;font-size:12.5px;font-weight:700;color:var(--good-text);white-space:nowrap;padding:0 6px}
/* Mobile: stack the action under the name instead of competing with it for
   the line. Side by side at 390px the art chip plus the Add button left the
   name barely 110px, which shattered "American Express Green" across three
   lines. Given its own row the name gets ~225px and reads in one or two. */
@media(max-width:759px){
  .uniRow{display:grid;grid-template-columns:auto minmax(0,1fr);
    grid-template-areas:"art text" "act act";gap:6px 10px;align-items:start;padding:10px}
  .uniArt{grid-area:art;padding-top:2px}
  .uniText{grid-area:text}
  .uniRow>.uniAddBtn,.uniRow>.uniHeld{grid-area:act;justify-self:end}
  .uniRow .wsCardChip--pick{width:40px;height:26px}
}
.uniAddedWrap{margin-top:16px}
.uniAddedItem .wDelBtn{margin-left:auto}
/* Totals, separated (Brief #56): credit-card points, airline miles/points,
   hotel points, and the estimated $ value each get their own labeled card
   instead of one folded-together "≈ $X across N programs" sentence. Wraps
   at narrow widths (flex-wrap) rather than ever forcing page-level overflow. */
.hubTotals{display:flex;flex-wrap:wrap;gap:12px;margin:14px 0 18px}
.hubTotalItem{flex:1 1 140px;min-width:140px;background:var(--bg2);border:1px solid var(--line);border-radius:var(--radius);padding:12px 14px;text-align:center}
/* Estimated value card (Jeremy: "should be green and stand out -- we're
   talking about money"). Distinct green tint + border (not the neutral
   --brand-soft the other three totals share) so it visually separates from
   the point/mile counts, and the $ figure itself uses --good-text -- the
   same per-theme-tuned green as the Strategy matrix's best-in-category
   number (5.32:1 on light --bg-surface, 8.13:1 on dark) -- at a larger
   size/weight than the other .hubTotalNum values for extra emphasis. */
.hubTotalItem.hubTotalValue{background:rgba(15,118,110,.10);border-color:rgba(15,118,110,.35)}
[data-theme="dark"] .hubTotalItem.hubTotalValue{background:rgba(52,211,153,.13);border-color:rgba(52,211,153,.4)}
.hubTotalNum{display:block;font-size:20px;font-weight:800;color:var(--ink)}
.hubTotalItem.hubTotalValue .hubTotalNum{color:var(--good-text);font-size:23px;font-weight:850}
.hubTotalLbl{display:block;font-size:12px;color:var(--ink2);font-weight:650;margin-top:4px;line-height:1.4}
.hubTotalSub{font-weight:500;font-size:11px}
@media(max-width:430px){
  .wRow{padding:11px 12px;gap:9px 10px}
  .wName{font-size:14.5px}
  .uniSearch{padding:12px}
  .hubTotalItem{flex-basis:calc(50% - 6px);min-width:0}
  /* Mobile round 2 (c) polish: .hubSummary's roomy 18px/20px card padding
     (fine on desktop) was eating ~40px of the ~390px viewport before the
     wallet table even started, on top of the sitewide .wrap gutter --
     directly widening the horizontal-scroll gap .hubTableScroll needs to
     cover (measured ~145px overflow on the credit-cards bucket at 390px
     with a real 4-card wallet). Tighter card padding + a touch less
     row/name-gap reclaims real width for the PROGRAM/BALANCE columns
     without touching the 44px tap targets PR #135 established (.hubTable
     .inlineBalField.sm .balInput, .hubRemoveBtn, .addRowBtn etc. are
     untouched here). */
  .hubSummary{padding:14px 12px}
}
/* === end WS-MOBILE wallet === */

/* === WS-ONBOARD === */
/* Wallet/onboarding balance-entry UX cleanup (mobile onboarding review from
   real iPhone screenshots -- Jeremy: "The mobile onboarding process is
   genuinely awful"). Replaces the WS-53 "explicit Save/OK" button that used
   to sit next to every balance field: it doubled as a second, confusing
   save path alongside the "balances save automatically as you type" copy,
   and on a long list of programs (Flying Blue, Marriott Bonvoy, Hyatt, ...)
   it meant a Save button on every single row. .balInput still autosaves on
   every keystroke via onBal/onProgBal/onHubBal (persist()+scheduleCloudSync,
   unchanged) -- this is just a quiet, non-interactive confirmation that the
   keystroke landed. See balSavedTagHTML/flashBalSaved in app.js. */
.balSavedTag{font-size:12px;font-weight:700;color:var(--brand-ink);opacity:0;transition:opacity .25s;white-space:nowrap;pointer-events:none}
.balSavedTag.show{opacity:1}
/* One primary action pinned to the bottom of the wallet hub while editing
   balances -- "Done" once signed in, "Save my points" (opens the sign-in
   modal) while signed out -- replacing the old per-row Save button as the
   only clear "I'm finished" affordance on a long balance-entry list. */
.hubBottomAction{margin-top:16px}
/* === end WS-ONBOARD === */

/* === WS-ASKPROM bottom-dock ===
   Retired by WS-DOCK (Fable chat-first spec v2 pass 2): the scroll-
   triggered, position:fixed #askDock bottom bar is gone -- the ONE
   composer lives permanently in #dock (normal flow, last child of
   <main class="appMain">; see the .dock rule near the end of this file),
   present unconditionally on every route, never toggled by scroll. */
/* === end WS-ASKPROM === */

/* === WS-STRAT8 === */
/* Strategy page overhaul: mini-wallet strip, category-matrix explainer, and
   the reworked "cards to consider" section (annotated reasons + a
   diversification caveat + a per-card Ask AI hand-off). See app.js
   stratMiniWalletHTML/renderStrategyHTML/strategyGapsHTML. */
.stratMiniWallet{background:var(--bg2);border:1px solid var(--line);border-radius:var(--radius);padding:14px 16px;margin-bottom:20px}
.stratMiniWalletHead{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}
.stratMiniWalletHead h4{margin:0;font-size:14.5px;font-weight:750;color:var(--ink)}
.stratMiniWalletCount{font-weight:500;font-size:13px}
.stratMiniWalletChips{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
.stratMiniChip{display:flex;align-items:center;gap:8px;background:var(--bg-surface);border:1px solid var(--line);border-radius:999px;padding:6px 12px 6px 6px;font-size:13.5px;font-weight:650;color:var(--ink)}
.stratMiniChip .ico{width:26px;height:26px;font-size:10px}
.stratMiniChip img,.stratMiniChip svg{width:26px;height:26px}
/* Category-matrix explainer heading (task #2) uses the same h3/.muted
   pattern as every other section header on this page (stratMatrixHTML,
   strategyGapsHTML) rather than inventing new type styles. */
.stratCatHead{color:var(--ink)}
/* "Cards to consider" per-suggestion why-list (task #3): plain list, no
   bullets of its own styling beyond spacing — the concrete before/after
   sentence (see strategyGapsHTML's whyItems) is the point, not decoration. */
.stratGapWhy{margin:2px 0 10px;padding-left:20px;font-size:13.5px;color:var(--ink2);line-height:1.5}
.stratGapWhy li{margin:4px 0}
.stratGapAsk{padding:0 0 12px}
.stratAskAiBtn{min-height:44px}
/* Diversification caveat (task #4): a quiet, factual callout — not an
   alarm/warning color, just visually distinct from the recommendation rows
   above and below it so it reads as a separate, honest aside. */
.stratCaveat{background:var(--bg2);border:1px solid var(--line);border-left:3px solid var(--accent);border-radius:8px;padding:10px 14px;margin:0 0 14px;font-size:13.5px;color:var(--ink2);line-height:1.5}
@media(max-width:600px){
  .stratMiniWalletHead{align-items:flex-start}
  .stratMiniChip{font-size:13px}
}
/* === end WS-STRAT8 === */

/* === WS-CHAT === */
/* Item 2/6: the assistant view's ONE ask surface — a persistent input
   pinned to the bottom of the conversation thread, styled with the same
   navy/gold treatment as #askBar/#askDock (Item 6) so the whole Ask AI
   experience reads as one consistent surface rather than the plain white
   .askbox it used to sit inside. Sticky (not fixed) so it settles at the
   bottom of the viewport once the thread grows taller than the screen,
   without ever covering content above it. */
.askThreadBar{position:sticky;bottom:10px;z-index:6;margin-top:18px;padding:14px 18px;background:linear-gradient(180deg,#1c2e57,var(--brand));border-radius:20px;box-shadow:0 16px 34px rgba(20,32,43,.24),0 1px 2px rgba(20,32,43,.08);border:1px solid rgba(255,255,255,.10)}
.askThreadRow{display:flex;align-items:center;gap:10px;border:0;padding:0;background:none;box-shadow:none}
.askThreadInput{flex:1;min-width:0;width:100%;border:1px solid rgba(255,255,255,.28);background:rgba(255,255,255,.1);color:#fff;border-radius:999px;padding:12px 18px;font-size:16px;font-family:var(--font);outline:none;min-height:44px}
.askThreadInput::placeholder{color:rgba(255,255,255,.68)}
.askThreadInput:focus{border-color:var(--accent);background:rgba(255,255,255,.16)}
.askThreadBtn.btn.primary{flex:none;background:var(--accent);color:var(--accent-on);box-shadow:none;min-height:44px;padding:12px 22px;font-size:15px}
.askThreadBtn.btn.primary:hover{background:#c79646}
.askThreadBalance{font-size:12px;color:rgba(255,255,255,.75);margin-top:8px;padding-left:2px}
@media(max-width:600px){
  /* Brief #61 item 3: stay sticky at the bottom on mobile too, matching
     desktop -- the flat position:static this replaced left the bar
     scrolling away with the thread on every phone. It only relaxes back to
     static, letting the bar flow with the page instead of pinning to the
     (keyboard-shrunk) viewport bottom, while the input/button inside has
     focus -- i.e. while the on-screen keyboard is actually open. */
  .askThreadBar{position:sticky;bottom:10px;margin-top:14px}
  .askThreadBar:focus-within{position:static}
}

/* Item 6: a thin gold rule on every AI answer bubble (the navy is already
   carried by .bubble.me) so the thread keeps the same brand navy/gold
   pairing used everywhere else on the site, not just plain white boxes. */
.bubble.ai{border-left:3px solid var(--accent)}

/* Item 4: modal copy button + explanation get a little more breathing room
   than the generic .modal p styling, since this modal now carries two
   paragraphs of explanation before the textarea. */
.deeperModal p{margin:0 0 10px}
.deeperModal p:last-of-type{margin-bottom:14px}
/* === end WS-CHAT === */

/* === WS-REALAI ===
   Ask AI reliability + honesty fixes: the honest signed-out bubble and the
   holdings-confirmation bubble reuse .bubble.ai/.btn.primary/.btn.block/
   .btn.sm/.starter exactly as every other chat message on the site — the
   one genuinely new rule is letting the signed-out bubble's full-width CTA
   label wrap on narrow screens. Every existing .btn label elsewhere in the
   app is short enough to never need it, but "Sign in for a real answer
   grounded in your points →" is not, and without this it would overflow a
   390px viewport instead of wrapping to a second line. */
.bubble.ai3-signedout .btn.block{white-space:normal;text-align:center;line-height:1.3}
/* Brief #61 item 1: the sign-in nudge now sits below a real free answer
   rather than being the whole bubble -- this divider keeps "here's your
   free answer" and "sign in for the real, personalized one" visually
   distinct instead of running together. */
.signedOutCta{margin-top:14px;padding-top:12px;border-top:1px dashed var(--line)}
.signedOutCta p{margin:0 0 10px;font-size:14.5px}
/* === end WS-REALAI === */

/* === WS-CLAUDE p1 tokens ===
   Retired by WS-THEME (top of file, pass 1 of the chat-first spec v2):
   the --ask-* scoped tokens this block used to define (light defaults,
   later overridden by body.theme-ask-dark below) are now plain aliases
   in the sitewide :root onto the semantic --bg-.../--text-.../--accent-...
   tokens, so #view-assistant reads the same active [data-theme] as every
   other route instead of carrying its own separate light/dark system. */
/* === end WS-CLAUDE p1 tokens === */

/* === WS-CHAT1 ===
   Fable Pass 1 chat-shell rebuild for #/ask: one pinned composer, kill
   "Ask another question" + the floating suggestion box, contextual
   follow-up chips, wallet pill, the ✦ Go deeper reskin, and the loading/
   error/empty states. See FABLE-CHAT-REDESIGN.md §A/§D + the Remove list.
   (WS-CLAUDE p1 note: the color literals below were converted to the
   --ask-* tokens above as part of the Claude-style build's token-layer
   pass -- every default still resolves to the exact same value, so this
   is a no-visual-diff cleanup, not a restyle. The restyle lives in the
   body.theme-ask-dark overrides further down.) */

/* §A1 structure, generalized sitewide by WS-DOCK (pass 2): .appShell/
   .appMain are now UNCONDITIONALLY a viewport-height flex column (see
   those rules further down) with #viewsScroll as the one internal scroll
   region and #dock pinned after it -- so the outer page never double-
   scrolls against the inner history region, on ANY route, not just this
   one. #view-assistant additionally subdivides itself into its own
   pinned-top (#chatWalletBar/#askHomeWrap) + scrolling
   (#chatHistoryWrap) structure, same as before. Composer/chips/pill are
   normal flow — never position:fixed — so nothing can ever cover an
   answer again. */
#view-assistant.view.on{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden;animation:none}
@media(max-width:780px){
  body.viewIsAssistant header.nav .wrap{height:48px}
}

.chatTopBar{flex:none;padding:10px 20px 0}
.chatTopBar .back{cursor:pointer}

/* === PP #10/#21: §E2 wallet pill top banner (.chatWalletBar, .chatWalletPill,
   .chatWalletHint) removed along with its markup in index.html -- see the
   "PP #10/#21 (legacy wallet-summary top banner removed)" comment there.
   The composer's bottom "Your Wallet" chip and the #/ask empty-state
   wallet-preview card keep their own, separate styles untouched. */
/* === PASS5-HANDOFF (Fable v2 §5 row 5) ===: "← Back to {page} (keeps this
   chat)" -- a plain-text link row, same --ask-* token family the removed
   wallet pill used to share, only ever visible on #/ask and only when this
   visit came from a ⤢ handoff (see renderAskBackBar in chat.js). */
.askBackBar{flex:none;padding:8px 20px 0}
.askBackBar[hidden]{display:none}
.askBackLink{display:inline-flex;align-items:center;font-size:12.5px;font-weight:650;color:var(--ask-gold-bright);text-decoration:none;cursor:pointer}
.askBackLink:hover,.askBackLink:focus-visible{text-decoration:underline;outline:none}

/* === Back-to-Ask-AI bar ===: the mirror-direction affordance (see
   renderBackToAskBar in chat.js) -- renders on every non-#/ask route, so
   it uses the sitewide semantic tokens (--ink2/--brand-ink, theme-aware
   via [data-theme]) rather than the --ask-* family above, which is
   #/ask-scoped only. Quiet, plain-text link, same visual weight as
   .askBackLink but themed for light routes too. */
.backToAskBar{padding:10px 20px 0;position:sticky;top:var(--topnav-h);z-index:19;background:var(--bg2)}
.backToAskBar[hidden]{display:none}
.backToAskLink{display:inline-flex;align-items:center;font-size:12.5px;font-weight:650;color:var(--ink2);text-decoration:none;cursor:pointer}
.backToAskLink:hover,.backToAskLink:focus-visible{color:var(--brand-ink);text-decoration:underline;outline:none}

/* §A1 history — the ONLY scrolling region on this view. Centered 760px
   column on desktop so history/chips/composer read as one connected
   surface (the actual fix for "isn't connected", complaint #1/#2; 760px
   matches the Claude-style mockup's --col so the home and chat composers
   share the same measure -- see Jeremy's feedback #4, never full-bleed). */
.chatHistoryWrap{flex:1;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;background:var(--ask-canvas)}
.chatHistoryWrap[hidden]{display:none}
.chatHistoryInner{max-width:var(--chatcol);margin:0 auto;padding:16px 20px 24px}
.chatHistoryInner .chat{max-width:none}

/* §A1/§A2 bottom stack: retired by WS-DOCK (pass 2) as a per-view element
   -- chips + composer + balance line now live in the sitewide #dock (see
   the .dock rule near the end of this file), not a #chatBottomWrap scoped
   to #view-assistant. */

/* §A3 contextual follow-up chips — one horizontal-scroll row, ghost pills,
   directly above the composer. Regenerated per answer (renderFollowupChips
   in app.js), never appended — an empty row collapses via [hidden]. */
.followupChips{display:flex;gap:8px;overflow-x:auto;-webkit-overflow-scrolling:touch;padding:0 0 10px;scrollbar-width:none}
.followupChips::-webkit-scrollbar{display:none}
.followupChips[hidden]{display:none}
.chipGhost{flex:none;border:1px solid var(--ask-chip-border);background:var(--ask-chip-bg);color:var(--ask-chip-ink);border-radius:999px;padding:8px 14px;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap;font-family:var(--font)}
.chipGhost:hover{border-color:var(--ask-gold);background:var(--ask-chip-bg-hover)}
.chipGhost:last-child{margin-right:24px}
.chipGoDeeper{border-color:var(--ask-gold);color:var(--ask-gold);font-weight:700}
/* === GUIDED-AI-INTAKE (client) ===: selected-state for a tapped chip
   inside the guided-intake collecting card (.collectBandRow / .collectChip
   --redemption band+choice buttons) -- reuses .chipGhost's own palette
   tokens rather than inventing new colors, just flips to the filled/
   "confirmed" treatment so a tapped answer reads as answered at a glance. */
.chipGhost.sel{background:var(--ask-gold);border-color:var(--ask-gold);color:var(--bg-surface, #fff)}
/* guided-intake spend: exact-figure number boxes (replaced the spend bands),
   styled to match the Wallet Budget tab's $ __ /mo field. */
.collectAmtHint{margin:6px 0 4px;font-size:12px;color:var(--text-2)}
.collectAmtRow{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:8px}
.collectAmtLabel{font-size:13.5px;font-weight:600;color:var(--text-1)}
.collectAmtField{display:inline-flex;align-items:center;gap:2px;border:1px solid var(--line-strong);background:var(--bg-surface-2);border-radius:10px;padding:6px 10px;min-height:40px}
.collectAmtField:focus-within{border-color:var(--ask-gold);box-shadow:0 0 0 3px var(--accent-soft)}
.collectAmtCur{font-size:14px;font-weight:600;color:var(--text-2)}
.collectAmtInput{border:0;background:transparent;outline:none;width:74px;text-align:right;font-size:15px;font-weight:600;color:var(--text-1);font-family:var(--font);padding:0 2px}
/* JOB3 (iOS focus-zoom, cont'd -- see EOF): a single class, (0,1,0), still
   outranks the sitewide guard's bare `input` (0,0,1) even though the guard
   has a type selector too -- class count beats type count regardless of
   how many types are on the other side. Computed 15px, zoomed on focus.
   Matched at (0,1,0) here so it wins on source order. */
@media(max-width:899px){ .collectAmtInput{font-size:16px} }
.collectAmtUnit{font-size:12px;color:var(--text-2)}

/* §A2 the composer — full-width white field, gold focus ring, gold Send
   pill (disabled at 40% when empty). Auto-grows up to ~4 lines then
   scrolls internally (height managed in PP.composerInput). WS-DOCK (pass
   2): this ONE node now lives permanently in the sitewide #dock -- the
   old .composerHome hero-position modifier is retired (Decision 4: no
   hero-centered composer anywhere). */
/* WS-ASKEMPTY (item 2): the composer is now a two-row column -- .composerTop
   (textarea + Send, same layout the old flat .composer row used) plus a
   .composerToolbar row underneath holding the persistent "Your Wallet"
   button, matching the Claude/ChatGPT composer shape (input on top, a
   toolbar row below, both inside one rounded box). .composer itself keeps
   the outer box (background/border/radius/shadow) but switches to a column
   flex so the two rows stack; the 16px left inset the old flat row had
   moves onto .composerTop specifically (6px box padding + 10px = 16px). */
.composer{display:flex;flex-direction:column;background:var(--bg-surface-2);border:1px solid var(--line-strong);border-radius:20px;padding:6px;box-shadow:0 10px 26px var(--ask-composer-shadow);transition:background .2s,border-color .2s,box-shadow .2s}
.composer:focus-within{box-shadow:0 0 0 2px var(--ask-gold),0 10px 26px var(--ask-composer-shadow)}
.composerTop{display:flex;align-items:flex-end;gap:8px;padding:0 0 0 10px}
.composerInput{flex:1;min-width:0;border:0;outline:none;resize:none;background:transparent;font:15px/1.45 var(--font);color:var(--ask-ink);padding:10px 0;max-height:108px;overflow-y:auto}
.composerInput::placeholder{color:var(--ask-muted)}
.composerInput:disabled{color:var(--ask-muted);cursor:not-allowed}
.composerSend{flex:none;border:0;border-radius:999px;background:var(--ask-gold);color:var(--ask-gold-ink);font-weight:750;font-size:14.5px;padding:11px 20px;min-height:44px;cursor:pointer;font-family:var(--font)}
.composerSend:hover:not(:disabled){background:var(--ask-gold-bright)}
.composerSend:disabled{opacity:.55;cursor:not-allowed}
.composerToolbar{display:flex;align-items:center;gap:8px;padding:6px 8px 4px 10px;margin-top:2px;border-top:1px solid var(--ask-line)}
/* Mobile audit P1 #4 (2026-07-19): 32px min-height on the two most
   persistent, always-visible controls on the entire site (this pill +
   .composerFeedbackBtn below) was under the 44px floor. Bumped straight to
   44px rather than an invisible ::before hit-slop — unlike the tiny matrix
   icon buttons, these are full visible text pills where the "hit area"
   and the "visible button" should just be the same box; a taller pill
   here reads as correctly sized, not bloated. */
.composerWalletBtn{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--ask-line-strong);background:transparent;border-radius:999px;padding:6px 12px;font-size:12.5px;font-weight:650;color:var(--ask-ink-dim);cursor:pointer;font-family:var(--font);min-height:44px;transition:border-color .15s,background .15s,color .15s}
.composerWalletBtn svg{width:15px;height:15px;flex:none}
.composerWalletBtn:hover{border-color:var(--ask-gold);background:var(--ask-gold-soft);color:var(--ask-ink)}
/* Small "Feedback" button in the AI window's composer toolbar (backlog
   #85/#86 + Jeremy's note) — same shell as .composerWalletBtn immediately
   above it, own class only so it can be told apart in the DOM/tests. */

/* §A4 free-balance indicator — muted, gold at <=2, and a distinct message
   at 0 (composer itself is disabled by renderAskBalance in app.js). */
.composerBalance{text-align:center;font-size:12px;color:var(--ask-balance-ink);padding:8px 4px 0}
.composerBalance[hidden]{display:none}
.composerBalance.low{color:var(--ask-gold);font-weight:700}
.composerBalance.zero{color:var(--ask-gold);font-weight:700}

/* §A5/§B5 assistant cards — empty state + answer footer row. */
.assistantCard.emptyCard{border-left:3px solid var(--ask-gold)}
.emptyStarterChips{margin-top:12px}
.chatFooterRow{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-top:12px;padding-top:10px;border-top:1px solid var(--ask-line)}
.chatFooterGround{font-size:12px;color:var(--ask-muted)}
.chatFooterGround a{color:var(--ask-muted)}
/* Brief #61 item 6's citation disclosure, folded into the footer row: the
   "Grounded in..." text doubles as a <summary> toggle (no default marker,
   no bullet) when there's real wallet data to cite. */
.chatFooterDetails{min-width:0;flex:1}
.chatFooterDetails summary.chatFooterGround{cursor:pointer;list-style:none;user-select:none}
.chatFooterDetails summary.chatFooterGround::-webkit-details-marker{display:none}
.chatFooterSeeSources{text-decoration:underline}
.chatFooterMethodLink{margin-top:8px}
.chatFooterMethodLink a{font-size:12px;color:var(--ask-muted)}

/* §D1 the ✦ Go deeper button — the ONLY colored action in the footer row,
   so it reads as the premium next step, not an off-brand pill (complaint
   #4/#5). Ghost gold border, fills gold on hover/press.
   Jeremy 2026-07-19: "should be a button with a little logo and a one or
   two word description, it shouldn't show up as a giant button on every
   chat response" -- tightened padding/font further and added the icon+gap
   layout for the shortened "✦ Your AI ↗" label (chat.js footerRowHTML).
   Click target/behavior unchanged, this is sizing/copy only. */
.goDeeperBtn{flex:none;display:inline-flex;align-items:center;gap:5px;border:1px solid var(--ask-gold);background:transparent;color:var(--ask-gold-bright);border-radius:999px;padding:6px 12px;font-size:12.5px;font-weight:750;cursor:pointer;white-space:nowrap;font-family:var(--font);min-height:32px}
/* P0 fix (2026-07-19): the default ghost state used --ask-gold-ink
   (--accent-on, "text on gold FILLS" -- 6.4:1 against a solid gold
   background) as the label/icon color while the button's own background
   is transparent. Against the assistant card surface -- especially the
   dark-theme card (#16212c) sitting right next to that same near-black
   ink (#14202b) -- label + icon were essentially camouflaged: an
   outlined pill with invisible contents. Swapped to --ask-gold-bright
   (--accent-text), the token actually designed to be gold TEXT read
   against a normal surface (6.2:1 light / 8.9:1 dark). The ink token is
   still correct once the background truly turns solid gold on
   hover/focus, so it's applied there instead. */
.goDeeperBtn:hover,.goDeeperBtn:focus-visible{background:var(--ask-gold);color:var(--ask-gold-ink)}
.goDeeperIcon{font-size:12px;line-height:1}

/* §A6 loading state — honest 3-step status, ✦ pulses. */
.loadingCard .ai3-status{display:flex;align-items:center;gap:9px;font-size:14px;color:var(--ask-muted)}
.starPulse{color:var(--ask-gold);font-size:16px;line-height:1;display:inline-block;animation:wsChatStarPulse 1.2s ease-in-out infinite}
@keyframes wsChatStarPulse{0%,100%{opacity:.35}50%{opacity:1}}
@media(prefers-reduced-motion:reduce){ .starPulse{animation:none;opacity:1} }

/* §A7 error card — inline, never a toast, never a dead end. */
.errorCard{border-left:3px solid var(--ask-error)}

@media(pointer:coarse){
  .goDeeperBtn{min-height:44px}
  .chipGhost{min-height:36px;display:inline-flex;align-items:center}
}
/* === end WS-CHAT1 === */

/* === WS-CLAUDE p2 dark restyle ===
   Retired as a body-class-scoped, #/ask-only surface by WS-THEME (pass 1
   of the chat-first spec v2, top of file): the --ask-* tokens this block
   used to flip on body.theme-ask-dark are now sitewide :root aliases
   pointed at the semantic --bg-.../--text-.../--accent-... tokens, which follow
   [data-theme] on <html> like every other route. The selectors below
   (previously prefixed `body.theme-ask-dark `) are unprefixed now -- same
   rules, always active, themed by the same mechanism as the rest of the
   app instead of a special case. Contrast floor per spec §3.3: body copy
   >= --text-2 (9.9:1 dark / 8.0:1 light), nothing readable-sized drops
   below --text-4 (5.2:1 dark / 3.9:1 light -- the old #6b7885 3.6:1
   culprit is gone). The ✦ pulse's prefers-reduced-motion guard already
   lives on .starPulse above and isn't theme-scoped, so it keeps working
   unchanged here. */
#view-assistant{background:var(--ask-canvas)}
.chatTopBar .back{color:var(--ask-muted)}
.chatTopBar .back:hover{color:var(--ask-ink)}

/* Chat bubbles + assistant cards -- scoped under #view-assistant so the
   home-page demo's identical .bubble classes (.ppHero .bubble) are
   untouched. */
#view-assistant .bubble.me{background:var(--ask-user-bubble);color:var(--ask-ink)}
#view-assistant .bubble.ai{background:var(--ask-surface);border-color:var(--ask-line);color:var(--ask-ink-dim);box-shadow:0 6px 24px rgba(0,0,0,.28)}
#view-assistant .bubble.ai strong{color:var(--ask-ink)}

/* Receipts -- #receiptScrim is shared by every route (Explore, Wallet,
   #/ask, ...); these rules are unscoped now so the receipt modal renders
   in the active [data-theme] no matter which page opened it, instead of
   only ever being dark from #/ask. */
#view-assistant .receipt-trig{border-bottom-color:var(--ask-muted);color:inherit}
#view-assistant .receipt-trig::after{color:var(--ask-muted)}
#view-assistant .receipt-trig:hover{border-bottom-color:var(--ask-ink)}
#receiptScrim .modal{background:var(--ask-surface-2);border:1px solid var(--ask-line-strong);color:var(--ask-ink-dim)}
#receiptScrim .modal .x{color:var(--ask-muted)}
#receiptScrim .modal .x:hover{color:var(--ask-ink)}
#receiptScrim h3,
#receiptScrim h4{color:var(--ask-ink)}
#receiptScrim .receiptSrc,
#receiptScrim .receiptAsOf{color:var(--ask-muted)}
#receiptScrim .receiptSrc a{color:var(--ask-gold-bright)}
#receiptScrim .kv,
#receiptScrim .receiptChanges{border-color:var(--ask-line)}

/* Knowledge pill -- scoped to #/ask instances; the wallet/home pill slots
   stay light. */
#view-assistant .knowledgePill{background:var(--ask-surface);border-color:var(--ask-line);border-top-color:var(--ask-gold)}
#view-assistant .knowledgePillLabel{color:var(--ask-muted)}
#view-assistant .knowledgePillText{color:var(--ask-ink-dim)}
#view-assistant .pillChip{background:var(--ask-gold-soft);color:var(--ask-gold-bright);border-color:rgba(199,154,59,.35)}

/* Go-deeper sheet -- only ever opened from #/ask (PP.deeper()). */
#deepScrim .modal{background:var(--ask-surface);border:1px solid var(--ask-line-strong);color:var(--ask-ink-dim)}
#deepScrim h3{color:var(--ask-ink);font-family:var(--ask-serif);font-weight:500;margin:0}
#deepScrim .muted{color:var(--ask-muted)}
#deepScrim .x{color:var(--ask-muted)}
#deepScrim .x:hover{color:var(--ask-ink)}
#deepScrim textarea{background:var(--ask-surface-2);border-color:var(--ask-line);color:var(--ask-ink-dim)}
#deepScrim .note{color:var(--ask-faint)}
#deepScrim .btn.ghost{background:transparent;border-color:var(--ask-line-strong);color:var(--ask-ink-dim)}
#deepScrim .btn.accent{background:var(--ask-gold);color:var(--ask-gold-ink)}
/* Pass 9 (#24): the modal's ✦ moves from an inline glyph mashed against the
   heading text into its own small circular badge -- same four-pointed-star
   glyph and gold tokens used everywhere else (.goDeeperIcon, .starPulse),
   just given the "clean modern treatment" a modal header deserves. Tokens
   are the sitewide --ask-* pass-throughs (see WS-THEME at the top of this
   file) so this reads correctly in both themes with no extra overrides. */
#deepScrim .deeperHead{display:flex;align-items:center;gap:10px;margin-bottom:6px}
#deepScrim .deeperSparkle{flex:none;display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:50%;background:var(--ask-gold-soft);color:var(--ask-gold-bright);font-size:16px;line-height:1}

/* Signed-out Instant Read + sign-in upsell (Fable §C/§2.8) -- these render
   inside #view-assistant too (the honest "computed, no AI" card a
   signed-out visitor sees), so they inherit the dark restyle the same as
   everything else on this surface. */
#view-assistant .instantReadCard{background:var(--ask-surface);border-color:var(--ask-line);border-left-color:var(--ask-gold);color:var(--ask-ink-dim)}
#view-assistant .instantReadBadge{background:var(--ask-gold-soft);color:var(--ask-gold-bright);border-color:rgba(199,154,59,.35)}
#view-assistant .instantReadSubhead{color:var(--ask-muted)}
#view-assistant .instantReadValueLine{color:var(--ask-ink)}
#view-assistant .instantReadGoesWhere a{color:var(--ask-gold-bright)}
#view-assistant .signInUpsellCard{background:var(--ask-surface-2);border-color:var(--ask-line)}
#view-assistant .signInUpsellBody,
#view-assistant .signInUpsellReassure{color:var(--ask-muted)}
#view-assistant .signInUpsellHead{color:var(--ask-ink)}
#view-assistant .signedOutCta{border-top-color:var(--ask-line)}
/* === end WS-CLAUDE p2 dark restyle === */

/* === WS-CONTRAST ===
   Founder-reported contrast bugs on the live dark #/ask surface (real
   screenshots, not a spec re-read): several WS-CLAUDE p2 selectors above
   only covered the CONTAINER (backgrounds/borders) of a few elements, not
   every child text/link inside them -- so those children kept inheriting
   pre-dark, light-theme literals (--ink2 #4a5b68, --brand-ink #0e1a33, the
   global `a{color:var(--brand-ink)}`) that read fine on white but are
   near-invisible on the #101820/#16212c/#1b2836 dark tokens. Every rule
   below is scoped the same way the rest of WS-CLAUDE p2 is (body.theme-
   ask-dark + #view-assistant/#receiptScrim/#deepScrim), so it only ever
   touches the dark #/ask surface -- no light route is affected. */

/* Bug 1: the footer "Go Deeper" pill's ghost (transparent-bg) state used
   --ask-gold-ink for its text -- a near-black navy meant for text ON a
   filled gold background (the :hover state), not for text on the dark
   canvas showing through the transparent default state. Default text is
   now the same bright gold as the border; the :hover fill state (which
   already turns the background solid gold) keeps the dark ink text so it
   stays legible against its own fill. Label copy itself is renamed in
   app.js (footerRowHTML) from "✦ Go deeper" to "Use your own AI
   assistant" -- CSS here only fixes legibility, not position/behavior. */
#view-assistant .goDeeperBtn{color:var(--ask-gold-bright)}
#view-assistant .goDeeperBtn:hover{color:var(--ask-gold-ink)}

/* Bug 2: the "— see sources" disclosure (citationsHTML, an inline <details>
   under the footer row -- not a separate scrim/modal, but the founder's
   screenshot groups it with the receipts sheet since tapping either one
   opens a citation list) is the actual "light background sitting on the
   dark surface": `.citationsList{background:var(--bg2)}` is a flat, un-
   themed light literal with no ask-dark override at all, so the whole
   expanded box rendered as a light card floating inside the dark answer
   card. Its rows compounded that -- each held currency's NAME is a plain
   <span> styled by the sitewide `.citationRow span{color:var(--ink2)}`,
   also never brought into the ask-dark token set (it read fine on the
   accidental light box, which is exactly why the bug was easy to miss in
   isolation). Same root cause hits the #receiptScrim sheet's own trailing
   bits: `.receiptChangeDate` (also --ink2) and any plain, unclassed <a> in
   its body (the "How we verify →" link + each "What's changed" row's
   "source" link), which fall back to the sitewide `a{color:var(--brand-
   ink)}`. The sheet's own .modal/.receiptSrc/h3/h4 background+ink were
   already dark (WS-CLAUDE p2 above) -- these are the specific children
   that were still reading light-theme colors on that already-dark sheet. */
#view-assistant .citationsList{background:var(--ask-surface-2)}
#view-assistant .citationRow span{color:var(--ask-muted)}
#receiptScrim a{color:var(--ask-gold-bright)}
#receiptScrim .receiptChangeDate{color:var(--ask-muted)}
#receiptScrim .receiptChangeItem{border-color:var(--ask-line)}

/* Bug 3: the "✦ WORTH KNOWING" pill's "See in Explore →" link is built by
   the same knowledgePillHTML() used on the (light) wallet/home pill slots,
   via the shared `.pillLink a{color:var(--brand-ink)}` -- scoped to
   #view-assistant only, same convention as every other knowledgePill* rule
   in WS-CLAUDE p2 above, so the wallet/home light pill is untouched. */
#view-assistant .pillLink a{color:var(--ask-gold-bright)}

/* Bug 4 (audit): the "AI-generated estimate — not financial advice"
   footnote and any other plain `.muted` text rendered inside an assistant
   answer card use the sitewide `.muted{color:var(--ink2)}` -- WS-CLAUDE p2
   only ever brought #deepScrim's .muted into the dark set, not #view-
   assistant's or #receiptScrim's. Also: the sign-in upsell's ghost button
   ("Email me a sign-in link") used the sitewide `.btn.ghost{background:
   #fff;color:var(--brand-ink)}` -- a solid white rectangle sitting on the
   dark canvas, the literal "light popover bleeding onto the dark surface"
   the audit calls out even though it's a button, not a modal. Mirrors the
   transparent-ghost treatment #deepScrim's own .btn.ghost already got. */
#view-assistant .muted{color:var(--ask-muted)}
#receiptScrim .muted{color:var(--ask-muted)}
#view-assistant .btn.ghost{background:transparent;border-color:var(--ask-line-strong);color:var(--ask-ink-dim)}

/* Bug 4 (audit) continued: #deepScrim's own dark override for its textarea
   (WS-CLAUDE p2 above: `body.theme-ask-dark #deepScrim textarea{...}`) has
   never actually applied -- #deepText carries its color/background/border
   as an INLINE style in index.html, and inline styles always beat an
   external class selector no matter its specificity, dark-scoped or not.
   Rather than reach for !important, the inline style was trimmed back to
   layout-only properties (index.html) and its visual defaults moved here
   as a normal, unscoped base rule -- zero light-theme diff (same literals
   the inline style had), and it lets the existing dark override actually
   win the cascade the way it always should have. */
#deepText{border:1px solid var(--line);color:var(--ink2);background:var(--bg2)}
/* === end WS-CONTRAST === */

/* === WS-CLAUDE p3 home state (composer relocation retired by WS-DOCK) ===
   Fable §2.2-2.4 / chat-first spec v2 §1.1 Decision 4: the empty-
   conversation layout of #/ask -- centered serif greeting + up to 4
   starter chips, ABOVE the one composer, which lives permanently in the
   sitewide #dock now (never relocated here -- the old .composerHome
   hero-position modifier + #askHomeComposerSlot slot are gone). Markup
   lives in index.html; visibility is driven by renderAskHomeState() in
   app.js. */
/* PP #30 (reworked): the empty-state column groups with the docked
   composer as one unit. Base rule below still anchors the greeting to the
   END of the cross axis (align-items:flex-end) -- i.e. hugging whatever
   sits right below it -- which by itself reads as "just above a
   bottom-pinned composer" (the previous #30 shipment). The NEW Claude/
   ChatGPT-style requirement -- the whole group (greeting + #dock)
   vertically CENTERED in the viewport, only on the truly empty #/ask
   state -- is layered on top via body.askEmpty further down (toggled in
   router.js's updateAssistantChatSurfaces whenever the route or
   chatStarted flips): it makes #viewsScroll/#view-assistant/.askHomeWrap
   shrink-to-fit their content instead of stretching to fill the height
   above #dock, then lets .appMain center the resulting (shrunk content +
   #dock) stack on its own main axis. #dock itself is untouched -- same
   node, same DOM position, same flex:0 0 auto sizing -- it just ends up
   directly under a shrunk, centered greeting block instead of pinned to
   the bottom of a tall empty one. The instant chatStarted flips true
   (first send) or the route changes away from #/ask, askEmpty comes off
   and this reverts to the plain fill-height/bottom-pinned rule below. */
.askHomeWrap{flex:1;min-height:0;display:flex;align-items:flex-end;justify-content:center;padding:24px 24px 48px;overflow-y:auto}
.askHomeWrap[hidden]{display:none}
.askHomeCenter{width:min(var(--chatcol),100%);display:flex;flex-direction:column;align-items:center}
.askHomeGreeting{font-family:var(--ask-serif);font-weight:400;font-size:clamp(26px,3.4vw,38px);letter-spacing:-.01em;text-align:center;line-height:1.2;color:var(--ask-ink);margin:0}
.askHomeStar{color:var(--ask-gold);font-size:.72em;vertical-align:.12em;margin-right:9px}
.askHomeSub{margin:14px 0 0;font-size:15.5px;color:var(--ask-ink-dim);text-align:center;line-height:1.55;max-width:560px}
.askHomeSub a{color:var(--ask-ink-dim);text-decoration:underline}
/* Signed-out Wallet and Strategy explainers keep the public promise visible
   before a visitor is asked to enter anything. */
.ppPublicWallet{max-width:1120px;margin:16px auto;padding:clamp(10px,2vw,26px);display:grid;gap:clamp(44px,7vw,82px)}
.ppPublicEyebrow,.ppsEyebrow{margin:0 0 9px;color:var(--accent-text);font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}
.ppPublicWallet h1,.ppsPublicStrategy h2{margin:0;font:700 clamp(32px,5vw,52px)/1.02 var(--display);letter-spacing:-.035em;color:var(--ink)}
.ppPublicLead,.ppsPublicLead{max-width:700px;margin:16px 0 0;font-size:17px;line-height:1.6;color:var(--ink2)}
.ppPublicActions,.ppsPublicActions{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin:24px 0 12px}
.ppPublicTrust{margin:0;color:var(--ink2);font-size:14px}
.ppPublicWalletView #view-points>.wrap>h2,.ppPublicWalletView #view-points>.wrap>.pageSub,.ppPublicWalletView #walletInterviewChipWrap{display:none}
.ppPublicWalletHero{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(280px,.75fr);align-items:end;gap:clamp(30px,6vw,76px);padding:clamp(34px,7vw,84px) 0 10px}
.ppWalletOutcome{display:grid;gap:11px;padding:24px;border:1px solid color-mix(in srgb,var(--accent) 45%,var(--line));border-radius:22px;background:color-mix(in srgb,var(--accent) 7%,var(--bg-surface));box-shadow:0 18px 50px rgba(16,34,52,.08)}
.ppWalletOutcome span{font-size:11px;font-weight:800;letter-spacing:.09em;text-transform:uppercase;color:var(--accent-text)}
.ppWalletOutcome strong{padding:12px 0;border-top:1px solid var(--line);font:700 17px/1.25 var(--display);color:var(--ink)}
.ppWalletSample{padding:clamp(22px,4vw,40px);border:1px solid var(--line);border-radius:26px;background:var(--bg-surface);box-shadow:0 20px 58px rgba(16,34,52,.08)}
.ppWalletSampleHead{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:28px;margin-bottom:24px}.ppWalletSample h2,.ppWalletUnlocks h2,.ppWalletTrust h2,.ppWalletClosing h2{margin:0;font:700 clamp(28px,4vw,42px)/1.05 var(--display);letter-spacing:-.03em;color:var(--ink)}
.ppWalletSampleHead p:not(.ppPublicEyebrow),.ppWalletUnlocks>p,.ppWalletTrust p,.ppWalletClosing p{max-width:690px;margin:11px 0 0;color:var(--ink2);font-size:15px;line-height:1.55}
.ppWalletSampleTotal{display:grid;gap:4px;text-align:right}.ppWalletSampleTotal span{font-size:12px;font-weight:750;color:var(--ink2)}.ppWalletSampleTotal strong{font:800 clamp(38px,5vw,58px)/1 var(--display);letter-spacing:-.04em;color:var(--ink)}.ppWalletSampleTotal small{font-size:12px;color:var(--ink2)}
.ppWalletSampleRows{display:grid;grid-template-columns:1fr 1fr;gap:10px}.ppWalletSampleRows article{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:15px;border:1px solid var(--line);border-radius:15px;background:var(--bg-canvas)}.ppWalletSampleRows span{display:grid;gap:4px;font-weight:750}.ppWalletSampleRows small{font-size:12px;font-weight:500;color:var(--ink2)}.ppWalletSampleRows b{font-size:16px}
.ppWalletDemoLink{display:inline-flex;align-items:center;min-height:44px;margin-top:17px;color:var(--accent-text);font-weight:750;text-decoration:underline}
.ppWalletUnlocks{display:grid;gap:22px}.ppWalletUnlockGrid{display:grid;grid-template-columns:repeat(3,1fr);gap:13px}.ppWalletUnlockGrid article{padding:22px;border:1px solid var(--line);border-radius:19px;background:var(--bg-surface)}.ppWalletUnlockGrid span{display:grid;place-items:center;width:34px;height:34px;border-radius:999px;background:var(--accent-soft);color:var(--accent-text);font-weight:800}.ppWalletUnlockGrid h3{margin:17px 0 7px;font-size:19px}.ppWalletUnlockGrid p{margin:0;color:var(--ink2);font-size:14px;line-height:1.48}
.ppWalletTrust{display:grid;grid-template-columns:1.1fr .9fr;gap:30px;padding:clamp(24px,4vw,40px);border-radius:24px;background:color-mix(in srgb,var(--teal) 8%,var(--bg-surface))}.ppWalletTrust ul{display:grid;align-content:center;gap:12px;margin:0;padding-left:24px;font-size:14px;font-weight:650;line-height:1.4}
.ppWalletClosing{display:flex;align-items:center;justify-content:space-between;gap:32px;padding:clamp(24px,4vw,40px);border-radius:24px;background:#173330;color:#fff}.ppWalletClosing h2{color:#fff}.ppWalletClosing p{color:rgba(255,255,255,.77)}.ppWalletClosing .ppPublicActions{flex:0 0 auto;margin:0}.ppWalletClosing .secondary{color:#fff;border-color:rgba(255,255,255,.55)}
.ppsPublicStrategy{max-width:760px}.ppsPublicSample{margin-top:22px}.ppsSampleTag{display:inline-block;margin-bottom:8px;font-size:12px;font-weight:800;color:var(--accent-text);text-transform:uppercase;letter-spacing:.06em}.ppsTextLink{display:inline-flex;align-items:center;min-height:44px;color:var(--ink);font-weight:700;text-decoration:underline}.pricingStart{margin:16px 0 0}
@media(max-width:760px){.ppPublicWalletHero,.ppWalletTrust{grid-template-columns:1fr}.ppWalletSampleHead{grid-template-columns:1fr}.ppWalletSampleTotal{text-align:left}.ppWalletUnlockGrid{grid-template-columns:1fr}.ppWalletClosing{align-items:flex-start;flex-direction:column}.ppWalletClosing .ppPublicActions{width:100%}.ppPublicWallet h1,.ppsPublicStrategy h2{font-size:38px}}
@media(max-width:560px){.ppWalletSampleRows{grid-template-columns:1fr}.ppWalletClosing .btn{width:100%}.ppPublicWallet h1,.ppsPublicStrategy h2{font-size:34px}}
/* WS-ASKEMPTY (item 3): the wallet-preview card -- replaces the old plain
   .askHomeWalletFold text line with three calcBucket() bucket totals
   (Credit cards / Airline miles / Hotel points), the same held-program
   logo chips wizHeroLogos() renders on the wallet page, and a "Go to your
   wallet" CTA. Populated by askHomeWalletPreviewHTML() in chat.js; renders
   '' (stays [hidden]) whenever there's no wallet value yet. */
.askHomeWalletPreview{margin-top:20px;width:100%;max-width:440px;background:var(--ask-surface);border:1px solid var(--ask-line-strong);border-radius:16px;padding:16px 18px;box-shadow:0 6px 20px var(--ask-composer-shadow)}
.askHomeWalletPreview[hidden]{display:none}
.askHomeWalletTotals{display:flex;justify-content:space-between;gap:10px}
.askHomeWalletTotal{display:flex;flex-direction:column;align-items:center;flex:1;min-width:0}
.askHomeWalletTotalLabel{font-size:11px;font-weight:650;color:var(--ask-muted);text-transform:uppercase;letter-spacing:.03em;text-align:center}
.askHomeWalletTotalVal{margin-top:4px;font-size:17px;font-weight:750;color:var(--ask-ink)}
.askHomeWalletLogos{display:flex;flex-wrap:wrap;justify-content:center;gap:5px;margin-top:14px}
.askHomeWalletLogos:empty{display:none;margin:0}
.askHomeWalletGoBtn{display:block;width:100%;margin-top:14px;border:1px solid var(--ask-gold);background:transparent;color:var(--ask-gold-bright);border-radius:999px;padding:9px 16px;font-size:13.5px;font-weight:750;cursor:pointer;font-family:var(--font);text-align:center;transition:background .15s,color .15s}
.askHomeWalletGoBtn:hover{background:var(--ask-gold);color:var(--ask-gold-ink)}
/* WS-ASKEMPTY (item 4): starter chips are first-timers-only now
   (askHomeChipsHTML is only called for a first-timer -- see
   renderAskHomeState in chat.js); :empty collapses the row's own
   margin-top for a returning visitor instead of leaving a blank gap. */
.askHomeChips{margin-top:20px;display:flex;flex-wrap:wrap;justify-content:center;gap:9px;max-width:var(--chatcol)}
.askHomeChips:empty{display:none;margin:0}
.askHomeChip{display:inline-flex;align-items:center;padding:8.5px 15px;border-radius:999px;border:1px solid var(--ask-line-strong);font-size:13px;color:var(--ask-ink-dim);background:transparent;transition:border-color .15s,background .15s,color .15s;font-family:var(--font);cursor:pointer}
.askHomeChip:hover{border-color:var(--ask-gold);background:var(--ask-gold-soft);color:var(--ask-ink)}
/* WS-ASKEMPTY (item 5): .askHomeTrust now lives BELOW #dock (moved out of
   .askHomeCenter into .dockInner, see index.html) -- same look, just a
   tighter top margin since it now sits directly under the composer instead
   of under a tall chip row. Visibility (guests/first-timers only, #/ask
   empty state only) is gated in JS -- see renderAskHomeTrust() in chat.js. */
.askHomeTrust{margin:12px auto 0;max-width:var(--chatcol);font-size:12px;color:var(--ask-faint);text-align:center}
.askHomeTrust[hidden]{display:none}
.askHomeTrust a{color:var(--ask-faint);text-decoration:underline}
.askHomeTrust a:hover{color:var(--ask-muted)}

@media(max-width:600px){
  .askHomeGreeting{font-size:24px}
  .askHomeWrap{flex-direction:column;align-items:stretch;padding:16px 16px 32px}
  .askHomeWrap>.ppg{width:100%}
}

/* PP #30 (reworked, Jeremy's decision 2026-07-18): body.askEmpty is on
   ONLY on the truly empty #/ask screen (route===assistant &&
   !chatStarted -- see updateAssistantChatSurfaces in router.js). It
   overrides the flex sizing of the chain that leads down to #dock so the
   greeting group + composer read as ONE vertically centered column, like
   a fresh Claude/ChatGPT chat, instead of the fill-height/bottom-pinned
   layout above. #dock's own rule (near the end of this file) is
   untouched -- it keeps flex:0 0 auto and its normal position as the
   last child of .appMain; centering comes entirely from letting the
   content ABOVE it shrink instead of stretch, then centering the whole
   (now-shorter) .appMain stack on its main axis. */
/* WS-WALLET3 (Jeremy 2026-07-20, "giant open space at the top instead of
   having the menu bar pinned to the top"): centering used to be
   justify-content:center on .appMain, which centered EVERY flex child --
   including the mobile header.nav, floating the logo/hamburger bar ~170px
   down the screen on a 390x844 phone. Auto margins on the group we
   actually want centered ({#viewsScroll, #dock}) split the free space
   above/below THAT group instead: header.nav (a preceding sibling, mobile
   only) stays glued to the very top, and on desktop (header display:none)
   the result is pixel-identical to the old centering. Auto margins also
   collapse to 0 when content is taller than the viewport, so nothing can
   be pushed above the scrollable area. */
body.askEmpty #viewsScroll{margin-top:auto}
body.askEmpty #dock{margin-bottom:auto}
/* Include the assistant-route class so this empty-state exception outranks
   the later `body.viewIsAssistant .viewsScroll{flex:1}` shell rule. Without
   it, the shell stretches this region back to full height and leaves the
   greeting hundreds of pixels above the composer. */
body.viewIsAssistant.askEmpty .viewsScroll{flex:0 1 auto;min-height:0}
/* view-assistant's base rule carries overflow:hidden (it normally manages
   its OWN internal scroll via #chatHistoryWrap, never needing the
   ancestor #viewsScroll's scrollbar). In the shrink-to-fit askEmpty state
   there is no inner scroller anymore -- #chatHistoryWrap stays [hidden]
   the whole time -- so overflow has to be handed back to #viewsScroll
   (still overflow-y:auto, untouched) or unusually tall content (a long
   wallet fold + 4 chips on a short viewport) would silently clip instead
   of scrolling. */
body.askEmpty #view-assistant.view.on{flex:0 1 auto;min-height:0;overflow:visible}
body.askEmpty .askHomeWrap{flex:0 1 auto;align-items:center;padding:0 24px 22px;overflow-y:visible}
@media(max-width:600px){
  body.askEmpty .askHomeWrap{padding:0 16px 18px}
}
/* === end WS-CLAUDE p3 home state === */

/* === WS-SHELL ===
   Fable Claude-style build spec, passes 5+7: the left sidebar app shell
   that replaces the top nav header sitewide, and its <900px off-canvas
   drawer (build spec passim; mockups/claude-style-home.html §2.5/§7 of
   FABLE-CLAUDE-STYLE-SPEC.md). New DOM: .appShell (flex row) wraps
   .sidebar + #menuScrim + .appMain; .appMain wraps header.nav (now
   mobile-only), #viewsScroll (which wraps every existing
   #view-* container -- the sitewide <footer> that used to sit here too
   was removed, WS-NOFOOTER, as redundant with this sidebar nav), and
   #dock -- none of the #view-*
   markup changed, they were only re-parented one level deeper
   (see index.html). The sitewide "Beta" banner that used to sit here as
   its own full-width row was retired in favor of a small .sidebarBetaPill
   next to the logo in .sidebarBrand -- same signal, without permanently
   eating a row of vertical space above #viewsScroll on every route.
   WS-THEME update (chat-first spec v2 pass 1): the sidebar used to be a
   fixed-dark frame "independent of body.theme-ask-dark" regardless of
   route. It no longer is -- --shell-* are sitewide :root aliases (top of
   file) onto the same semantic tokens as the rest of the app, so the
   sidebar now follows the active [data-theme] too: one coherent theme,
   chrome included, not a permanently-dark frame around a toggleable page.
   WS-DOCK update (chat-first spec v2 pass 2): .appShell/.appMain are now
   UNCONDITIONALLY sized to the viewport (previously this only kicked in
   on the assistant view, via a body.viewIsAssistant class) so the same
   "one internal scroll region + a pinned bottom dock, never a double
   page-scroll" structure applies on every route, not just #/ask. */
/* WS-SHELL-SCOPE, 2026-07-26 (CHAIN-STATUS.md "THE SHELL SPEC"): the
   viewport-height, single-internal-scroll-region shell is now SCOPED BACK
   to body.viewIsAssistant only, restoring the boundary the comment above
   used to describe before WS-DOCK pass 2 made it unconditional. #/ask still
   needs the composer pinned above one scrolling region, surviving the iOS
   keyboard via 100dvh. Every other route now scrolls the DOCUMENT: no fixed
   viewport height, no #viewsScroll overflow, so full-page screenshots,
   browser-chrome collapse, scroll restoration, in-page find and print all
   work again, and the #dock reserved-space strip only exists where #dock's
   pill still exists (assistant route / askWidgetMode, see ASK-WIDGET below). */
.appShell{display:flex;align-items:stretch}
.skipLink{position:absolute;left:-9999px;top:12px;z-index:1000;display:flex;align-items:center;min-height:44px;padding:0 16px;border-radius:8px;background:var(--accent);color:var(--accent-text);font-weight:700;text-decoration:none}
.skipLink:focus-visible{left:12px;outline:3px solid var(--ink);outline-offset:2px}
.appMain{flex:1;min-width:0;display:flex;flex-direction:column}
.viewsScroll{display:flex;flex-direction:column;
  min-height:calc(100vh - var(--topnav-h));min-height:calc(100dvh - var(--topnav-h))}
.viewsScroll > .view.on{flex:1 0 auto}
body.ppOnboarding .viewsScroll{min-height:100vh;min-height:100dvh}

body.viewIsAssistant .appShell{height:100vh;height:100dvh;overflow:hidden}
body.viewIsAssistant .appMain{height:100%;overflow:hidden}
body.viewIsAssistant .viewsScroll{flex:1;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch}

/* === WS-DOCK ===: the persistent bottom dock (Fable chat-first spec v2
   §1.1/§5 pass 2, Decision 4) -- the LAST child of .appMain (after
   #viewsScroll, see the rule above), flex:0 0 auto so it never grows or
   shrinks in the outer flex column and always keeps its natural height.
   NORMAL FLOW, never position:fixed -- it's a flex sibling of the one
   scroll region, not something floating over it, so it can never overlap
   an answer card (the shell harness's own invariant). .dockInner reuses
   the sitewide .wrap max-width/centering convention (see .wrap above);
   its own padding carries the vertical rhythm + the safe-area inset for
   the home-bar on notched phones. .appShell/.appMain are sized off
   100dvh (see above), which is what keeps this pinned-bottom dock clear
   of the on-screen mobile keyboard (PR #79) without any extra JS -- the
   dvh unit itself shrinks with the keyboard on modern engines, the same
   fix that already keeps a focused .balInput visible (see the focusin
   listener in app.js) working here for free. */
/* WS-ASKEMPTY (item 1): the contrasted full-width band behind #dock is
   gone -- #dock itself is fully transparent now, in both themes, so only
   the rounded .composer box (its own background/border/shadow, unchanged)
   floats on the page background, Claude/ChatGPT-style. Nothing else about
   #dock changes: still flex:0 0 auto, still normal flow, never
   position:fixed (see the WS-DOCK comment above). */
.dock{flex:0 0 auto;background:transparent;border-top:none;box-shadow:none}
/* PP #30 refinement (backlog P0, contained composer): .dockInner used to
   rely on the shared .wrap class purely for its --maxw(1080px) centering,
   which made #composer stretch to a 1080px-wide bar --
   read as a full-width bar cutting across the screen instead of a
   contained Claude/ChatGPT-style box. Overriding max-width here (same
   selector specificity as .wrap, later in the cascade, so it wins) caps
   the whole dock content column -- composer, follow-up chips, and the
   balance line -- at --chatcol instead,
   so they all render as one aligned, contained column matching
   .chatHistoryInner/.askHomeCenter below. Below --chatcol (tablet/mobile)
   this has no effect -- the column just shrinks with the viewport inside
   its existing padding, same "near-full-width with side padding" behavior
   as before. */
.dockInner{max-width:var(--chatcol);margin:0 auto;padding:10px 20px calc(10px + env(safe-area-inset-bottom))}
@media(max-width:600px){
  .dockInner{padding:8px 14px calc(8px + env(safe-area-inset-bottom))}
}
/* === end WS-DOCK === */

/* structured-model move cards + pill + nextstep + inline followups (§2.4,
   Decision 1) -- built by renderStructuredAnswer()/moveHTML() in chat.js,
   inside a normal .bubble.ai.assistantCard so the enclosing card look is
   unchanged in the chat thread. */
.bottomline{font-size:15px;font-weight:600;color:var(--text-1);line-height:1.5;margin:2px 0 8px}
.move{border-top:1px solid var(--line);margin-top:13px;padding-top:12px}
.move-head{display:flex;align-items:baseline;gap:9px;flex-wrap:wrap}
.move-num{color:var(--accent-text);font-weight:700;font-size:13px}
.move-title{font-weight:600;font-size:13.5px;color:var(--text-1)}
.explore-link{margin-left:auto;font-size:12px;color:var(--text-3);text-decoration:none;white-space:nowrap;cursor:pointer}
.explore-link:hover{color:var(--accent-text)}
.move-body{margin-top:5px;color:var(--text-2);font-size:13px}
.moveReceipts{margin-top:7px;display:flex;gap:6px;flex-wrap:wrap}
.nextstep{margin-top:13px;color:var(--text-2);font-size:13.5px}
.pillCard{background:var(--bg-surface);border:1px solid var(--line);border-top:2px solid var(--accent);
  border-radius:14px;padding:13px 17px;font-size:13px;line-height:1.55;color:var(--text-2);margin-top:13px}
.pillCard .pill-label{display:flex;align-items:center;gap:6px;font-size:10.5px;letter-spacing:.09em;
  text-transform:uppercase;color:var(--text-3);margin-bottom:6px}
.pillCard .pill-label .star{color:var(--accent);font-size:11px}
.followrow{display:flex;gap:8px;flex-wrap:wrap;margin-top:13px}
/* === end PASS4-PANEL === */

/* === WS-OFFTOPIC (backlog #35) ===: the off-topic decline card
   (declineCardHTML() in chat.js). Deliberately calm/neutral -- a
   text-color accent bar (matches --text-3, not --danger/--warn) rather
   than the amber/red tone used for genuine problem states elsewhere in
   this file, since declining an out-of-scope question is a normal,
   expected outcome, not an error. Lives inside the same
   .bubble.ai.assistantCard wrapper as every other answer card. */
.declineCard{background:var(--bg-surface);border:1px solid var(--line);border-top:2px solid var(--text-3);
  border-radius:14px;padding:14px 17px;font-size:13.5px;line-height:1.55;color:var(--text-2)}
.declinePromptBox{color:var(--text-1)}
/* === end WS-OFFTOPIC === */

/* === WS-MDFORMAT ===: clean numbered list for a bottomline string that
   stuffed its own "PART ONE / PART TWO / PART THREE" outline into one
   free-form string (chat.js splitPartLabeledSections/formatAnswerBodyHtml)
   -- same card/answer typography as .move above, just a lighter-weight
   list shape since there's no title/link/receipts row to align. */
.answerParts{list-style:none;margin:10px 0 0;padding:0;counter-reset:answerPart}
.answerParts li{border-top:1px solid var(--line);margin-top:10px;padding-top:10px;
  color:var(--text-2);font-size:13.5px;line-height:1.55;counter-increment:answerPart}
.answerParts li:first-child{border-top:0;margin-top:0;padding-top:0}
.answerParts li::before{content:counter(answerPart)".";font-weight:700;color:var(--accent-text);margin-right:6px}
/* === end WS-MDFORMAT === */

.sidebar{width:var(--shell-w);flex:none;background:var(--shell-bg);border-right:1px solid var(--shell-line);
  display:flex;flex-direction:column;padding:14px 10px 10px;position:sticky;top:0;height:100vh;height:100dvh;
  overflow-y:auto;z-index:30}
.sidebarBrand{display:flex;align-items:center;padding:6px 10px 16px;cursor:pointer;flex:none}
.sidebarBrandImg{height:22px;display:block}
/* PP #26: the standalone "+ New chat" button (.sidebarNewChat) that used
   to sit here, between the brand and the nav, is gone -- redundant with
   the "Ask AI" nav link directly below. .sidebarBrand's own 16px bottom
   padding already gives the nav clean breathing room without it. */
.sidebarNav{display:flex;flex-direction:column;gap:1px;flex:1;min-height:0}
/* Mobile audit P1 #10 (2026-07-19): 36px min-height put the drawer's 7
   primary nav rows (Ask P&P AI, Your Wallet, Programs, Points Strategy,
   Elite Status, Methodology, Updates) under the 44px floor on the site's
   main navigation once the off-canvas drawer is open. Bumped to 44px —
   these are full-width rows with plenty of room, so this is a plain
   height increase, not a hit-slop trick. */
.sidebarNav a{display:flex;align-items:center;gap:10px;padding:8.5px 12px;border-radius:8px;
  font-size:13.5px;font-weight:600;color:var(--shell-ink-dim);text-decoration:none;cursor:pointer;
  transition:background .15s,color .15s;min-height:44px}
.sidebarNav a:hover{background:var(--shell-surface);color:var(--shell-ink)}
.sidebarNav a.active{background:var(--shell-surface);color:var(--shell-ink)}
.sidebarNav .navIc{width:16px;height:16px;flex:none;color:var(--shell-muted)}
.sidebarNav a.active .navIc{color:var(--shell-gold)}
/* === PASS6-RECENTS (Fable v2 §5 row 6) ===: the Ask-AI recents sub-list --
   "New chat" action, up to 5 recents (scrollable), "See all chats →".
   Indented under the "Ask AI" row above it (34px ~= icon width + gap) so it
   reads as a nested sub-list, not a sibling nav item. Every row is a plain
   <button>/<a> styled to match the sidebar's own token language (shell-*
   tokens, same as .sidebarNav a). */
.sidebarSubNav{display:flex;flex-direction:column;gap:2px;margin:2px 4px 8px 34px}
.sidebarSubNew{display:flex;align-items:center;gap:6px;background:none;border:none;
  color:var(--shell-gold);font-size:12.5px;font-weight:700;padding:6px 6px;border-radius:6px;
  cursor:pointer;text-align:left;font-family:var(--font);min-height:44px}
.sidebarSubNew:hover{background:var(--shell-surface)}
.sidebarSubNewIc{width:13px;height:13px;flex:none}
.sidebarSubRecents{display:flex;flex-direction:column;gap:1px;max-height:168px;overflow-y:auto}
.sidebarSubItem{display:flex;flex-direction:column;gap:1px;background:none;border:none;
  text-align:left;padding:5px 6px;border-radius:6px;cursor:pointer;color:var(--shell-ink-dim);
  width:100%;font-family:var(--font);min-height:44px}
.sidebarSubItem:hover{background:var(--shell-surface);color:var(--shell-ink)}
.sidebarSubTitle{font-size:12.5px;font-weight:650;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sidebarSubSnippet{font-size:11px;color:var(--shell-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sidebarSubEmpty{font-size:11.5px;color:var(--shell-muted);padding:5px 6px;line-height:1.4}
.sidebarSubAll{display:flex;align-items:center;font-size:12px;font-weight:700;color:var(--shell-gold);
  padding:6px 6px;min-height:44px;cursor:pointer;text-decoration:none}
.sidebarSubAll:hover{text-decoration:underline}
/* === WS-THEME toggle ===
   WS-UIPOLISH: was a plain nav row (moon icon + "Dark mode" text) --
   ambiguous, since a single static icon+label doesn't show there are
   TWO modes or which one is active. Now a compact, contrasted flip
   switch: both a sun AND a moon icon render at once inside
   .themeToggleTrack, with a sliding .themeToggleThumb behind whichever
   is active. Active side gets var(--accent-soft) thumb fill +
   var(--accent-text) icon color; the inactive icon stays var(--text-3)
   (muted) -- so which mode is on is obvious without reading text. Still
   lives as its own row inside .sidebarNav, directly below "Updates",
   and stays a <button role="switch"> (not an <a>/[data-nav]) since it's
   an action, not a route -- deliberately excluded from the
   [data-nav].active routing highlight applied elsewhere in app.js.
   renderThemeToggle() in app.js fills the track/thumb/icon markup and
   aria-checked/aria-label on load and after every toggle; the actual
   slide animation is pure CSS (transform on data-active), guarded by
   prefers-reduced-motion below. */
.sidebarFooter{border-top:1px solid var(--shell-line);margin-top:10px;padding:10px 10px 4px}
/* Send feedback (backlog #85/#86 + Jeremy's note): its own row at the top
   of the footer, above the account row — matches .sidebarNav a's token
   language (shell-*) but isn't a route link, same reasoning as
   .themeToggleBtn above it not getting the [data-nav].active treatment. */
.sidebarFeedbackBtn{display:flex;align-items:center;gap:8px;width:100%;padding:7px 8px;margin:0 0 6px;
  border-radius:8px;border:none;background:none;color:var(--shell-ink-dim);font-size:12.5px;font-weight:650;
  cursor:pointer;text-align:left;font-family:var(--font);min-height:44px;transition:background .15s,color .15s}
.sidebarFeedbackBtn:hover,.sidebarFeedbackBtn:focus-visible{background:var(--shell-surface);color:var(--shell-ink)}
.sidebarFeedbackBtn svg{width:14px;height:14px;flex:none;color:var(--shell-muted)}
.sidebarAccount{display:flex;align-items:center;gap:10px;min-width:0;min-height:44px}
/* backlog #77: the signed-in account row is now a <button> (see
   renderSidebarAccount, ui-shared.js) that opens #/profile -- reset button
   chrome and reproduce the exact flex layout .sidebarAccount had directly
   on the wrap div before, plus a hover/focus affordance so it reads as
   clickable like every other sidebar control. */
.sidebarAccountBtn{display:flex;align-items:center;gap:10px;min-width:0;width:100%;
  background:none;border:0;padding:6px;margin:-6px;border-radius:10px;text-align:left;
  font:inherit;color:inherit;cursor:pointer;transition:background .15s}
.sidebarAccountBtn:hover,.sidebarAccountBtn:focus-visible{background:var(--shell-surface)}
/* PP #29: was stretching full sidebar width (a flex-column nav item with
   the default align-items:stretch, so width:auto alone still filled the
   cross axis) -- the hover/active/focus highlight spanned the entire
   sidebar even though only the small sun/moon pill is interactive-looking.
   align-self:flex-start + width:fit-content shrink the actual click target
   (and its hover/focus background) down to hug the track, matching how a
   toggle control should read. role=switch, the slide animation, keyboard
   operability, and the tooltip (title/aria-label) are all set in JS
   (renderThemeToggle, ui-shared.js) and untouched by this. */
.sidebarNav .themeToggleBtn{display:inline-flex;align-items:center;align-self:flex-start;width:fit-content;padding:8.5px 12px;border-radius:8px;
  cursor:pointer;transition:background .15s;min-height:44px;border:none;background:transparent;
  font-family:var(--font)}
.sidebarNav .themeToggleBtn:hover{background:var(--shell-surface)}
.themeToggleTrack{position:relative;display:inline-flex;align-items:center;box-sizing:border-box;
  width:52px;height:26px;border-radius:999px;background:var(--bg-canvas);
  border:1px solid var(--line-strong);padding:2px;flex:none;transition:border-color .15s}
.sidebarNav .themeToggleBtn:hover .themeToggleTrack{border-color:var(--accent)}
.themeToggleThumb{position:absolute;top:1px;left:1px;width:22px;height:22px;border-radius:50%;
  background:var(--accent-soft);transition:transform .2s ease}
.themeToggleTrack[data-active="moon"] .themeToggleThumb{transform:translateX(26px)}
@media (prefers-reduced-motion:reduce){ .themeToggleThumb{transition:none} }
.themeToggleIcon{position:relative;z-index:1;display:flex;align-items:center;justify-content:center;
  width:22px;height:22px;color:var(--text-3)}
.themeToggleIcon svg{width:13px;height:13px}
.themeToggleIcon.is-active{color:var(--accent-text)}
/* First-visit theme-toggle hint (2026-07-22): light is the product default,
   so a no-preference visitor gets a brief, subtle glow pulse on the toggle to
   discover the dark/light switch. Added/removed by maybeShowThemeHint()
   (router.js): once ever, gated on localStorage. The ring uses the theme-aware
   --accent token so it reads correctly in BOTH light and dark. The pulse rides
   on the .themeToggleTrack pill; three gentle iterations, then JS clears the
   class. prefers-reduced-motion disables the animation here (and the JS also
   skips adding it), so reduced-motion users see no movement at all. */
@keyframes themeHintPulse{
  0%{box-shadow:0 0 0 0 var(--accent-soft)}
  70%{box-shadow:0 0 0 7px transparent}
  100%{box-shadow:0 0 0 0 transparent}
}
.themeToggleBtn.themeHintPulse .themeToggleTrack{animation:themeHintPulse 1.15s ease-out 3;border-color:var(--accent)}
@media (prefers-reduced-motion:reduce){ .themeToggleBtn.themeHintPulse .themeToggleTrack{animation:none} }
.sidebarAvatar{width:30px;height:30px;border-radius:50%;background:linear-gradient(135deg,#2c3e50,#14202b);
  border:1px solid var(--shell-gold);color:var(--shell-gold);font-size:11.5px;font-weight:700;
  display:flex;align-items:center;justify-content:center;flex:none}
.sidebarWho{min-width:0}
.sidebarName{font-size:13px;color:var(--shell-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sidebarPlan{font-size:11px;color:var(--shell-faint)}
.sidebarAccount.sidebarSignIn{display:block}
.sidebarSignInBtn{width:100%;padding:10px 12px;border-radius:10px;border:1px solid var(--shell-line-strong);
  background:transparent;color:var(--shell-ink);font-size:13.5px;font-weight:650;cursor:pointer;
  font-family:var(--font);min-height:44px;transition:background .15s,border-color .15s}
.sidebarSignInBtn:hover{background:var(--shell-surface);border-color:var(--shell-gold)}

/* Theme-aware logo swap: sitewide (sidebar brand, mobile topbar, footer)
   -- assets/logo_full.svg (dark ink) for light theme, logo_full_dark
   mode_transparent.svg (light ink) for dark theme. Both <img> render;
   CSS shows only the one matching [data-theme]. */
.themeLogoDark{display:none}
:root[data-theme="dark"] .themeLogoLight{display:none}
:root[data-theme="dark"] .themeLogoDark{display:block}
/* === end WS-THEME toggle === */

/* === WS-ADMINWALLET ===: small "Admin" badge next to the sidebar profile
   name for admin identities (see isAdminEmail in app.js). .sidebarNameRow
   wraps the existing .sidebarName (unchanged) + the new badge so the name
   still ellipsis-truncates on its own and the badge never gets squeezed
   or clipped off. */
.sidebarNameRow{display:flex;align-items:center;gap:6px;min-width:0}
.sidebarNameRow .sidebarName{flex:1;min-width:0}
.sidebarAdminBadge{flex:none;display:inline-block;font-size:9.5px;font-weight:750;letter-spacing:.03em;
  text-transform:uppercase;padding:2px 6px;border-radius:999px;background:var(--shell-gold);color:var(--accent-on)}

/* WS-DOCK (pass 2): .appShell/.appMain are unconditionally sized to the
   viewport now (see those rules further up) -- the body.viewIsAssistant-
   scoped duplicate of this that used to live here is retired, it was
   fully subsumed once that sizing stopped being assistant-view-only. */

/* Desktop: sidebar is a static, always-visible column; header.nav (now
   just logo + hamburger, see index.html) is never shown. Mobile: sidebar
   becomes an off-canvas drawer behind the hamburger, header.nav is the
   slim topbar. Breakpoint matches the shell brief's "<900px" mobile spec. */
/* REDESIGN-B PASS 1: header.nav is the sitewide topnav now (was mobile-
   only under the sidebar shell) — never hidden at any width. The sidebar
   is Ask-AI-only: on desktop it renders ONLY while body.viewIsAssistant
   (the chats column); below 900px it stays the off-canvas drawer, opened
   by the topbar "Chats" button on the assistant route. */
@media(min-width:900px){
  #menuScrim{display:none!important}
  .sidebar{display:none}
  body.viewIsAssistant .sidebar{display:flex}
}
@media(max-width:899px){
  .sidebar{position:fixed;top:0;left:0;bottom:0;height:100vh;height:100dvh;transform:translateX(-100%);
    transition:transform .22s cubic-bezier(.2,.7,.3,1);box-shadow:0 0 40px rgba(0,0,0,.4)}
  .sidebar.mobileOpen{transform:translateX(0)}
  .menuBtn{display:inline-flex}
  header.nav{position:sticky;top:0;z-index:20}
}
@media(max-width:899px) and (prefers-reduced-motion:reduce){
  .sidebar{transition:none}
}
/* === end WS-SHELL === */

/* Collapsible sidebar (#6). The driver toggles body.sidebarCollapsed and adds
   a .sidebarCollapseBtn chevron into the sidebar header. Collapsed, the
   sidebar narrows to nothing and .appMain (flex:1) reclaims the width with a
   smooth transition; the transition is scoped to >=900px so it never clobbers
   the <900px off-canvas drawer's transform animation (defined above). The
   button styling itself is width-agnostic. Reduced motion drops the animation.
   Note for the driver: at width:0 the in-header button is clipped, so the
   expand affordance must live outside the collapsed sidebar (e.g. the topnav). */
.sidebarCollapseBtn{display:inline-flex;align-items:center;justify-content:center;flex:none;
  width:26px;height:26px;margin-left:auto;border:none;border-radius:7px;background:none;cursor:pointer;
  color:var(--shell-muted);transition:background .12s,color .12s,transform .2s}
.sidebarCollapseBtn:hover,.sidebarCollapseBtn:focus-visible{background:var(--shell-surface);color:var(--shell-ink)}
.sidebarCollapseBtn svg{width:16px;height:16px}
@media(min-width:900px){
  .sidebar{transition:width .2s cubic-bezier(.2,.7,.3,1)}
  body.sidebarCollapsed .sidebar{width:0;padding-left:0;padding-right:0;border-right:0;overflow:hidden}
  body.sidebarCollapsed .sidebarCollapseBtn{transform:rotate(180deg)}
}
/* Expand affordance: hidden except when the sidebar is collapsed on the
   assistant route at desktop width (the collapsed sidebar is width:0, so its
   in-header chevron is clipped — this floating button brings it back). */
.sidebarExpandBtn{display:none;align-items:center;justify-content:center;width:34px;height:34px;
  border:1px solid var(--line);border-radius:9px;background:var(--bg-surface);color:var(--text-2);cursor:pointer}
.sidebarExpandBtn:hover{border-color:var(--accent);color:var(--text-1)}
.sidebarExpandBtn svg{width:18px;height:18px}
@media(min-width:900px){
  body.viewIsAssistant.sidebarCollapsed .sidebarExpandBtn{display:inline-flex;position:fixed;left:10px;top:64px;z-index:40}
}
@media(prefers-reduced-motion:reduce){
  .sidebar{transition:none}
  .sidebarCollapseBtn{transition:none}
}

/* === WS-RECEIPTS ===
   Tappable-provenance affordance for structured/verified numbers (transfer
   ratios, program "Typical value", wallet per-program value) + the receipt
   sheet itself (#receiptScrim, cloned from #detailScrim's .scrim/.modal
   pattern) + the Methodology page's prose. Every .receipt-trig is a real
   <button> so Enter/Space activation and focus are native, not re-built. */
.receipt-trig{
  display:inline-flex;align-items:center;
  background:none;border:0;margin:0;padding:0 2px;
  font:inherit;font-weight:inherit;color:inherit;cursor:pointer;
  border-bottom:1px dotted var(--ink2);
  border-radius:3px;
}
.receipt-trig::after{content:"ⓘ";font-size:.72em;color:var(--ink2);margin-left:3px;vertical-align:2px}
.receipt-trig:hover{border-bottom-color:var(--ink)}
.receipt-trig:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* Row context (transfer-partner sub-line, hub table value cell): the
   trigger inherits that row's existing text size/color rather than forcing
   its own, so it reads as "the same number, now tappable" not a new
   element competing with the row. */
.row .receipt-trig{font-size:inherit;color:inherit}
/* .hubRowVal already carries its own bold/brand-ink look (styles.css ~561);
   restate it explicitly here (not "inherit") so this combined selector's
   higher specificity doesn't flatten that look back down to plain body
   text the way an "inherit" would win the cascade over the older rule. */
.hubRowVal.receipt-trig{font-size:14px;font-weight:750;color:var(--brand-ink);width:100%}
@media(max-width:600px),(pointer:coarse){
  .receipt-trig{min-height:44px;padding:0 6px}
  /* Brief #61 item 6: summary.srcnote is a real tap target now (opens the
     citations list), not decorative text -- same 44px floor as every other
     tappable receipt affordance. */
  summary.srcnote{min-height:44px;display:flex;align-items:center}
}
#receiptScrim .modal{max-height:82vh;overflow:auto}
.receiptSrc,.receiptAsOf{font-size:13.5px;color:var(--ink2);margin:6px 0}
.receiptSrc a{color:var(--brand-ink)}
.receiptChanges{margin-top:14px;padding-top:10px;border-top:1px solid var(--line)}
.receiptChanges h4{font-size:12px;margin:0 0 8px;color:var(--ink2);text-transform:uppercase;letter-spacing:.03em;font-weight:700}
.receiptChangeItem{font-size:13.5px;padding:8px 0;border-bottom:1px solid var(--line)}
.receiptChangeItem:last-child{border-bottom:0}
.receiptChangeDate{color:var(--ink2);font-size:12px;display:block;margin-bottom:2px}
/* Methodology page prose */
#methodologyWrap h3{font-size:16px}
/* #17 content max-width: cap methodology prose to the house ~720px readable
   measure (same as .exIntro/.stratSecSub) so long lines wrap earlier on wide
   monitors. Headings/lists that aren't running prose keep their own flow. */
#methodologyWrap p{margin:0 0 10px;max-width:720px}
#methodologyWrap ul{font-size:14px;max-width:720px}
/* === end WS-RECEIPTS === */

/* === WS-PILLS ===
   Knowledge pills (Fable §D2 visual spec): white card, 1px border, a 2px
   GOLD TOP border — deliberately different from the Instant Read's gold
   LEFT border and from plain assistant chat cards, so a pill never reads
   as "the answer" or "the computed report," only as the taught-you-
   something reward. Lives in three slots (chat answer card, wallet page,
   home hero) that all share this one component + the pickPill rule
   library in app.js. */
.knowledgePill{
  background:var(--bg-surface);
  border:1px solid var(--line);
  border-top:2px solid var(--accent);
  border-radius:var(--radius);
  padding:14px 16px;
  margin-top:14px;
}
.knowledgePillHead{display:flex;align-items:center;gap:6px;margin-bottom:6px}
.knowledgePillStar{color:var(--accent);font-size:13px;line-height:1}
.knowledgePillLabel{font-size:11px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--ink2)}
.knowledgePillText{margin:0;font-size:15px;line-height:1.5;color:var(--ink)}
/* The receipt chip inside a pill — gold-tinted per §D2 ("[1:1 ✓]"), a
   distinct look from the plain dotted-underline .receipt-trig used
   elsewhere, so the ratio/valuation token reads as part of the pill's
   own reward styling rather than a generic inline citation. */
.pillChip{
  display:inline-flex;align-items:center;
  background:var(--accent-soft);color:var(--accent-text);
  border:1px solid var(--accent);border-radius:999px;
  padding:2px 9px;margin:0 2px;
  font:inherit;font-size:12.5px;font-weight:750;
  cursor:pointer;white-space:nowrap;
}
.pillChip:hover{border-color:var(--accent)}
.pillChip:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.pillLink{margin:8px 0 0;font-size:13px}
.pillLink a{color:var(--brand-ink);font-weight:650;cursor:pointer}
/* Chat slot: sits inside the assistant card, below the footer row, so it
   reads as "below the answer" (Fable's placement) and never overlaps the
   follow-up chip row, which lives outside #chat entirely in its own strip
   above the composer. */
.assistantCard .knowledgePill{margin-top:14px}
/* Wallet slot (§D4.2): one pill under the table, above the "what do you
   want to do" action grid. */
.hubSummary + .knowledgePill{margin-top:16px}
/* Home slot (§D4.3): centered, page-width-capped, signed-in only. */
.homePillWrap{max-width:640px;margin:18px auto 0}
.homePillWrap[hidden]{display:none}
/* WS-UIPOLISH: the home pill read as too big/heavy for a small hint --
   shrink ONLY this slot (padding, border weight, label/heading/text size,
   margins) rather than the shared .knowledgePill rules above, which the
   chat-answer and wallet-page slots still use unchanged. Tokens only. */
.homePillWrap .knowledgePill{padding:10px 12px;border-top-width:2px;margin-top:12px}
.homePillWrap .knowledgePillHead{margin-bottom:4px;gap:5px}
.homePillWrap .knowledgePillStar{font-size:11px}
.homePillWrap .knowledgePillLabel{font-size:10px}
.homePillWrap .knowledgePillText{font-size:13px;line-height:1.4}
.homePillWrap .pillLink{margin:5px 0 0;font-size:12px}
@media(max-width:600px),(pointer:coarse){
  .pillChip{min-height:32px}
}
/* === end WS-PILLS === */

/* === WS-RECEIPTS-AI ===
   Inline variant of .receipt-trig for a verified number matched INSIDE a
   rendered AI answer paragraph (see annotateAnswerHtml in app.js). Reuses
   the exact same trigger markup/handler/sheet as every other receipt on
   the site — this block only tunes it to sit naturally inline in a
   sentence of running prose instead of a row/table cell. */
.receipt-inline{color:inherit;font:inherit;font-weight:inherit;line-height:inherit;vertical-align:baseline}
@media(max-width:600px),(pointer:coarse){
  .receipt-inline{padding:2px 6px;margin:-2px -2px;min-height:44px}
}
/* === end WS-RECEIPTS-AI === */

/* === WS-WALLET2 === */
/* Wallet v2: inline row edit, remove-with-confirm+undo, CSS card-art chips,
   per-row freshness. Card art is CSS-only -- no real card images (rights
   risk) -- built at render time (app.js: cardArtForCard/progArt) from the
   same verified CCOLORS/ALLYHEX values used elsewhere, plus a small named-
   tier override list for the handful of cards worth a distinct look (gold
   Amex, platinum Amex, navy Sapphire, near-black Venture X). Used in both
   wallet rows and both add-search pickers -- one component, four surfaces
   (minus the interview, which isn't part of this pass). */
.wsCardChip{position:relative;display:inline-flex;align-items:flex-end;justify-content:flex-start;width:40px;height:26px;border-radius:5px;background:var(--wschip-bg,var(--brand));flex:none;overflow:hidden;box-shadow:0 1px 2px rgba(20,32,43,.28),inset 0 0 0 1px rgba(255,255,255,.14)}
.wsCardChip::before{content:"";position:absolute;top:4px;left:4px;width:8px;height:6px;border-radius:2px;background:linear-gradient(135deg,#efe0ab,#c7ad5f)}
.wsCardChip::after{content:"";position:absolute;right:-7px;bottom:-9px;width:20px;height:20px;border-radius:50%;background:radial-gradient(circle at 30% 30%,rgba(255,255,255,.5),rgba(255,255,255,0) 70%)}
.wsCardChipWordmark{position:relative;z-index:1;font-size:6px;font-weight:800;letter-spacing:.02em;color:var(--wschip-fg,#fff);opacity:.92;padding:0 3px 3px 0;text-transform:uppercase;margin-left:auto}
.wsCardChip--pick{width:46px;height:29px}
/* Airline/hotel program rows get a smaller plain swatch (alliance color,
   ALLYHEX) instead of the full card-shaped chip -- a program isn't a card,
   so it doesn't get the EMV-notch/wave treatment, just a colored tag next
   to the existing brand logo (kept, unchanged). */
.wsProgChip{width:15px;height:15px;border-radius:5px;background:var(--wschip-bg,var(--brand));flex:none;box-shadow:inset 0 0 0 1px rgba(255,255,255,.3)}
.wArt .wsCardChip,.wArt .wsProgChip,.uniArt .wsCardChip,.uniArt .wsProgChip{margin-right:0}

/* Item 4: "Updated Nm ago" -- omitted entirely (no placeholder dash) when
   the row has never been touched since Wallet v2 shipped. Past 90 days, a
   quiet "Still right?" link opens the same tap-to-edit path as the balance
   itself, instead of a nag banner. */
.hubRowUpdated{font-size:12px;margin-top:2px}
.wsStaleLink{border:0;background:none;padding:0;color:var(--brand-ink);font-weight:700;font-size:12px;cursor:pointer;text-decoration:underline}

/* Item 1: tap-a-balance-to-edit-in-place -- styled as plain text until
   tapped so it doesn't read as a form control up front, but still clears
   the 44px touch-target floor the rest of the wallet holds itself to. */
.hubBalTap{border:0;background:none;padding:8px 4px;min-height:44px;display:inline-flex;align-items:center;gap:5px;font:inherit;font-weight:750;color:var(--ink);cursor:pointer;border-radius:8px}
.hubBalTap:hover,.hubBalTap:focus-visible{background:var(--bg2)}
.hubBalEditIcon{font-size:12px;opacity:.55}

/* Item 2: remove-with-confirm, then undo. Confirmation swaps the row/chip's
   own content in place -- never a modal, matching the rest of the wallet's
   inline philosophy -- and the undo notice reuses that same slot for ~6s
   after the real delete before the row is gone for good. */
.wsRemoveConfirm{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:10px 12px;background:var(--danger-bg);border:1px solid var(--danger-border);border-radius:10px;font-size:13.5px;color:var(--ink)}
.wsRemoveConfirmBtns{display:flex;gap:8px;margin-left:auto}
.wsRemoveBtnDanger{background:var(--bg-surface);color:var(--danger-ink);border:1.5px solid var(--danger-border-strong)}
.wsRemoveBtnDanger:hover{background:var(--danger-bg-hover)}
.wsUndoRow{padding:10px 12px;background:var(--bg2);border-radius:10px;font-size:13.5px;color:var(--ink2)}
.wsUndoBtn{border:0;background:none;padding:0;color:var(--brand-ink);font-weight:750;cursor:pointer;text-decoration:underline;font-size:13.5px}
.hubGhostRow td{padding:8px 8px}

/* Item 3 (picker half): the "Add a credit card" panel was a native
   <select>, which can't host the chip markup -- it's now a type-ahead +
   row list matching the airline/hotel search pattern already next to it. */
.hubAddCardGroup{margin-bottom:10px}
.hubAddCardGroup:last-child{margin-bottom:0}
.hubAddCardGroupLabel{font-size:11.5px;font-weight:750;text-transform:uppercase;letter-spacing:.03em;color:var(--ink2);padding:8px 4px 2px}
.wsCardSearchFooter{margin:10px 4px 2px;font-size:13.5px}
.wsCardSearchFooter a{font-weight:650}
/* === end === */

/* === WS-AUTHGREET === */
/* Personal greeting (Fable §B). #homeGreeting reuses the existing h1/.sub
   type rules (descendant selectors match the h1/p tags regardless of the
   extra wrapper), so this block only needs its own spacing + the receipt
   trigger's in-flow sizing when it lands inside an h1/p instead of a
   citation row. */
.homeGreetingSub .receipt-trig{font-size:inherit;font-weight:700;color:var(--brand-ink)}
.homeGreeting[hidden],#homeHeroDefault[hidden]{display:none}

/* Honest blended-value line under the home greeting (Rec 3) — a quiet
   secondary line stating the wallet's blended cents-per-point average. */
.blendedAvg{margin:6px 0 0;max-width:640px;font-size:12.5px;line-height:1.5;color:var(--text-3)}

/* Auth modal v2 (Fable §E1) — mode toggle, two-column name row, password
   field + reveal toggle, and the friendly-error/action-link states. Reuses
   .fieldLabel/.searchbar/.searchbarLg/.btn/.note from the base sheet; only
   the pieces unique to this modal are defined here. */
.authTabs{display:flex;gap:4px;background:var(--bg2);border:1px solid var(--line);border-radius:999px;padding:4px;margin:14px 0 16px}
.authTab{flex:1;border:0;background:none;border-radius:999px;padding:9px 10px;min-height:44px;font-size:13.5px;font-weight:700;color:var(--ink2);cursor:pointer}
.authTab.on{background:var(--brand);color:#fff;box-shadow:0 2px 8px rgba(20,32,43,.18)}
.authDivider{display:flex;align-items:center;gap:10px;margin:16px 0;font-size:12px;font-weight:700;color:var(--ink2);text-transform:uppercase;letter-spacing:.04em}
.authDivider::before,.authDivider::after{content:"";flex:1;height:1px;background:var(--line)}
.authNameRow{display:flex;gap:12px;margin-bottom:14px}
.authNameRow .authField{flex:1;min-width:0}
.authNameRow .authField .searchbarLg{width:100%}
.authPasswordWrap{position:relative}
.authPasswordWrap input{width:100%;padding-right:46px}
.authEyeBtn{position:absolute;right:6px;top:50%;transform:translateY(-50%);border:0;background:none;font-size:16px;line-height:1;padding:8px;cursor:pointer;color:var(--ink2);min-height:44px;min-width:36px}
/* Mobile audit P1 #8 (2026-07-19): min-height:44px above already clears the
   vertical floor, but min-width:36px leaves the effective tap target 8px
   narrow on the one dimension that matters for a small icon sitting inside
   a text field. Same invisible-hit-slop approach as .matrixNoteTrig
   (::before, absolutely positioned, out of flow) rather than widening the
   visible 36px glyph box itself, which would either overlap the typed
   password text or force the input's padding-right wider than it needs to
   be. -4px each side adds the 8px needed to reach a true 44px-wide target
   without moving or resizing anything visible. */
.authEyeBtn::before{content:'';position:absolute;inset:0 -4px}
.authHelper{font-size:12px;color:var(--ink2);margin:6px 0 14px}
.authFootnote{font-size:13px;margin:12px 0 0;text-align:center}
.authFootnote a{font-weight:650;cursor:pointer}
.authFootnoteRow{display:flex;justify-content:space-between;gap:10px;font-size:13px;margin:12px 0 0;flex-wrap:wrap}
.authFootnoteRow a{font-weight:650;cursor:pointer}
.authErrorNote{color:var(--danger-ink);background:var(--danger-bg);border:1px solid var(--danger-border);border-radius:10px;padding:9px 12px;margin:6px 0 4px}
.authErrorActions{font-size:12.5px;margin:2px 0 10px;display:flex;gap:10px}
.authErrorActions a{font-weight:650;cursor:pointer}
.authPrivacyNote{margin-top:16px;line-height:1.5}
.authNamePrompt{margin:14px 0 6px;padding:14px;background:var(--bg2);border:1px solid var(--line);border-radius:12px}
@media(max-width:480px){
  .authNameRow{flex-direction:column;gap:0}
  .authFootnoteRow{flex-direction:column;gap:4px}
  /* Round-2 audit, P0-02 fix-adjacent: stacking .authFootnoteRow's two links
     with flex-direction:column also stacks the "·" separator between them
     (it's a bare text node, so it's its own flex item, see auth.js) onto its
     own line -- an orphan dot with nothing either side of it. Simplest fix
     that doesn't touch the two-link layout on wider screens: the separator
     is only decorative (aria-hidden, auth.js), so drop it entirely once the
     links are stacked -- a line break already does its job of separating
     them. */
  .authFootnoteRow .authFootnoteSep{display:none}
}
/* end === WS-AUTHGREET === */

/* === WS-INSTANT === */
/* Fable §C — the signed-out Instant Read. A distinct card style, on
   purpose: white background, 3px GOLD LEFT border, its own header badge.
   Deliberately NOT `.bubble`/`.assistantCard` (those are the chat-reply
   shape) and NOT the knowledgePill look either (that one uses a 2px GOLD
   TOP border, per the WS-PILLS comment above) — three visually distinct
   things on this page: an assistant reply, a "worth knowing" pill, and a
   proudly-computed-not-AI snapshot. */
.instantReadWrap{margin:10px 0;max-width:100%}
.instantReadCard{
  background:var(--bg-surface);
  border:1px solid var(--line);
  border-left:3px solid var(--accent);
  border-radius:var(--radius);
  padding:16px 18px;
  box-shadow:var(--shadow);
}
.instantReadBadge{
  display:inline-flex;align-items:center;gap:6px;
  font-size:11.5px;font-weight:800;letter-spacing:.03em;
  color:var(--accent-text);background:var(--accent-soft);
  border:1px solid var(--accent);border-radius:999px;
  padding:4px 11px;margin-bottom:10px;
}
.instantReadSubhead{margin:0 0 14px;font-size:13.5px;color:var(--ink2);line-height:1.5}
.instantReadValueLine{margin:0 0 4px;font-size:15.5px;line-height:1.6}
.instantReadValueLine .receipt-trig{margin-left:2px}
.instantReadGoesWhereLabel{margin:16px 0 6px;font-size:14px}
.instantReadGoesWhere{margin:0;padding-left:20px;font-size:14.5px;line-height:1.6}
.instantReadGoesWhere li{margin-bottom:8px}
.instantReadGoesWhere a{color:var(--brand-ink);font-weight:650;white-space:nowrap}
.instantReadCard .knowledgePill{margin-top:16px}

/* §C3 — the sign-in upsell, directly under the Instant Read. A quieter,
   flat card (no accent border of its own) so the eye still lands on the
   Instant Read's gold edge first; this is the "next step", not a second
   headline. */
.signInUpsellCard{
  background:var(--bg2);
  border:1px solid var(--line);
  border-radius:var(--radius);
  padding:16px 18px;
  margin-top:12px;
  transition:box-shadow .2s;
}
.signInUpsellHead{margin:0 0 8px;font-size:15.5px;line-height:1.4}
.signInUpsellHead em{font-style:italic}
.signInUpsellBody{margin:0 0 14px;font-size:13.5px;color:var(--ink2);line-height:1.5}
.signInUpsellActions{display:flex;flex-direction:column;gap:10px;max-width:340px}
.signInUpsellReassure{margin:12px 0 0;font-size:12px;color:var(--ink2)}
/* §4 — "gently highlights the sign-in card" when a signed-out visitor
   submits another question while the Instant Read is already showing:
   never a dead error, never a second stacked copy, just a couple of quick
   gold pulses on the card that already answers "what do I do next." */
.signInUpsellCard.pulseHighlight{animation:wsInstantPulse 1.4s ease-out}
@keyframes wsInstantPulse{
  0%{box-shadow:0 0 0 0 rgba(212,166,81,.55)}
  40%{box-shadow:0 0 0 6px rgba(212,166,81,0)}
  100%{box-shadow:0 0 0 0 rgba(212,166,81,0)}
}
@media(prefers-reduced-motion:reduce){ .signInUpsellCard.pulseHighlight{animation:none} }
@media(max-width:600px),(pointer:coarse){
  .instantReadCard,.signInUpsellCard{padding:14px 14px}
}
/* === end === */

/* === WS-INTERVIEW ===
   The onboarding interview (Fable §A) — "Two minutes, seven questions."
   Reuses the site's one existing .scrim/.modal shell (same as #saveScrim/
   #detailScrim/#receiptScrim) so Esc-to-close and the backdrop convention
   come for free; only the interview-specific pieces are defined here.
   Centered card on desktop, a true bottom sheet on mobile per Jeremy's
   point.me reference. */
#interviewScrim .modal.wsIntModal{
  max-width:480px;width:100%;max-height:86vh;overflow:auto;position:relative;padding:28px 26px 24px;
}
.wsIntClose{position:absolute;top:14px;right:16px;font-size:20px}
.wsIntProgress{
  display:inline-flex;align-self:flex-start;
  font-size:11.5px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;
  color:var(--accent-text);background:var(--accent-soft);border:1px solid var(--accent);border-radius:999px;
  padding:5px 12px;margin:2px 30px 14px 0;
}
.wsIntBody h3{margin-bottom:8px}
.wsIntBody{margin-bottom:18px}
/* The interview uses these as short breaks between questions. Keep the fact
   card compact so it does not read like another full onboarding step. */
#interviewScrim .knowledgePill{max-width:400px;margin:0 auto;padding:10px 12px}
#interviewScrim .knowledgePillHead{gap:5px;margin-bottom:3px}
#interviewScrim .knowledgePillStar{font-size:11px}
#interviewScrim .knowledgePillLabel{font-size:10px}
#interviewScrim .knowledgePillText{font-size:13.5px;line-height:1.4}
#interviewScrim .pillChip{font-size:12px;padding:1px 8px}
.wsIntContinue{margin-bottom:10px}
.wsIntSkip{
  display:block;width:100%;text-align:center;border:0;background:none;
  color:var(--ink2);font-size:13.5px;font-weight:650;cursor:pointer;padding:8px;min-height:36px;
}
.wsIntSkip:hover{color:var(--brand-ink);text-decoration:underline}
.wsIntFooter{font-size:12px;margin:14px 0 0;text-align:center}
/* Q0 name row -- mirrors .authNameRow exactly (same two-field pattern). */
.wsIntNameRow{display:flex;gap:12px}
.wsIntNameRow input{flex:1;min-width:0}
/* Single/multi-select choice rows (Q1, Q6, Q7, Q8) -- big tap-friendly rows,
   not tiny native radios/checkboxes, matching the rest of the site's chip
   language. */
.wsIntChoiceList{display:flex;flex-direction:column;gap:8px}
.wsIntChoice{
  display:flex;align-items:center;gap:10px;width:100%;text-align:left;
  border:1px solid var(--line);background:var(--bg-surface);border-radius:12px;
  padding:13px 14px;font:inherit;font-size:14.5px;font-weight:650;color:var(--ink);
  cursor:pointer;min-height:44px;
}
.wsIntChoice:hover{border-color:var(--brand)}
.wsIntChoice.on{background:var(--brand-soft);border-color:var(--brand);color:var(--brand-ink)}
.wsIntCheck{margin-left:auto;color:var(--brand-ink);font-weight:800}
/* Q2 card/program picker -- reuses the WS-WALLET2 card-art chip component
   (cardChipHTML/progChipHTML in app.js) so a card here looks exactly like
   its wallet row, per §C3 ("one component, four surfaces"). */
.wsIntPickList{max-height:260px;overflow:auto;border:1px solid var(--line);border-radius:12px;padding:6px;margin:12px 0}
.wsIntPickGroupLabel{font-size:11.5px;font-weight:750;text-transform:uppercase;letter-spacing:.03em;color:var(--ink2);padding:10px 8px 4px}
.wsIntPickRow{display:flex;align-items:center;gap:10px;padding:10px 8px;border-radius:10px;cursor:pointer;min-height:44px}
.wsIntPickRow:hover{background:var(--bg2)}
.wsIntPickRow.on{background:var(--brand-soft)}
.wsIntSmall{font-size:12.5px;margin:8px 0 0}
/* Q3 balances -- one row per selected program, same numeric-input pattern
   as the hub/wizard (.inlineBalField/.balInput), just laid out for a
   single-column interview card. */
.wsIntBalList{display:flex;flex-direction:column;gap:10px;margin:12px 0}
.wsIntBalRow{display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--line)}
.wsIntBalRow:last-child{border-bottom:0}
/* Q5 destination suggestion chips */
.wsIntGoalChips{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}
.wsIntGoalChip{
  border:1px solid var(--line);background:var(--bg-surface);border-radius:999px;padding:8px 14px;
  font-size:13.5px;font-weight:650;color:var(--ink);cursor:pointer;min-height:36px;
}
.wsIntGoalChip:hover{border-color:var(--brand);color:var(--brand-ink)}
/* Closing card CTAs */
.wsIntClosingBtns{display:flex;gap:10px;flex-wrap:wrap;margin-top:6px}
/* Entry-point CTA/chip (§5) -- shared markup for #homeInterviewChipWrap and
   #walletInterviewChipWrap, populated by renderInterviewChip() in app.js. */
.wsIntChipWrap{max-width:640px;margin:14px auto 0;text-align:center}
.wsIntChipWrap[hidden]{display:none}
.wsIntCta{display:inline-flex}
.wsIntChipRow{display:inline-flex;align-items:center;gap:6px;background:var(--bg2);border:1px solid var(--line);border-radius:999px;padding:4px 4px 4px 14px}
.wsIntFinishChip{border:0;background:none;padding:8px 4px;font:inherit;font-size:13.5px;font-weight:700;color:var(--brand-ink);cursor:pointer;min-height:36px}
.wsIntChipX{border:0;background:none;color:var(--ink2);font-size:16px;line-height:1;padding:8px;cursor:pointer;min-height:36px;min-width:36px;border-radius:999px}
.wsIntChipX:hover{background:var(--bg-surface);color:var(--ink)}
#view-points .wsIntChipWrap{margin:0 0 16px;text-align:left}
#view-points .wsIntChipRow{padding-left:14px}
@media(max-width:600px),(pointer:coarse){
  .wsIntNameRow{flex-direction:column;gap:12px}
  .wsIntChoice,.wsIntPickRow,.wsIntFinishChip,.wsIntChipX,.wsIntSkip,.wsIntGoalChip{min-height:44px}
  #interviewScrim .pillChip{min-height:44px}
  /* Bottom sheet: the modal docks to the bottom edge, full width, rounded
     top corners only -- matches the point.me reference pattern Jeremy
     called out (§A1). */
  #interviewScrim{align-items:flex-end;padding:0}
  #interviewScrim .modal.wsIntModal{max-width:none;border-radius:20px 20px 0 0;max-height:90vh;padding:22px 18px 20px}
}
/* === end WS-INTERVIEW === */

/* === WS-VALUEFIX (Item #7): wallet remove controls, Edit-mode only ===
   Remove ✕'s (hubRemoveCol for air/hotel; the per-card chip ✕ for bank)
   now only render at all once "Edit points & programs" is on -- app.js
   handles that gating. This is just the polish: when a bank row's
   card-chip list has nothing to show (no held-cards, no ✕ in view mode),
   don't let the empty flex wrapper's margin-top still nudge the row --
   same "consistent, aligned" table rhythm whether Edit mode is on or off. */
.hubCardChips:empty{display:none;margin-top:0}
/* === end WS-VALUEFIX === */

/* === PASS6-RECENTS (Fable v2 §5 row 6) ===: #/chats "See all chats" page --
   search box reuses .searchbar/.searchbarLg (same control as #/status'
   program search), banner reuses .banner (same as Elite Status/Updates),
   rows are their own light card matching the ppHero/.panel surface
   language elsewhere on the site. Both themes via the same --bg-surface/
   --line/--text-* tokens every other data page already uses. */
.chatsBucketLabel{font-size:12.5px;font-weight:750;color:var(--text-3);
  text-transform:uppercase;letter-spacing:.03em;margin:20px 0 8px}
.chatsBucketLabel:first-child{margin-top:14px}
.chatsList{display:flex;flex-direction:column;gap:8px}
.chatsRow{display:flex;flex-direction:column;gap:3px;text-align:left;width:100%;
  background:var(--bg-surface);border:1px solid var(--line);border-radius:12px;
  padding:12px 14px;cursor:pointer;transition:border-color .15s,box-shadow .15s;
  font-family:var(--font)}
.chatsRow:hover{border-color:var(--line-strong);box-shadow:0 2px 8px rgba(20,32,43,.08)}
.chatsRowTitle{font-size:14.5px;font-weight:700;color:var(--text-1)}
.chatsRowSnippet{margin:0;font-size:13px;color:var(--text-3);white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}

/* ---- CHAT PURGE (Jeremy 2026-07-25 decision 2) ----
   Chats are kept forever, so #/chats carries the escape hatch. The resting
   state is deliberately quiet — a text button, not a red slab — because it
   sits below a list the visitor scrolls every day and must not read as the
   page's main action. The armed state is the loud one. Both buttons clear
   44px so neither is a mis-tap target on a phone. */
.chatsPurge{margin:26px 0 8px;padding-top:18px;border-top:1px solid var(--line)}
.chatsPurgeStart{background:none;border:0;padding:11px 0;min-height:44px;
  font-family:var(--font);font-size:13.5px;font-weight:650;color:var(--text-3);
  cursor:pointer;text-decoration:underline;text-underline-offset:3px}
.chatsPurgeStart:hover{color:#b42318}
.chatsPurge--armed{background:var(--bg-surface);border:1px solid var(--line-strong);
  border-radius:12px;padding:14px 16px;margin-top:26px}
.chatsPurgeWarn{margin:0 0 12px;font-size:13.5px;line-height:1.5;color:var(--text-2)}
.chatsPurgeWarn b{color:var(--text-1)}
.chatsPurgeBtns{display:flex;flex-wrap:wrap;gap:10px}
.chatsPurgeBtns button{min-height:44px;padding:11px 16px;border-radius:10px;
  font-family:var(--font);font-size:13.5px;font-weight:700;cursor:pointer}
.chatsPurgeCancel{background:var(--bg-surface);color:var(--text-1);border:1px solid var(--line-strong)}
.chatsPurgeGo{background:#b42318;color:#fff;border:1px solid #b42318}
.chatsPurgeGo:hover{background:#912018;border-color:#912018}
.chatsPurgeGo[disabled]{opacity:.6;cursor:default}
.chatsPurgeDone{margin:26px 0 8px;padding-top:18px;border-top:1px solid var(--line);
  font-size:13.5px;color:var(--text-3)}


/* ---- Signed-out demo walkthrough: clear the bottom dock (mobile) ----
   The demo's step CTA ("Continue"/final CTAs) sat UNDER the docked composer
   at 390px — the button rendered at y765-809 while the dock started at y713,
   so the walkthrough looked like a dead end on a phone. The dock is a
   sitewide invariant (verify-ws-dock asserts #dock exists on every route),
   so we pad the demo view to clear it rather than hiding it. */
#view-demo{padding-bottom:220px}
@media(min-width:900px){#view-demo{padding-bottom:140px}}

/* === STRAT-TRUEVALUE (#121, Matt Value Brief §2.6) ===
   Clothes for the true-value Strategy page: earning-power header, equation
   chips, per-row ¢/$1 values, break-even callouts, and the matrix lens
   toggle. Everything uses the existing theme tokens (--ink/--ink2/--line/
   --bg2/--good-text/--accent-*) so dark mode needs no extra rules. */
.stratPower{display:grid;grid-template-columns:1fr 1fr 1.2fr;gap:12px;margin:0 0 20px}
.stratPowerCell{background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--radius);padding:16px}
.stratPowerCell .lbl{font-size:11.5px;font-weight:750;text-transform:uppercase;letter-spacing:.05em;color:var(--ink2)}
.stratPowerCell .num{font-size:25px;font-weight:800;letter-spacing:-.4px;margin-top:4px;color:var(--ink);font-variant-numeric:tabular-nums}
.stratPowerCell .num .unit{font-size:14px;font-weight:600;color:var(--ink2)}
.stratPowerCell.gain{border-color:var(--good)}
.stratPowerCell.gain .num{color:var(--good-text)}
.stratPowerCell .sub{font-size:12.5px;color:var(--ink2);margin-top:4px;line-height:1.45}
.stratPowerNote{grid-column:1/-1;font-size:12.5px;color:var(--ink2);margin:0}
/* category ranking rows */
.stratFee{font-weight:500;color:var(--ink2);font-size:12.5px;white-space:nowrap}
.pill.cashPill{background:var(--bg2);color:var(--ink2);border:1px solid var(--line)}
.stratEq{font-size:12.5px;color:var(--ink2);margin-top:3px;line-height:1.5;font-variant-numeric:tabular-nums}
.stratEq b{color:var(--ink);font-weight:750}
.stratEqNote{color:var(--ink2);font-weight:500}
.row .val.stratValCol{display:flex;flex-direction:column;align-items:flex-end;gap:1px;flex:none}
.stratCpd{font-weight:800;font-size:15px;white-space:nowrap;font-variant-numeric:tabular-nums}
.stratCpd.top{color:var(--good-text)}
.stratYrVal{font-size:12px;font-weight:500;color:var(--ink2);white-space:nowrap;font-variant-numeric:tabular-nums}
.stratSpendNote{margin:0 0 8px;font-size:13px}
/* honesty caveats (typical-redemption framing — brief §2.3, required) */
.stratValueCaveat{font-size:12px;color:var(--ink2);margin:10px 0 0;padding-top:10px;border-top:1px dashed var(--line);line-height:1.5}
.stratHonesty{margin:0 0 16px}
/* fee break-even callout in "cards to consider adding" */
.stratBreakeven{display:flex;gap:10px;align-items:flex-start;background:var(--accent-soft);border:1px solid var(--accent);border-radius:10px;padding:12px 14px;margin:0 0 12px;font-size:13.5px;color:var(--ink);line-height:1.5}
.stratBreakeven b{color:var(--accent-text)}
/* matrix lens toggle (Multiplier ⇄ ¢ per $1) */
.lensSeg{display:inline-flex;border:1px solid var(--line-strong);border-radius:10px;overflow:hidden;flex:none}
/* PASS 3: white-space:nowrap — "$ Value" was wrapping onto two lines at
   phone widths, making the two segment buttons uneven heights. */
.lensBtn{border:0;background:var(--bg-surface);padding:8px 14px;font-family:var(--font);font-size:13px;font-weight:700;color:var(--ink2);cursor:pointer;min-height:40px;white-space:nowrap}
.lensBtn.on{background:var(--accent);color:var(--accent-on)}
.lensBtn:not(.on):hover{background:var(--bg2)}
/* both lens values are always in the DOM; the container class picks one */
.stratMatrix .lensValTrue{display:none}
.stratMatrix.lensTrue .lensValTrue{display:inline}
.stratMatrix.lensTrue .lensValMult{display:none}
.matrixLensNote{display:none;font-size:12.5px;color:var(--ink2);margin:0 0 10px}
.stratMatrix.lensTrue .matrixLensNote{display:block}
/* base ¢/pt column: verified AWVALS context, visibly quieter than rates */
.matrixTable th.matrixBaseCol{font-weight:750;color:var(--ink2);font-size:12px;text-transform:uppercase;letter-spacing:.02em;width:92px}
.matrixTable td.matrixBaseCol{color:var(--ink2);font-variant-numeric:tabular-nums;width:92px}
@media(max-width:760px){
  .stratPower{grid-template-columns:1fr}
  .stratPowerCell{padding:14px}
}
/* === end STRAT-TRUEVALUE #121 === */

/* === Backlog #142: Strategy EARN + USE sections (folded onto #121) === */
/* Three-part page scaffolding (jump nav + numbered section headers), the
   redemption-method value bars (part 2), and the enrollment checklist
   (part 3). Everything flows through the existing semantic tokens; the bar
   colors reuse the established meanings — neutral for the cash floor, gold
   (--accent) for the typical rate, green (--good) for the best transfer,
   matching the matrix's best-in-category green. */
.stratPageNav{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin:0 0 24px}
.stratPageNavBtn{display:flex;align-items:center;gap:10px;background:var(--bg2);border:1px solid var(--line);border-radius:var(--radius);padding:10px 12px;cursor:pointer;text-align:left;font:inherit;color:var(--ink);min-height:52px;transition:border-color .15s ease}
.stratPageNavBtn:hover{border-color:var(--line-strong)}
.stratPageNavLabel{display:flex;flex-direction:column;line-height:1.3;min-width:0}
.stratPageNavLabel b{font-size:13.5px;font-weight:750}
.stratPageNavLabel .muted{font-size:12px}
.stratSecNum{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:50%;background:var(--accent-soft);color:var(--accent-text);font-size:12.5px;font-weight:800}
.stratSection{scroll-margin-top:14px}
.stratSection + .stratSection{border-top:1px solid var(--line);margin-top:30px;padding-top:26px}
.stratSecHead{margin:0 0 14px}
.stratSecKicker{display:inline-flex;align-items:center;gap:8px;font-size:11.5px;font-weight:750;letter-spacing:.07em;text-transform:uppercase;color:var(--accent-text)}
.stratSecTitle{margin:8px 0 0;font-size:19px;color:var(--ink)}
.stratSecSub{margin:4px 0 0;max-width:720px}
.stratEarnAddHead{margin:0 0 6px;font-size:16px;color:var(--ink)}
.stratLead{max-width:760px;font-size:14.5px;line-height:1.6;color:var(--ink2);margin:0 0 16px}
.stratValGrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:12px;margin:0 0 4px}
.stratValCard{background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--radius);padding:14px 16px;box-shadow:var(--shadow);min-width:0}
.stratValHead{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:0 0 10px}
.stratValHead .grow{flex:1 1 140px;min-width:0}
.stratValHead strong{font-size:15px;color:var(--ink)}
.stratValPill{margin-left:0;white-space:nowrap}
.stratValRow{display:grid;grid-template-columns:minmax(0,1fr) 60px;gap:3px 10px;align-items:center;margin:0 0 9px}
.stratValLabel{grid-column:1/-1;font-size:13px;font-weight:650;color:var(--ink);line-height:1.35;overflow-wrap:anywhere}
.stratValLabel .muted{font-weight:500;font-size:12px}
.stratValTrack{height:8px;border-radius:999px;background:var(--bg2);overflow:hidden;min-width:0}
.stratValFill{height:100%;border-radius:999px}
.stratValFill.stratValCash{background:var(--line-strong)}
.stratValFill.stratValAvg{background:var(--accent)}
.stratValFill.stratValXfer{background:var(--good)}
.stratValCents{font-size:13px;font-weight:800;color:var(--ink);text-align:right;white-space:nowrap}
.stratValDollars{font-size:13.5px;line-height:1.55;color:var(--ink2);background:var(--bg2);border-radius:8px;padding:8px 12px;margin:10px 0 0}
.stratValBest{color:var(--good-text)}
.stratValAlso{font-size:12.5px;line-height:1.5;margin:10px 0 0}
.stratValNote{font-size:12.5px;line-height:1.5;color:var(--ink2);margin:8px 0 0}
.stratValNote b{color:var(--ink)}
.stratActLine{font-size:13.5px;line-height:1.5;margin:8px 0 4px;color:var(--ink)}
.stratActLine .pill{margin:0 6px 0 0}
.stratActGotcha{font-size:12.5px;line-height:1.5;margin:6px 0 0}
.stratActChecklist{background:var(--bg2);border:1px solid var(--line);border-radius:var(--radius);padding:14px 16px;margin:16px 0 0}
.stratActChecklist h4{margin:0 0 4px;font-size:14.5px;font-weight:750;color:var(--ink)}
.stratActCheckItem{display:flex;gap:10px;padding:9px 0 8px;border-top:1px solid var(--line)}
.stratActChecklist h4 + .stratActCheckItem{border-top:0}
.stratActCheckItem b{font-size:13.5px;color:var(--ink)}
.stratActCheckItem p{margin:2px 0 0;font-size:13px;line-height:1.5}
.stratActCheckMark{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:50%;background:var(--good);color:#fff;font-size:11px;font-weight:800;margin-top:1px}
@media(max-width:700px){
  .stratPageNav{grid-template-columns:1fr;gap:8px}
  .stratPageNavBtn{min-height:44px}
  .stratValGrid{grid-template-columns:1fr}
}
/* === end Backlog #142 === */

/* === ASK-WIDGET (Jeremy 2026-07-20; Path B consolidation 2026-07-22;
   pill retired 2026-07-26, WS-SHELL-SCOPE) ===
   Originally: on every route except the dedicated Ask P&P AI page, the big
   anchored composer collapsed to a small "Ask P&P AI" pill floating in
   #dock's bottom-right corner. That pill (.askFab) and the reserved #dock
   strip it needed are GONE as of 2026-07-26 — see the shell-scoping rule
   right below and CHAIN-STATUS.md "THE SHELL SPEC". The entry points are
   now the topnav "Ask P&P AI" link (>=900px) and .topnavAskBtn (<900px).
   Driven by the same body class set in updateAskWidget() (chat.js):
     body.askWidgetMode  -> current route is NOT the assistant page
   The assistant route carries no class, so its full ChatGPT-style dock is
   untouched by any of this. */

/* WS-SHELL-SCOPE, 2026-07-26: the floating pill is gone (the topnav "Ask
   P&P AI" item / .topnavAskBtn are the entry points now — see the topnav
   rules and index.html), and with it the reserved #dock strip that used to
   sit under the footer on every non-assistant route. #dock itself still
   exists in the DOM as .appMain's dock sibling (assistant route keeps its
   full composer there), so on every OTHER route we simply remove it from
   flow instead of reserving space for an empty row. */
body.askWidgetMode .dock{display:none}
/* === end ASK-WIDGET === */

/* === REDESIGN-B PASS 1: "Precision" token layer + top navigation ===
   Direction B (smithfamai.com/pp-redesign-b, Jeremy sign-off 2026-07-20).
   Pass 1 scope: the nav shell (slim top navigation sitewide, sidebar
   Ask-AI-only, mobile nav sheet) + the additive visual tokens (Fraunces
   serif display moments, tabular numerals for figures). Brand navy/gold/
   green and both themes are untouched — everything below flows through
   the existing WS-THEME semantic tokens, so light AND dark stay
   first-class with zero new palette. Per-page polish is Pass 2. */
:root{
  /* ONE serif display face, used for the single restrained serif moment
     per page (page titles, Ask-AI greeting). Falls back to the old
     --ask-serif stack when Fraunces hasn't loaded. */
  --font-display:'Source Serif 4','Iowan Old Style','Palatino Linotype',Palatino,Georgia,serif;
  --ask-serif:var(--font-display); /* the Ask-AI greeting inherits the display face */
  --topnav-h:58px;
}

/* ---- Site-wide announcement banner (assets/announce.js) ----
   Slim full-bleed bar at the very top; three tones key off the existing
   semantic tokens so light/dark both work. Hidden by default (the [hidden]
   attribute), shown only when an active announcement loads. */
.announceBanner{width:100%;background:var(--accent-soft);border-bottom:1px solid var(--accent);color:var(--text-1)}
.announceBanner[hidden]{display:none}
.announceBanner--success{background:var(--brand-soft);border-bottom-color:var(--brand)}
.announceBanner--warn{background:rgba(199,120,59,.14);border-bottom-color:#c7783b}
.announceBannerIn{max-width:1100px;margin:0 auto;display:flex;align-items:center;gap:12px;padding:9px 18px}
.announceBannerMsg{flex:1 1 auto;font-size:13.5px;font-weight:600;line-height:1.4}
.announceBannerX{flex:0 0 auto;appearance:none;background:none;border:0;color:var(--text-2);font-size:20px;line-height:1;cursor:pointer;padding:2px 6px;border-radius:8px;min-height:36px;min-width:36px}
.announceBannerX:hover{background:rgba(0,0,0,.06);color:var(--text-1)}
@media (max-width:600px){ .announceBannerIn{padding:8px 14px;gap:8px} .announceBannerMsg{font-size:13px} }
/* Contextual "← Back to {page}" bar on the Wallet (#7) */
.walletBackBar{margin:10px 0 0}
.walletBackBar[hidden]{display:none}
.walletBackBtn{display:inline-flex;align-items:center;gap:6px;min-height:44px;padding:8px 14px;border-radius:999px;
  border:1px solid var(--line-strong);background:var(--bg-surface);color:var(--accent-text);
  font:700 13px var(--font);cursor:pointer}
.walletBackBtn:hover{background:var(--accent-soft);border-color:var(--accent)}
.walletBackBtn svg{width:16px;height:16px;flex:none}
/* Site-wide contextual "← Back to {page}" bar -- generalizes .walletBackBar/
   .walletBackBtn above (originally Wallet-only, #7) so every page's back bar
   looks identical instead of each view rolling its own. New call sites
   should use .ppBackBar/.ppBackBtn; .walletBackBar/.walletBackBtn are left
   as-is for existing markup. */
.ppBackBar{margin:10px 0 0}
.ppBackBar[hidden]{display:none}
.ppBackBtn{display:inline-flex;align-items:center;gap:6px;min-height:44px;padding:8px 14px;border-radius:999px;
  border:1px solid var(--line-strong);background:var(--bg-surface);color:var(--accent-text);
  font:700 13px var(--font);cursor:pointer}
.ppBackBtn:hover{background:var(--accent-soft);border-color:var(--accent)}
.ppBackBtn svg{width:16px;height:16px;flex:none}
/* Test-account "Delete Test Wallet" button (topnav slot; only rendered for
   test accounts by renderTestAcctBar). Danger-tinted, compact, 44px tap. */
.testAcctSlot{display:flex;align-items:center;flex:none}
.testAcctSlot:empty{display:none}
.testAcctBtn{appearance:none;font:inherit;font-size:12.5px;font-weight:700;white-space:nowrap;cursor:pointer;
  min-height:44px;padding:8px 14px;border-radius:999px;color:var(--danger-ink,#b3261e);
  background:rgba(179,38,30,.08);border:1px solid var(--danger-border,rgba(179,38,30,.4))}
.testAcctBtn:hover{background:rgba(179,38,30,.15)}
/* ---- Top navigation bar (header.nav.topnav) ---- */
.topnav .topnavIn{display:flex;align-items:center;gap:8px;height:var(--topnav-h);
  max-width:1400px;margin:0 auto;padding:0 20px;min-width:0}
.topnav .logo{gap:8px;flex:none}
.topnavLinks{display:flex;align-items:center;gap:2px;margin-left:14px;min-width:0;
  overflow-x:auto;scrollbar-width:none}
.topnavLinks::-webkit-scrollbar{display:none}
.topnavLinks a{padding:7px 11px;border-radius:999px;color:var(--text-2);font-weight:600;
  font-size:13px;text-decoration:none;cursor:pointer;white-space:nowrap;
  transition:color .12s,background .12s}
.topnavLinks a:hover{color:var(--text-1)}
.topnavLinks a.active{background:var(--bg-surface);color:var(--text-1);
  box-shadow:inset 0 0 0 1px var(--line-strong),var(--shadow)}
.topnavLinks a.topnavAsk{color:var(--accent-text);display:inline-flex;align-items:center;gap:6px}
.topnavLinks a.topnavAsk .topnavAskStar{font-size:12px}
.topnavLinks a.topnavAsk.active{background:var(--accent-soft);color:var(--accent-text);
  box-shadow:inset 0 0 0 1px var(--accent)}

/* ---- "Data & Resources" top-nav dropdown ----
   .navDrop is an inline-block wrapper inside .topnavLinks holding the trigger
   link (which reuses the .topnavLinks a look) + a .navDropMenu popover. The
   popover behaviour is scoped to >=900px; below that the top nav is display:none
   and the sections live in the hamburger sheet, so .navDrop is just a plain
   inline link (menu + caret hidden). Tokens carry both themes. */
.navDrop{position:relative;display:inline-block;flex:none}
.navDropCaret{display:none}
.navDropMenu{display:none}
@media(min-width:900px){
  .navDrop>a{display:inline-flex;align-items:center;gap:5px}
  .navDropCaret{display:inline-block;font-size:9px;line-height:1;color:var(--text-3);
    margin-left:1px;transition:transform .12s}
  .navDropMenu{display:block;position:absolute;top:calc(100% + 6px);left:0;z-index:30;
    min-width:220px;background:var(--bg-surface);border:1px solid var(--line);
    border-radius:12px;box-shadow:var(--shadow);padding:6px;
    opacity:0;visibility:hidden;transform:translateY(-4px);
    transition:opacity .14s,transform .14s,visibility .14s}
  .navDrop:hover .navDropMenu,.navDrop:focus-within .navDropMenu,.navDrop.is-open .navDropMenu{
    opacity:1;visibility:visible;transform:none}
  .navDrop:hover .navDropCaret,.navDrop:focus-within .navDropCaret,.navDrop.is-open .navDropCaret{
    transform:rotate(180deg)}
  .navDropItem{display:block;padding:9px 12px;border-radius:8px;color:var(--text-1);
    font-size:13px;font-weight:600;text-decoration:none;cursor:pointer;transition:background .12s}
  .navDropItem:hover{background:var(--accent-soft)}
  .navDropItemSub{display:block;font-size:12px;font-weight:500;color:var(--text-3);
    margin-top:1px;line-height:1.4}
}
@media(prefers-reduced-motion:reduce){
  .navDropMenu{transition:none}
  .navDropCaret{transition:none}
}
.topnavSpace{flex:1;min-width:8px}

/* Theme toggle + account block, relocated from the sidebar into the
   topbar (same ids/render fns — renderThemeToggle/renderSidebarAccount in
   ui-shared.js — only the styling context is new). */
.topnav .themeToggleBtn{display:inline-flex;align-items:center;justify-content:center;padding:4px 8px;min-height:44px;border-radius:999px;
  border:none;background:transparent;cursor:pointer;font-family:var(--font);flex:none}
.topnav .themeToggleBtn:hover .themeToggleTrack{border-color:var(--accent)}
.topnav .sidebarAccount{min-height:0;width:auto;flex:none;gap:0}
.topnav .sidebarAccount.sidebarSignIn{display:flex}
.topnav .sidebarAccountBtn{margin:0;padding:3px;width:auto;min-width:44px;min-height:44px;border-radius:999px;gap:0}
.topnav .sidebarWho{display:none} /* compact: avatar only in the topbar */
.topnav .sidebarSignInBtn{width:auto;padding:8px 16px;min-height:44px;border-radius:999px;
  font-size:13px;white-space:nowrap}

/* Compact Ask entry point, <900px only (WS-SHELL-SCOPE, 2026-07-26): the
   topnav "Ask P&P AI" link only lives in .topnavLinks, which is display:none
   below 900px (see the @media(max-width:899px){.topnavLinks{display:none}}
   rule below) -- so with the floating .askFab pill removed, this icon button
   is the ONLY entry point to Ask AI at narrow widths. Mirrors #topnavChatsBtn's
   own <900px scoping. Sits beside #themeToggleBtn (index.html places it there);
   flex:0 0 auto and a fixed 44x44 box so it behaves exactly like
   .topnav .themeToggleBtn under the "hamburger must never be the thing that
   overflows" fix further down -- a fixed-size element, not one of the
   shrink/truncate candidates, so adding it cannot push .menuBtn (order:99)
   out of the 440px topbar the way the unrelated 2026-07-26 phone bug did. */
.topnavAskBtn{display:none;align-items:center;justify-content:center;
  width:44px;height:44px;min-width:44px;min-height:44px;flex:0 0 auto;
  border:none;background:transparent;border-radius:999px;cursor:pointer;
  color:var(--text-2)}
.topnavAskBtn:hover{background:var(--bg-surface-2);color:var(--text-1)}
.topnavAskBtn svg{width:20px;height:20px;flex:none}
@media(max-width:899px){
  body.askWidgetMode .topnavAskBtn{display:inline-flex}
}

/* Persistent points-total control (#7) — a compact pill on the right of the
   slim top nav showing the wallet's total points + its dollar value. Tokens
   carry both themes; no dark override needed. When the driver sets the HTML
   `hidden` attr (nothing to show) it must not render — the base display below
   would otherwise beat UA `[hidden]`, so guard it explicitly. */
.ptsTopBar{display:inline-flex;align-items:center;flex:none;margin-left:4px}
.ptsTopBar[hidden]{display:none}
.ptsTopBarBtn{display:inline-flex;align-items:baseline;gap:7px;border:1px solid var(--line);
  background:var(--bg-surface);border-radius:999px;padding:6px 13px;min-height:44px;cursor:pointer;
  font-family:var(--font);white-space:nowrap;transition:border-color .12s,background .12s}
.ptsTopBarBtn:hover{border-color:var(--accent);background:var(--bg-surface-2)}
.ptsTopBarPts{font-size:12px;font-weight:600;color:var(--text-2)}
.ptsTopBarVal{font-size:13.5px;font-weight:700;color:var(--text-1);font-variant-numeric:tabular-nums}
/* Collapse gracefully on narrow widths: drop the $ value first, then the whole
   bar on very small screens where the topbar is tight. */
@media(max-width:560px){.ptsTopBarVal{display:none}}
@media(max-width:400px){.ptsTopBar{display:none}}

/* "Chats" button — assistant route, <900px only: opens the chats drawer
   (the old sidebar off-canvas mechanics, PP.toggleMenu). */
.topnavChatsBtn{display:none;align-items:center;gap:6px;border:1px solid var(--line-strong);
  background:var(--bg-surface);color:var(--text-2);border-radius:999px;padding:7px 13px;
  /* was min-height:36px. Dead once the 44px rule landed later in the file, but
     left as a trap for the next person who reorders these rules. Owner ruling #4. */
  font:600 13px var(--font);cursor:pointer;min-height:44px;flex:none;white-space:nowrap}
.topnavChatsBtn:hover{color:var(--text-1);border-color:var(--accent)}
.topnavChatsBtn .navIc{width:15px;height:15px}
@media(max-width:899px){
  body.viewIsAssistant .topnavChatsBtn{display:inline-flex}
}
@media(max-width:899px){
  :root{--topnav-h:55px}
  .topnav .topnavIn{height:54px;padding:0 12px;gap:6px}
  .topnavLinks{display:none}
}
@media(min-width:641px) and (max-width:899px){
  :root{--topnav-h:58px}
  .topnav .topnavIn{height:58px;padding:0 14px;gap:8px}
}
@media(max-width:520px){
  .topnav .logo{gap:6px}
  .topnav .sidebarBetaPill{display:none}
  .topnavChatsBtn{padding:7px 10px}
}

/* ---- Slim site footer -- moves secondary links (Methodology / Updates /
   Elite Status) out of the top nav and into a quiet strip at the bottom of
   the page, so .topnavLinks stops competing for space with the primary
   nav items. ---- */
.siteFooter{border-top:1px solid var(--line);background:var(--bg-surface-2);padding:20px;margin-top:64px;
  font-size:13.5px;color:var(--text-3);display:flex;flex-wrap:wrap;gap:0 22px;
  justify-content:center;align-items:center}
.siteFooterBrand{font-weight:800;font-size:13.5px;letter-spacing:-.02em;color:var(--text-1);
  min-height:44px;display:inline-flex;align-items:center}
.siteFooterLink{color:var(--text-2);text-decoration:none;cursor:pointer;min-height:44px;
  display:inline-flex;align-items:center}
.siteFooterLink:hover{color:var(--text-1)}
.siteFooterNote{color:var(--text-4);min-height:44px;display:inline-flex;align-items:center;text-align:center}
/* Ask AI and admin are full-height app surfaces. The homepage retains the
   shared footer so Pricing and legal pages have a visible landing-page path. */
body.viewIsAssistant #siteFooter,body.viewIsAdmin #siteFooter{display:none}

/* ---- "Data & Resources" landing page (#view-resources) ----
   Calm card grid linking out to the data sub-pages (Methodology / Updates /
   Elite Status / etc.). 3-up on desktop, single column at <=760px. All colours
   are tokens, so light + dark come for free. */
.resHead{max-width:720px;margin:0 0 22px}
.resHead .resIntro{font-size:14.5px;color:var(--text-2);line-height:1.55;margin:6px 0 0}
/* auto-fit, not a hardcoded count. This was `repeat(3,1fr)`, so adding the
   fourth card (the program name map) left it orphaned on its own row while
   three sat above it. A literal column count silently goes stale every time a
   card is added, and the person adding the card is not looking at this file.

   Sized so four fit inside --maxw (1080px): 4 x 240 + 3 x 16 gaps = 1008.
   It steps down to 3, then 2, then 1 on its own as the viewport narrows, so
   the explicit 1-column rule below 760px is now a floor rather than the only
   thing standing between this and a broken layout. */
.resGrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px;max-width:var(--maxw)}
@media(max-width:760px){.resGrid{grid-template-columns:1fr}}
.resCard{display:flex;flex-direction:column;background:var(--bg-surface);
  border:1px solid var(--line);border-radius:var(--radius);padding:22px;
  transition:border-color .14s,box-shadow .14s}
.resCard:hover{border-color:var(--accent);box-shadow:var(--shadow)}
.resCardIcon{font-size:22px;line-height:1;margin-bottom:12px}
.resCardTitle{font-size:17px;font-weight:700;color:var(--text-1);margin:0 0 6px}
.resCardBody{font-size:14px;color:var(--text-2);line-height:1.55;margin:0 0 16px}
.resCardLink{margin-top:auto;display:inline-flex;align-items:center;gap:4px;
  font-size:13.5px;font-weight:700;color:var(--accent-text);text-decoration:none;cursor:pointer}
.resCardLink:hover{text-decoration:underline}

/* ---- /name-map — the Program Name Map reference page (.nm*) ----
   Design pass (Jeremy, 2026-07-26: "this is a wall of beige"). Before this,
   every surface on the page — canvas, jump box, table header, table body —
   rendered within a few RGB points of the same cream, so a 38-row × 8-column
   data table simply dissolved into the page. The fix is the figure/ground
   idiom of .resCard directly above: real --bg-surface cards with a --line
   hairline, var(--radius) corners, sitting ON the cream --bg-canvas instead
   of merging with it — the table card additionally carries var(--shadow),
   because the table is the page's figure and everything else supports it.
   Every colour is a WS-THEME token, so light + dark both come for free.
   Decisions a future editor should know before "improving" them:
   - NO zebra striping, deliberately: rows are filtered by toggling .nmHide
     (display:none), and :nth-child() still COUNTS hidden rows, so stripes
     would clump unpredictably after every keystroke in the filter. Row
     hairlines + a hover wash do the row-tracking job without that bug.
   - The two ¢/pt columns (5 & 6) right-align with tabular figures so digits
     line up down the column. They stay --text-1, not green: these are
     neutral reference valuations, not good/bad verdicts.
   - .nmTableScroll owns BOTH axes (overflow:auto + a max-height). A sticky
     header needs a scrollport to stick to, and overflow-x:auto already makes
     this box the scroll container — so without its own vertical scrolling
     the sticky thead would be inert against page scroll. Horizontal overflow
     may live ONLY here: body{overflow-x:clip} makes any other overflow
     unreachable (clipped, not scrollable), i.e. a functional bug.
   - .nmSearch is 16px at EVERY width. Below 16px, iOS Safari zooms the
     viewport on focus and the page scrolls sideways — a live owner
     complaint. Do not shrink it in a media query, ever. ---- */
.nmPage{max-width:var(--maxw);margin:0 auto;padding:26px 20px 64px}
/* If the route already nests this page in .wrap (which carries the site's
   20px gutters), don't double them — .nmPage keeps only vertical padding. */
.wrap .nmPage{padding-left:0;padding-right:0}

/* ---- head: same idiom as .resPageTitle — the page's one serif moment ---- */
.nmHead{max-width:720px;margin:0 0 22px}
.nmTitle{font-family:var(--font-display);font-size:clamp(26px,3.6vw,36px);font-weight:600;
  letter-spacing:-.015em;line-height:1.12;margin:0 0 10px;color:var(--text-1)}
.nmLede{font-size:15px;line-height:1.6;color:var(--text-2);margin:0 0 12px}
.nmMeta{font-size:13px;line-height:1.55;color:var(--text-3);margin:0 0 4px}
.nmMeta a{color:var(--accent-text);font-weight:650;text-decoration:none}
.nmMeta a:hover{text-decoration:underline}

/* ---- shared card shell: the .resCard rest state ---- */
.nmCard{background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--radius)}

/* ---- jump nav: quiet card, kicker-style label ---- */
.nmJump{padding:16px 20px;margin:0 0 18px}
.nmJumpLabel{margin:0 0 8px;font-size:11.5px;font-weight:800;letter-spacing:.07em;
  text-transform:uppercase;color:var(--text-3)}
.nmJump ul{list-style:none;margin:0;padding:0;display:grid;gap:7px}
.nmJump li{font-size:13.5px;line-height:1.5;color:var(--text-2)}
.nmJump a{color:var(--accent-text);font-weight:700;text-decoration:none}
.nmJump a:hover{text-decoration:underline}
.nmJump a:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:4px}

/* ---- toolbar: search + live count ---- */
.nmToolbar{display:flex;flex-wrap:wrap;align-items:center;gap:10px 14px;margin:0 0 12px}
.nmSearch{flex:1 1 260px;max-width:440px;min-width:0;font:16px/1.3 var(--font);color:var(--text-1);
  background:var(--bg-surface);border:1px solid var(--line-strong);border-radius:10px;
  padding:11px 14px;-webkit-appearance:none;appearance:none}
.nmSearch::placeholder{color:var(--text-4)}
.nmSearch:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.nmCount{margin:0;font-size:13px;color:var(--text-3);font-variant-numeric:tabular-nums}

/* ---- the table card: the page's figure, so it alone gets the shadow ---- */
.nmTableCard{padding:0;overflow:hidden;box-shadow:var(--shadow);margin:0 0 26px}
.nmTableScroll{overflow:auto;max-height:min(72vh,680px);-webkit-overflow-scrolling:touch;
  overscroll-behavior-x:contain}
.nmTable{width:100%;min-width:880px;border-collapse:separate;border-spacing:0;
  font-size:13.5px;line-height:1.45}
/* border-collapse:separate, NOT collapse: collapsed borders detach from
   position:sticky cells in Chrome/Safari, so the header would shed its
   bottom rule the moment it sticks. */
.nmTable th{position:sticky;top:0;z-index:1;background:var(--bg-surface-2);
  text-align:left;font-size:11.5px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;
  color:var(--text-3);padding:11px 12px;border-bottom:1px solid var(--line-strong);white-space:nowrap}
.nmTable td{padding:9px 12px;border-bottom:1px solid var(--line);vertical-align:top;color:var(--text-2)}
.nmTable tbody tr:last-child td{border-bottom:0}
.nmTable tbody tr:hover td{background:var(--bg-surface-2)}
.nmTable th:first-child,.nmTable td:first-child{padding-left:18px}
.nmTable th:last-child,.nmTable td:last-child{padding-right:18px}
/* col 1 (Program) anchors the row */
.nmTable td:first-child{font-weight:700;color:var(--text-1);white-space:nowrap}
/* cols 2 & 4 (slug, AwardWallet key) are machine identifiers — mono */
.nmTable td:nth-child(2),.nmTable td:nth-child(4){white-space:nowrap;font-size:12.5px;
  font-family:ui-monospace,'SF Mono',SFMono-Regular,Menlo,Consolas,monospace}
/* cols 5 & 6 (Typical / Floor ¢-per-pt): right-aligned tabular figures */
.nmTable th:nth-child(5),.nmTable th:nth-child(6){text-align:right}
.nmTable td:nth-child(5),.nmTable td:nth-child(6){text-align:right;white-space:nowrap;
  font-variant-numeric:tabular-nums;font-weight:650;color:var(--text-1)}
/* cols 7 & 8 (transfer-list names, aliases) wrap — give them room to breathe */
.nmTable td:nth-child(7),.nmTable td:nth-child(8){min-width:220px;font-size:13px}
.nmHide{display:none}

/* ---- "use this in your own spreadsheet" ---- */
.nmUse{padding:24px 24px 22px}
.nmUse h2{font-size:20px;margin:0 0 8px}
.nmUse h3{font-size:15px;margin:20px 0 8px}
.nmUse>p{font-size:14px;color:var(--text-2);line-height:1.6;margin:0 0 6px}
.nmCopyRow{display:flex;align-items:stretch;gap:8px;margin:0 0 6px}
.nmCode{flex:1;min-width:0;display:block;overflow-x:auto;white-space:nowrap;
  font-family:ui-monospace,'SF Mono',SFMono-Regular,Menlo,Consolas,monospace;font-size:12.5px;
  color:var(--text-1);background:var(--bg-surface-2);border:1px solid var(--line);
  border-radius:10px;padding:10px 12px}
.nmCopyBtn{flex:none;font:650 13px var(--font);color:var(--text-2);background:var(--bg-surface);
  /* was min-height:40px — owner ruling #4. Third instance of the same shape:
     an under-44 declaration sitting alongside a 44px one. The gate now fails
     the build on these rather than letting the cascade decide. */
  border:1px solid var(--line-strong);border-radius:10px;padding:0 16px;min-height:44px;cursor:pointer;
  transition:border-color .12s,color .12s,background .12s}
.nmCopyBtn:hover{border-color:var(--accent);color:var(--accent-text);background:var(--accent-soft)}
.nmCopyBtn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.nmPre{overflow-x:auto;font-size:12.5px;line-height:1.55;color:var(--text-1);
  font-family:ui-monospace,'SF Mono',SFMono-Regular,Menlo,Consolas,monospace;
  background:var(--bg-surface-2);border:1px solid var(--line);border-radius:10px;
  padding:14px 16px;margin:8px 0 6px}
#nmDownloads{font-size:13.5px;color:var(--text-2);margin:14px 0 0}
#nmDownloads a{color:var(--accent-text);font-weight:650}

/* ---- narrow widths (390 / 440): tighter chrome, same 16px search ---- */
@media(max-width:520px){
  .nmPage{padding-top:18px}
  .nmJump{padding:14px 16px}
  .nmSearch{max-width:none;flex-basis:100%}
  .nmUse{padding:18px 16px}
  .nmTable th:first-child,.nmTable td:first-child{padding-left:14px}
  .nmTable th:last-child,.nmTable td:last-child{padding-right:14px}
}
/* ---- end /name-map ---- */

/* ---- Slim "← Data & Resources" breadcrumb atop each data sub-page
   (Methodology / Updates / Elite Status). Same idiom as .ppBackBar but a
   plainer, muted text link (chevron + label) that warms to the accent on
   hover, rather than the bordered pill of .ppBackBtn. ---- */
.dataCrumb{display:inline-flex;align-items:center;gap:6px;min-height:44px;margin:2px 0 8px;
  color:var(--text-3);font:600 13px var(--font);text-decoration:none;cursor:pointer;
  transition:color .12s}
.dataCrumb[hidden]{display:none}
.dataCrumb:hover{color:var(--accent-text)}
.dataCrumb svg{width:16px;height:16px;flex:none}

/* ---- Ask-AI-only sidebar (chats column) ---- */
.sidebarAiHead{display:flex;align-items:center;gap:8px;padding:8px 10px 14px;flex:none;
  font-weight:750;font-size:14px;color:var(--shell-ink);letter-spacing:-.01em}
.sidebarAiHead .sidebarAiStar{color:var(--shell-gold);font-size:13px}
.sidebarChatsNav{gap:2px}
.sidebarChatsNav .sidebarSubNew{border:1px solid var(--accent);background:var(--accent-soft);
  color:var(--accent-text);border-radius:10px;justify-content:center;font-size:13px;
  /* was min-height:40px — measured 227x40 and it BEAT the 44px base rule on
     specificity (0,2,0 vs 0,1,0). Owner ruling #4. check-tap-targets.mjs now
     fails the build on any rule that sets a guarded selector BELOW 44, which is
     the hole this line sat in. */
  padding:10px;min-height:44px;width:100%}
.sidebarChatsNav .sidebarSubNew:hover{background:var(--accent-soft);filter:brightness(1.03)}
.sidebarRecentsLabel{font-size:10.5px;font-weight:800;letter-spacing:.1em;text-transform:uppercase;
  color:var(--shell-muted);margin:16px 6px 4px}
.sidebarChatsNav .sidebarSubRecents{max-height:none;flex:1;min-height:0;overflow-y:auto}
.sidebarChatsNav .sidebarSubItem{padding:7px 8px}
/* beat the generic `.sidebarNav a` row rule (higher specificity) so
   "See all chats →" keeps its accent-link look in the chats column */
.sidebarNav a.sidebarSubAll{color:var(--accent-text);font-size:12.5px;font-weight:700;min-height:44px}
.sidebarNav a.sidebarSubAll:hover{background:var(--shell-surface);color:var(--accent-text)}

/* ---- Mobile nav sheet (site sections, <900px hamburger) ---- */
.navSheetScrim{position:fixed;inset:0;background:rgba(10,16,24,.45);z-index:95;
  opacity:0;pointer-events:none;transition:opacity .2s}
.navSheet{position:fixed;left:0;right:0;bottom:0;z-index:96;background:var(--bg-surface);
  border-radius:20px 20px 0 0;padding:14px 16px calc(22px + env(safe-area-inset-bottom));
  transform:translateY(102%);visibility:hidden;box-shadow:var(--shadow);
  transition:transform .25s cubic-bezier(.3,.9,.3,1),visibility .25s;
  max-height:82vh;overflow-y:auto}
body.navSheetOpen .navSheetScrim{opacity:1;pointer-events:auto}
body.navSheetOpen .navSheet{transform:none;visibility:visible}
@media(prefers-reduced-motion:reduce){ .navSheet{transition:none} }
@media(min-width:900px){ .navSheet,.navSheetScrim{display:none} }
.navSheetGrab{width:40px;height:4px;border-radius:99px;background:var(--line-strong);margin:0 auto 10px}
.navSheet a,.navSheet .navSheetFeedback{display:flex;align-items:center;gap:12px;width:100%;
  padding:12px 10px;border-radius:12px;font:600 15px var(--font);color:var(--text-1);
  text-decoration:none;cursor:pointer;border:none;background:none;text-align:left;min-height:48px}
.navSheet a:hover,.navSheet .navSheetFeedback:hover{background:var(--bg-surface-2)}
.navSheet a.active{background:var(--accent-soft);color:var(--accent-text)}
.navSheet a.active .navIc{color:var(--accent)}
.navSheet .navIc{width:17px;height:17px;flex:none;color:var(--text-3)}
.navSheet a.navSheetAsk{color:var(--accent-text)}
.navSheet a.navSheetAsk .navIc{color:var(--accent)}
.navSheet .navSheetFeedback{color:var(--text-2);font-size:14px;border-top:1px solid var(--line);
  border-radius:0;margin-top:6px;padding-top:14px}

/* ---- Precision type moments (Pass 1: shell + page headers only) ----
   ONE restrained serif display moment per page: the page title. The
   selector targets exactly the h2 that opens each view's first .wrap
   section (Programs, Your wallet, Points Strategy, Status, Updates,
   Methodology, Chats, Profile, Reset) — page CONTENT typography is
   untouched this pass. The Ask-AI page's serif moment is its existing
   greeting (--ask-serif, repointed to --font-display above). */
.view > section.wrap > h2:first-of-type{font-family:var(--font-display);font-weight:600;
  letter-spacing:-.015em;font-size:clamp(27px,3.6vw,34px);line-height:1.15}

/* Tabular numerals wherever a figure column needs to sit still —
   balances, values, matrix cells, KPI counts, table money. Additive:
   no size/weight changes, just lining tabular figures. */
.num{font-variant-numeric:tabular-nums;letter-spacing:-.01em}
table td,.kv b,.row .val,
.hubTotalItem,.hubBucketSub,.hubRowVal,.balInput,.inlineBalField,
.wCell--bal,.wCell--val,.wCell--rate,.matrixTable td,.composerBalance,
.askHomeWalletPreview,.tierReq,.statVal{font-variant-numeric:tabular-nums}
/* === end REDESIGN-B PASS 1 === */

/* === REDESIGN-B PASS 2: per-page Precision + marketing homepage ===
   Direction B final (smithfamai.com/pp-redesign-b-final, Jeremy sign-off).
   Everything flows through the WS-THEME semantic tokens; the only new
   tokens are the navy surface/CTA pairs below (per the final design, navy
   surfaces become slate and CTAs become gold in dark mode). */
:root{
  --pz-navy:#142343; --pz-navy-ink:#ffffff;      /* navy SURFACES (bubbles, big-eq, CTA band) */
  --pz-cta:#142343; --pz-cta-ink:#ffffff;        /* primary CTA fills */
  --pz-shadow-lg:0 2px 6px rgba(16,28,44,.06),0 20px 50px -16px rgba(16,28,44,.18);
}
:root[data-theme="dark"]{
  --pz-navy:#223142; --pz-navy-ink:var(--text-1);
  --pz-cta:#e5b964; --pz-cta-ink:#14202b;
  --pz-shadow-lg:0 2px 6px rgba(0,0,0,.35),0 20px 50px -16px rgba(0,0,0,.55);
}

/* ---- shared Precision primitives ---- */
.pzCard{background:var(--bg-surface);border:1px solid var(--line);border-radius:16px;box-shadow:var(--shadow)}
.pzCardPad{padding:22px}
@media(max-width:640px){.pzCardPad{padding:16px}}
h2.pzSec{font-size:12px;font-weight:800;letter-spacing:.1em;text-transform:uppercase;color:var(--text-3);
  margin:0 0 14px;display:flex;align-items:baseline;gap:10px}
h2.pzSec .amt{margin-left:auto;font-size:15px;font-weight:700;color:var(--text-1);letter-spacing:0;text-transform:none}
h2.pzSec .amt.cpp{font-size:12px;font-weight:600;color:var(--text-3)}
table.pzData{width:100%;border-collapse:collapse}
table.pzData th{font-size:10.5px;font-weight:800;letter-spacing:.09em;text-transform:uppercase;color:var(--text-3);
  text-align:left;padding:8px 10px;border-bottom:1px solid var(--line)}
table.pzData td{padding:13px 10px;border-bottom:1px solid var(--line);font-size:14px;vertical-align:middle}
table.pzData tr:last-child td{border-bottom:0}
table.pzData td.r,table.pzData th.r{text-align:right}
table.pzData .cpp{font-size:12px;color:var(--text-2)}
table.pzData b.best{color:var(--good-text)}
@media(max-width:640px){
  .hideM{display:none!important}
  table.pzData td{font-size:13.5px;padding:12px 6px}
  table.pzData th{padding:8px 6px}
}
.progName{display:flex;align-items:center;gap:11px;font-weight:600;min-width:0}
.progName .pn{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pzMark{width:30px;height:30px;border-radius:9px;display:grid;place-items:center;color:#fff;
  font-size:11px;font-weight:800;flex:none}
.pzMark.sm{width:22px;height:22px;font-size:9px;border-radius:6px}
.pzGrounded{display:inline-flex;align-items:center;gap:6px;font-size:10.5px;font-weight:700;letter-spacing:.05em;
  text-transform:uppercase;color:var(--good-text);background:rgba(15,118,110,.10);border-radius:999px;padding:3px 10px;margin-bottom:8px}
:root[data-theme="dark"] .pzGrounded{background:rgba(52,211,153,.12)}

/* ---- sticky section tabs (Wallet: Cards&Points/Budget · Strategy: Earn/Use/Activate) ---- */
.pzTabs{position:sticky;top:var(--topnav-h);z-index:15;display:flex;gap:6px;padding:10px 0 12px;margin:0 0 14px;
  background:linear-gradient(var(--bg-canvas) 78%,transparent);overflow-x:auto;scrollbar-width:none}
.pzTabs::-webkit-scrollbar{display:none}
.pzTabs a{border:1px solid var(--line);background:var(--bg-surface);color:var(--text-2);font-weight:600;
  font-size:13px;padding:7px 14px;border-radius:999px;white-space:nowrap;display:flex;gap:7px;align-items:center;
  text-decoration:none;cursor:pointer;flex:none}
.pzTabs a .k{font-size:10.5px;font-weight:800;color:var(--accent-text);background:var(--accent-soft);
  border-radius:99px;width:17px;height:17px;display:grid;place-items:center}
.pzTabs a.on{border-color:var(--accent);background:var(--accent-soft);color:var(--accent-text)}
.anchorSecPz,#stratSecEarn,#stratSecUse,#stratSecAct{scroll-margin-top:64px}
/* the .panel wrappers around #wizWrap/#stratWrap clip nothing, but give the
   sticky tabs an opaque-ish canvas so content doesn't ghost through */
#wizWrap .pzTabs,#stratWrap .pzTabs{background:linear-gradient(var(--bg-surface) 78%,transparent)}

/* ---- A9: Strategy top-of-page Earn/Use/Activate explainer boxes ----
   Collapse is driven by stratWireExplainCollapse (strategy.js) toggling
   .stratExplainCollapsed on #stratExplainWrap; the transition here is what
   makes that toggle read as a shrink/fade instead of an instant jump-cut.
   max-height (not height) so no JS measurement is needed for the animated
   property, and reduced-motion drops straight to the collapsed end state
   with no transition at all -- same box, same information, just no
   motion for anyone who asked not to see it. */
.stratExplainBoxes{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;
  max-height:600px;opacity:1;overflow:hidden;
  transition:max-height .38s ease,opacity .28s ease,margin .28s ease}
.stratExplainWrap{margin:0 0 16px}
.stratExplainWrap.stratExplainCollapsed .stratExplainBoxes{max-height:0;opacity:0;margin:0;pointer-events:none}
.stratExplainWrap.stratExplainCollapsed{margin-bottom:0}
.stratExplainBox{text-align:left;font:inherit;color:inherit;cursor:pointer;
  background:var(--bg-surface);border:1px solid var(--line);border-radius:14px;padding:16px;
  display:flex;flex-direction:column;gap:5px;min-width:0}
.stratExplainBox:hover{border-color:var(--accent);box-shadow:var(--shadow)}
.stratExplainNum{font-size:10.5px;font-weight:800;color:var(--accent-text);background:var(--accent-soft);
  border-radius:99px;width:18px;height:18px;display:grid;place-items:center}
.stratExplainTitle{margin:2px 0 0;font-size:15px;font-weight:700}
.stratExplainWhat{margin:0;font-size:12px;font-weight:650;color:var(--accent-text)}
.stratExplainBody{margin:0;font-size:12.5px;line-height:1.4;color:var(--text-2)}
@media(max-width:699px){.stratExplainBoxes{grid-template-columns:1fr}}
@media(prefers-reduced-motion:reduce){.stratExplainBoxes{transition:none}}

/* ---- A10: Strategy missing-spend flag ---- */
.stratSpendFlag{display:flex;align-items:flex-start;gap:10px;margin:0 0 16px;padding:12px 16px;
  background:var(--accent-soft);border:1px solid var(--accent);border-radius:12px;font-size:13.5px;color:var(--text-1)}
.stratSpendFlagIcon{flex:none;line-height:1.3}
.stratSpendFlagBody a{color:var(--accent-text);font-weight:700}

/* ---- A11: Wallet contextual cards sourced from Points Strategy ---- */
.stratInsightSection{margin-top:22px}
.stratInsightGrid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}
.stratInsightCard{background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--radius);
  padding:16px;cursor:pointer;text-align:left;font-family:var(--font);display:block;width:100%;min-width:0}
.stratInsightCard:hover{border-color:var(--accent);box-shadow:var(--shadow);transform:translateY(-2px)}
.stratInsightCard h4{margin:0 0 4px;font-size:14px}
.stratInsightCard p{margin:0;font-size:12.5px}
.stratInsightCard .go{margin-top:8px;font-size:12px;font-weight:700;color:var(--accent-text)}
@media(max-width:660px){.stratInsightGrid{grid-template-columns:1fr}}

/* ---- KPI cards (wallet first-run shell) ---- */
.pzKpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin:0 0 14px}
.pzKpi{min-width:0}
.pzKpi{background:var(--bg-surface);border:1px solid var(--line);border-radius:16px;padding:16px 18px;box-shadow:var(--shadow)}
.pzKpi .l{font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--text-3)}
.pzKpi .v{font-size:26px;font-weight:750;letter-spacing:-.02em;margin-top:4px;color:var(--text-1)}
.pzKpi .v.zero{color:var(--text-4)}
.pzKpi .s{font-size:12px;color:var(--text-2);margin-top:2px}
.pzKpi.hero{background:var(--pz-navy);border-color:var(--pz-navy)}
.pzKpi.hero .v{color:var(--pz-navy-ink)}
.pzKpi.hero .v.zero{color:color-mix(in srgb,var(--pz-navy-ink) 55%,transparent)}
.pzKpi.hero .l,.pzKpi.hero .s{color:color-mix(in srgb,var(--pz-navy-ink) 65%,transparent)}
@media(max-width:900px){.pzKpis{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:640px){.pzKpis .pzKpi:last-child:nth-child(odd){grid-column:1/-1}}

/* ---- wallet first-run: wizard frame + ghost shell ---- */
.wizardPz{border:1.5px solid var(--accent);border-radius:20px;background:var(--bg-surface);
  box-shadow:var(--pz-shadow-lg);overflow:hidden;margin:0 0 22px}
.wizHeadPz{display:flex;align-items:center;gap:12px;padding:14px 20px;background:var(--accent-soft);
  border-bottom:1px solid var(--line);flex-wrap:wrap}
.wizHeadPz .wizAmp{width:30px;height:30px;border-radius:9px;background:var(--pz-navy);color:var(--pz-navy-ink);
  display:grid;place-items:center;font-family:var(--font-display);font-size:18px;font-weight:700;flex:none}
.wizHeadPz b{font-size:15px;letter-spacing:-.01em;color:var(--text-1)}
.wizHeadPz .wizTime{font-size:12px;color:var(--text-2);font-weight:550}
.wizHeadPz .wizSafe{margin-left:auto;font-size:12px;font-weight:700;color:var(--good-text);white-space:nowrap}
.wizardPz .chatOnboard{margin:0;padding:16px 20px 20px}
@media(max-width:640px){.wizardPz .chatOnboard{padding:12px 14px 16px}.wizHeadPz{padding:12px 14px}}
.pzFirstShell{display:grid;gap:14px;margin-top:22px}
.shellCard{border:1.5px dashed var(--line-strong);border-radius:16px;background:var(--bg-surface-2);box-shadow:none}
.shellCard h2.pzSec .amt{color:var(--text-3)}
.shellRow td{color:var(--text-3)}
.shellRow .progName{opacity:.55;filter:grayscale(.5)}
.btnMini{border:1px solid var(--line-strong);background:var(--bg-surface);color:var(--text-2);font-weight:650;
  font-size:12px;border-radius:999px;padding:5px 13px;cursor:pointer;white-space:nowrap;font-family:var(--font)}
.btnMini:hover{border-color:var(--accent);color:var(--accent-text)}
/* ghost tables live inside the already-padded wallet panel — at phone
   width the columns must be allowed to shrink (fixed layout + ellipsised
   program name) or the table's min-content width overflows #viewsScroll */
@media(max-width:640px){
  .shellCard table.pzData{table-layout:fixed}
  .shellCard table.pzData th:first-child{width:50%}
  .shellCard .progName .cpp{display:none}
}

/* ---- monthly budget by category (wallet, first-run + populated) ---- */
.budgetSummary{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin-bottom:10px;
  font-size:13.5px;color:var(--text-2)}
.budgetSummary .tot{font-size:22px;font-weight:750;color:var(--text-1);letter-spacing:-.02em}
/* === CARD-ROW BUDGET (Option B rows, wider, card feel from A) ===
   Each category is a full-width bordered card: name + best-card note stacked
   on the left, amount pulled in on the right. Kills the sparse grid where the
   amount stranded on the far edge across empty space. */
.budgetRow{display:flex;justify-content:space-between;align-items:center;gap:16px;
  background:var(--bg-surface,var(--surface-1));border:1px solid var(--line);border-radius:12px;
  padding:13px 16px;margin-bottom:8px}
.budgetRow:last-of-type{margin-bottom:0}
.budgetRowMain{min-width:0;display:flex;flex-direction:column;gap:2px;flex:1 1 auto}
/* edit-mode rework: read-only balance + budget rows (no per-field pencils) */
.hubBalRO{font:inherit;font-weight:750;color:var(--ink);white-space:nowrap}
.budgetRowRO .budgetROval{font-size:17px;font-weight:750;color:var(--text-1);text-align:right;white-space:nowrap;flex:0 0 auto}
.budgetRowRO .budgetROempty{font-size:13px;font-weight:400;color:var(--text-4)}
.budgetRowEdit{align-items:center}
.budgetSecHead{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:2px}
.budgetRow .cat{font-size:14.5px;font-weight:650;color:var(--text-1);padding-top:0}
.budgetRow .cat span{display:block;font-size:11.5px;font-weight:400;color:var(--text-3);margin-top:2px}
/* === CLEAN-BOX BUDGET (wallet-budget-redesign) ===
   The band chips (.bandChips), the collapsed pill (.budgetPill), and the
   "or pick a range" link are REMOVED with the pattern itself — every
   category is one always-visible number box. */
.budgetCcNote{margin:0 0 8px;font-size:12.5px;line-height:1.5;color:var(--text-2);background:var(--accent-soft);border:1px solid var(--line);border-radius:10px;padding:10px 12px;max-width:640px}
.budgetCcNote b{color:var(--text-1)}
.budgetEstimateNote{margin:0 0 6px;font-size:12.5px;font-weight:600;color:var(--accent-text)}
.budgetField{display:flex;flex-direction:column;gap:6px;min-width:0;align-items:flex-end;flex:0 0 auto}
.budgetExactRow{display:inline-flex;align-items:center;gap:0;border:1px solid var(--line-strong,var(--line));
  border-radius:12px;overflow:hidden;background:var(--bg-surface);max-width:100%;
  transition:border-color .15s,box-shadow .15s}
.budgetExactRow:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.budgetExactRow .cur{padding:8px 2px 8px 13px;font-size:13.5px;font-weight:700;color:var(--text-3)}
.budgetExactRow input{width:104px;border:0;background:none;outline:none;font-family:var(--font);
  font-size:15px;font-weight:700;color:var(--text-1);padding:8px 8px 8px 3px;
  font-variant-numeric:tabular-nums}
.budgetExactRow input::placeholder{color:var(--text-4);font-weight:500}
/* The EIGHTH focus-zoom selector, found by walking the live wizard rather than
   by reading the stylesheet. `.budgetExactRow input` is (0,1,1) and beats the
   sitewide `input{font-size:16px}` guard (0,0,1), so all nine spending-step
   fields still computed 15px and still zoomed the page on focus -- on the one
   step that is nothing BUT typing numbers.

   Worth recording why it was missed: the previous pass enumerated seven
   offenders by grepping the stylesheet, and this rule reads as a layout rule
   (width, border, padding) that happens to carry a font-size. Grep found the
   seven that looked like input rules. The measurement found the eighth.
   Enumerating offenders is not a mechanism; it is a list that goes stale, and
   this is the second time in one day it went stale. The durable fix is a
   build-time assertion that no rule matching an input sets a font-size under
   16px outside a min-width:900px context -- it is on the backlog. */
@media(max-width:899px){ .budgetExactRow input{font-size:16px} }
.budgetUnit{padding:8px 13px 8px 0;font-size:12px;font-weight:700;color:var(--text-3)}
.periodToggle{display:inline-flex;border-left:1px solid var(--line);flex:0 0 auto;align-self:stretch;align-items:stretch}
.periodToggle button{border:0;background:none;padding:0 12px;font-size:12px;font-weight:700;
  color:var(--text-3);cursor:pointer;font-family:var(--font)}
.periodToggle button.on{color:var(--accent-text);background:var(--accent-soft)}
.budgetAnnualNote{margin:0;font-size:11.5px;color:var(--text-3);line-height:1.45;max-width:460px}
.linkBtn{align-self:flex-start;border:0;background:none;padding:2px 0;font-size:12px;font-weight:600;
  color:var(--accent-text);text-decoration:underline;cursor:pointer;font-family:var(--font)}
@media(max-width:560px){
  .budgetRow{align-items:stretch;flex-direction:column;gap:8px;padding:14px}
  .budgetRowRO .budgetROval{text-align:left}
  .budgetField{align-items:flex-start}
}
/* === WALLET TABS (wallet-budget-redesign): Balances / Budget / Goals ===
   Same visual language as Strategy v2's .st2Tabs; panes mirror .st2Pane. */
.wTabsBar{margin:16px 0 18px}
.wTabPane{display:none}
.wTabPane.on{display:block;animation:st2fade .22s ease}
@media(prefers-reduced-motion:reduce){.wTabPane.on{animation:none}}

/* Expiration-warning inline row (wallet/hub) -- amber-tinted, low-key: a
   heads-up, not an error. Reuses the amber used elsewhere for warn tones
   (.announceBanner--warn) rather than the red --danger tokens, since
   expiring points are informational, not a failure state. */
.expWarn{display:inline-flex;align-items:center;gap:6px;font-size:12.5px;color:#a05a1e;
  background:rgba(199,120,59,.14);border-radius:8px;padding:6px 10px}
.expWarnDot{width:7px;height:7px;border-radius:50%;background:#c7783b;flex:none}
/* #19: the hardcoded #a05a1e amber sits at ~2.8:1 on the dark surface (fails
   4.5:1) -- lift it to a brighter, still-calm amber in dark only. */
[data-theme="dark"] .expWarn{color:#e2a24d}
/* More-urgent variant: points expiring within ~3 months. A stronger, redder
   amber than the calm heads-up above, still short of the red --danger error
   state. Kept subtle -- same size/shape, just a hotter tone. */
.expWarn--soon,.expWarn--urgent{color:#a33b12;background:rgba(199,90,40,.16)}
.expWarn--soon .expWarnDot,.expWarn--urgent .expWarnDot{background:#c2410c}
[data-theme="dark"] .expWarn--soon,[data-theme="dark"] .expWarn--urgent{color:#f0a878;background:rgba(224,134,104,.18)}
/* dated (concrete) expiry line — same idiom as .expWarn, tabular figures */
.expWarnDated{font-variant-numeric:tabular-nums}
/* last-activity month input shown in wallet edit mode */
.expActivityEdit{display:inline-flex;align-items:center;gap:8px;margin-top:6px;flex-wrap:wrap}
.expActivityLbl{font-size:12px;font-weight:600;color:var(--text-3)}
.expActivityInput{font-family:var(--font);font-size:12.5px;padding:5px 8px;border:1px solid var(--line-strong,var(--line));border-radius:8px;background:var(--bg-surface);color:var(--text-1)}
.expActivityInput:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
/* JOB3 (iOS focus-zoom, cont'd -- see EOF): (0,1,0) outranks the sitewide
   guard's bare `input` (0,0,1). Computed 12.5px, zoomed on focus. Matched
   at (0,1,0) here so it wins on source order. */
@media(max-width:899px){ .expActivityInput{font-size:16px} }

/* Transfer-bonus card/list (wallet/strategy) -- compact, scannable rows of
   active program-to-program transfer bonuses. The percent uses the same
   accent-fill treatment as .badge--best since "bonus %" is the best-pick
   signal of the row. */
.xferBonusCard{background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--radius);padding:14px 16px}
.xferBonusRow{display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid var(--line)}
.xferBonusRow:last-child{border-bottom:0}
.xferBonusRow .xferFrom,.xferBonusRow .xferTo{font-size:13.5px;font-weight:650;color:var(--text-1)}
.xferBonusRow .xferArrow{color:var(--text-4);font-size:13px;flex:none}
.xferBonusRow .xferEnds{margin-left:auto;font-size:12px;color:var(--text-3);white-space:nowrap}
.xferBonusPct{display:inline-flex;align-items:center;font-size:10px;font-weight:800;letter-spacing:.03em;
  text-transform:uppercase;padding:2px 8px;border-radius:999px;line-height:1.4;white-space:nowrap;
  background:var(--accent);color:var(--accent-on);flex:none}

/* "Best card by category" cheat sheet -- a glanceable table reusing the
   budget/matrix row idiom (label left, value right, hairline dividers)
   rather than inventing a new grid. */
.cheatSheet{background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--radius)}
/* Alignment fix (2026-07-22): the row is a two-track grid — a FIXED-width
   category column, then the shared .cardCell (logo slot / name / rate). With
   the category track constant, the card cell starts at one x on every row, so
   the logo column, name column, and hard-right rate all line up row-to-row
   (previously the whole logo+name+rate group was right-aligned as a unit and
   the logo's x drifted with name length). Long names ellipsize via
   .cardName--1ln; the rate never wraps (.cardVal). */
.cheatSheetRow{display:grid;grid-template-columns:var(--cheatCatW,118px) minmax(0,1fr);align-items:center;gap:14px;
  padding:13px 16px;border-bottom:1px solid var(--line)}
.cheatSheetRow:last-child{border-bottom:0}
.cheatSheetCat{font-size:13.5px;font-weight:650;color:var(--text-1)}
.cheatSheetCard{font-size:13px;color:var(--text-2)}
.cheatSheetCard b{font-weight:750;color:var(--text-1)}
@media(max-width:480px){.cheatSheetRow{--cheatCatW:92px;gap:10px;padding:13px 12px}}

/* Home-dashboard wrapper around the free best-card-by-category cheat sheet
   (Rec 5). Reuses .cheatSheet's card + hairline-row styling as-is; this only
   adds section spacing, a heading, and a quiet green "free" chip framing it
   like the other dashboard sections. The translucent green reads on both
   themes, so no dark override is needed. */
.homeCheatSheet{margin:28px 0 0}
.homeCheatSheetHead{display:flex;align-items:baseline;gap:8px;margin:0 0 10px;flex-wrap:wrap}
.homeCheatSheetTitle{font-size:15px;font-weight:700;color:var(--text-1)}
.homeCheatSheetFree{display:inline-flex;align-items:center;font-size:10.5px;font-weight:800;letter-spacing:.05em;
  text-transform:uppercase;color:var(--good-text);background:rgba(15,118,110,.12);border-radius:999px;padding:2px 8px}
.homeCheatSheetSub{margin:0 0 12px;font-size:12.5px;line-height:1.5;color:var(--text-3);max-width:640px}

/* Per-budget-category "best card" hint (wallet.js) -- a small, calm,
   right-aligned line echoing .cheatSheetCard's tone: label text in --text-2,
   the winning card name in --good-text. On narrow screens it drops to a
   left-aligned inline line under its category. */
.bestCardHint{font-size:11.5px;color:var(--text-2);text-align:left;line-height:1.4;margin-top:5px}
.bestCardHint b{color:var(--good-text);font-weight:650}
.bestCardHint .num{white-space:normal;overflow-wrap:anywhere}

/* "Known sweet spots" reference card (Use tab, bottom) -- same card/hairline-row
   idiom as .xferBonusCard/.cheatSheet above, but each row is a short 4-line
   stack (program, redemption name, cost, why) rather than a single line, since
   the "why" needs room to breathe. */
.sweetSpots{background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--radius);padding:14px 16px}
.sweetSpotRow{padding:10px 0;border-bottom:1px solid var(--line)}
.sweetSpotRow:last-child{border-bottom:0}
.sweetSpotProg{font-size:11px;font-weight:700;letter-spacing:.02em;text-transform:uppercase;color:var(--text-3);margin-bottom:2px}
.sweetSpotName{font-size:13.5px;font-weight:650;color:var(--text-1)}
.sweetSpotCost{font-size:12.5px;font-weight:600;color:var(--accent-text);margin:2px 0}
.sweetSpotWhy{font-size:12.5px;color:var(--text-2);line-height:1.45}

/* ---- Ask sidebar: grouped recents labels ---- */
.sidebarGrpLbl{font-size:10.5px;font-weight:800;letter-spacing:.1em;text-transform:uppercase;
  color:var(--shell-muted);margin:14px 6px 4px}
.sidebarGrpLbl:first-child{margin-top:8px}

/* ---- marketing homepage (logged-out front door) ---- */
.mkHome{font-size:15px;line-height:1.55}
.mkHome a{cursor:pointer}
.mkKicker{font-size:12px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:var(--accent-text)}
/* WS-JOURNEY follow-up: the demo reel moved back to the differentiator
   section, so the hero's second column is gone. Single column now — the
   old .96fr/1.04fr grid left the copy jammed into the left half of the
   1240 track with a bare right half at desktop. */
.mkHero{max-width:1240px;margin:0 auto;padding:36px 20px 26px;display:grid;
  grid-template-columns:1fr}
@media(max-width:1080px){.mkHero{grid-template-columns:1fr;gap:34px;padding:30px 20px 16px}}
.mkHero h1{font-family:var(--font-display);font-size:clamp(30px,3.1vw,42px);font-weight:600;
  letter-spacing:-.02em;line-height:1.06;margin:14px 0 0;color:var(--text-1)}
.mkHero h1 em{font-style:normal;color:var(--accent-text)}
.mkSub{color:var(--text-2);font-size:15.5px;line-height:1.6;max-width:480px;margin-top:14px}
.mkCtas{display:flex;gap:10px;flex-wrap:wrap;margin-top:20px;align-items:center}
.btnHero{display:inline-flex;align-items:center;gap:9px;border:0;background:var(--pz-cta);color:var(--pz-cta-ink);
  font-weight:700;font-size:15.5px;border-radius:14px;padding:15px 26px;text-decoration:none;cursor:pointer;
  box-shadow:var(--pz-shadow-lg)}
.btnHero.gold{background:var(--accent);color:var(--accent-on)}
.btnHeroGhost{display:inline-flex;align-items:center;gap:8px;border:1px solid var(--line-strong);
  background:var(--bg-surface);color:var(--text-1);font-weight:650;font-size:15px;border-radius:14px;
  padding:14px 22px;text-decoration:none;cursor:pointer}
.mkTrust{display:flex;gap:16px;flex-wrap:wrap;margin-top:15px;font-size:12.5px;color:var(--text-2);font-weight:550}
.mkTrust span{display:inline-flex;align-items:center;gap:6px}
.mkTrust .tick{color:var(--good-text);font-weight:800}
@media(max-width:480px){.btnHero{font-size:14.5px;padding:14px 20px}}
/* Desktop only: with the right column gone, center the hero copy on the
   page axis so the width reads intentional instead of half-empty. Below
   1081px the hero keeps its original left-aligned stack. */
@media(min-width:1081px){
  .mkHeroCopy{max-width:720px;margin:0 auto;text-align:center}
  .mkHeroCopy .mkSub{margin-left:auto;margin-right:auto}
  .mkHeroCopy .mkCtas,.mkHeroCopy .mkTrust{justify-content:center}
}
/* hero product visual — WS-FOLD: the wallet card lives in the copy column
   now; the demo reel fills the hero's right column (see WS-FOLD block at
   the end of this file). The old static equation/AI teaser cards are gone. */
.mkWalletCard{padding:16px 18px 12px}
.mkWalletCard .l{font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--text-3);display:flex;align-items:center;gap:8px}
.mkSamplePill{font-size:9.5px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:var(--accent-text);background:var(--accent-soft);border-radius:999px;padding:2px 8px;letter-spacing:.04em}
.mkWalletCard .big{font-family:var(--font-display);font-size:30px;font-weight:600;letter-spacing:-.02em;margin-top:2px;color:var(--text-1)}
.mkWalletCard .big .u{font-size:14px;color:var(--text-2);font-family:var(--font);font-weight:600}
@media(max-width:640px){.mkWalletCard .big .u{display:block;margin-top:4px}}
.mkWalletCard .rows{margin-top:12px;border-top:1px solid var(--line)}
.mkWalletCard .row{display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid var(--line);
  font-size:13px;font-weight:600;color:var(--text-1)}
.mkWalletCard .row:last-child{border-bottom:0}
.mkWalletCard .row .r{margin-left:auto;text-align:right;font-weight:700}
.mkWalletCard .row .r span{display:block;font-size:11px;color:var(--text-3);font-weight:500}
/* WS-JOURNEY follow-up: the card now sits under the steps rail as the
   Step-1 exemplar, a direct child of .mkHome — which has no column of its
   own, so without this it rendered full-bleed edge to edge. A lone example
   object gets a FIXED centered width (not the 1240 marketing column: a
   4-row list stretched to ~1200px reads as a broken table), chrome matching
   the .mkStep cards, and clear air to the rail above / scroll cue below.
   width:min() keeps 20px side gutters at phone + tablet, so it can never
   overflow the viewport. */
.mkHome>.mkWalletCard{width:min(640px,calc(100% - 40px));margin:34px auto 0;
  padding:20px 22px 14px;background:var(--bg-surface);border:1px solid var(--line);
  border-radius:var(--radius);box-shadow:var(--shadow)}
@media(max-width:640px){.mkHome>.mkWalletCard{margin-top:26px;padding:16px 16px 10px}}

/* verified-data card (restyled 2026-07-22): was a full-bleed edge-to-edge
   band — the only element on the homepage that escaped the centered column,
   which is exactly what a user flagged. Now the OUTER div is the column
   (same 1240px + 20px gutters as .mkSec) and the tint/border moved to the
   INNER div as a rounded, contained card — surface-2 tint on a 1px --line
   border with the shared --radius, so it reads as page furniture, not a
   site-wide banner. scroll-margin clears the sticky header for the
   #mkScrollCue smooth-scroll that targets #mkStrip. */
.mkStrip{max-width:1240px;margin:42px auto 0;padding:0 20px;scroll-margin-top:76px}
.mkStripIn{background:var(--bg-surface-2);border:1px solid var(--line);border-radius:var(--radius);
  padding:18px 24px;display:flex;gap:12px 30px;align-items:baseline;
  flex-wrap:wrap;justify-content:center}
.mkStripIn .lead{font-size:13.5px;font-weight:700;color:var(--text-1)}
.mkStripIn .stat{font-size:12.5px;color:var(--text-2)}
.mkStripIn .stat b{color:var(--text-1);font-variant-numeric:tabular-nums}
.mkStripLink{font-weight:650;color:var(--accent-text);cursor:pointer;text-decoration:none;white-space:nowrap}
/* generic marketing sections */
.mkSec{max-width:1240px;margin:0 auto;padding:60px 20px 0}
.mkSec .secKick{font-size:11.5px;font-weight:800;letter-spacing:.13em;text-transform:uppercase;color:var(--accent-text)}
.mkSec>h2{font-family:var(--font-display);font-size:clamp(26px,3.4vw,38px);font-weight:600;letter-spacing:-.015em;
  line-height:1.12;margin:10px 0 0;max-width:640px;color:var(--text-1)}
.mkSec .secSub{color:var(--text-2);font-size:15px;max-width:600px;margin-top:12px;line-height:1.6}
.mkTableFoot{font-size:12.5px;color:var(--text-3);margin-top:10px}
.mkTableFoot a{color:var(--accent-text);font-weight:650;text-decoration:none}
.mkTrio{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:30px}
@media(max-width:900px){.mkTrio{grid-template-columns:1fr}}
.mkTrio .pzCard{padding:24px}
.mkTrio .ic{width:38px;height:38px;border-radius:11px;background:var(--accent-soft);color:var(--accent-text);
  display:grid;place-items:center;font-size:18px;margin-bottom:14px}
.mkTrio h3{font-size:17px;font-weight:750;letter-spacing:-.01em;margin:0;color:var(--text-1)}
.mkTrio p{font-size:13.5px;color:var(--text-2);margin:7px 0 0;line-height:1.6}
.mkTrio .proof{font-size:12.5px;font-weight:650;color:var(--accent-text);margin-top:12px;display:inline-block;text-decoration:none}
/* big equation moment */
.mkMathWrap{display:grid;grid-template-columns:1fr 1.1fr;gap:34px;align-items:center;margin-top:30px}
@media(max-width:1080px){.mkMathWrap{grid-template-columns:1fr}}
.mkBigEq{background:var(--pz-navy);color:var(--pz-navy-ink);border-radius:20px;padding:30px 30px 26px;box-shadow:var(--pz-shadow-lg)}
.mkBigEq .l{font-size:11px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;opacity:.65}
.mkBigEq .eq{font-family:var(--font-display);font-size:clamp(24px,3vw,32px);font-weight:600;letter-spacing:-.01em;
  margin-top:10px;line-height:1.25;font-variant-numeric:tabular-nums}
.mkBigEq .eq .dim{opacity:.55}
.mkBigEq .eq b{color:#e5b964}
.mkBigEq .s{font-size:13px;opacity:.78;margin-top:12px;line-height:1.55}
/* spread bars — two-column moment (bars left, 6× stat right on desktop) */
.mkSpreadWrap{margin-top:26px}
@media(min-width:900px){
  .mkSpreadWrap{display:grid;grid-template-columns:1.55fr 1fr;gap:56px;align-items:center}
}
.mkSpread{display:grid;gap:10px;margin-top:26px;max-width:720px}
.mkSpreadWrap .mkSpread{margin-top:0}
.mkSpreadRow{display:grid;grid-template-columns:190px 1fr 64px;gap:12px;align-items:center;font-size:13.5px;color:var(--text-1)}
.mkSpreadRow .track{height:10px;border-radius:99px;background:var(--bg-deep);overflow:hidden}
.mkSpreadRow .track i{display:block;height:100%;border-radius:99px;background:var(--accent);transform-origin:left center}
.mkSpreadRow .track i.hi{background:var(--good)}
.mkSpreadRow .track i.lo{background:var(--danger-ink);opacity:.65}
.mkSpreadRow .v{font-weight:750;text-align:right}
@media(max-width:640px){.mkSpreadRow{grid-template-columns:112px 1fr 52px;gap:8px;font-size:12.5px}}
/* right column: one big number, lots of air */
.mkSpreadAside{margin-top:34px}
@media(min-width:900px){.mkSpreadAside{margin-top:0;border-left:1px solid var(--line);padding-left:44px}}
.mkSpreadAside .bigX{font-family:var(--font-display);font-weight:600;font-size:clamp(58px,7vw,96px);
  line-height:.95;letter-spacing:-.03em;color:var(--text-1)}
.mkSpreadAside .cap{font-size:14px;color:var(--text-2);line-height:1.55;margin:12px 0 0;max-width:300px}
.mkSpreadAside .fig{font-size:12.5px;color:var(--text-3);line-height:1.6;margin:14px 0 0;max-width:300px}
.mkSpreadAside .fig b{font-weight:750}
.mkSpreadAside .fig .hiV{color:var(--good)}
.mkSpreadAside .fig .loV{color:var(--danger-ink)}
/* scroll-in animation. Default = bars FULL (correct with no JS and under
   prefers-reduced-motion). JS ARMS the animation by adding .js-anim to
   .mkSpread (collapses fills to scaleX(0)); the IntersectionObserver then
   adds .is-inview to .mkSpread to grow them. Inline width:X% stays the
   truth — we only scale 0→1. */
.mkSpread.js-anim .track i{transform:scaleX(0);transition:transform .8s cubic-bezier(.22,.9,.32,1)}
.mkSpread.js-anim.is-inview .track i{transform:scaleX(1)}
.mkSpread.js-anim .mkSpreadRow:nth-child(2) .track i{transition-delay:.09s}
.mkSpread.js-anim .mkSpreadRow:nth-child(3) .track i{transition-delay:.18s}
.mkSpread.js-anim .mkSpreadRow:nth-child(4) .track i{transition-delay:.27s}
.mkSpread.js-anim .mkSpreadRow:nth-child(5) .track i{transition-delay:.36s}
/* the aside rides the same trigger: gentle fade/rise after the bars start */
.mkSpread.js-anim+.mkSpreadAside{opacity:0;transform:translateY(8px);
  transition:opacity .7s ease .3s,transform .7s ease .3s}
.mkSpread.js-anim.is-inview+.mkSpreadAside{opacity:1;transform:none}
@media(prefers-reduced-motion:reduce){
  .mkSpread.js-anim .track i{transform:none;transition:none;transition-delay:0s}
  .mkSpread.js-anim+.mkSpreadAside{opacity:1;transform:none;transition:none}
}

/* AI differentiator thread */
.mkThread{padding:26px 0 0;max-width:720px}
.mkUMsg{background:var(--pz-navy);color:var(--pz-navy-ink);border-radius:16px 16px 4px 16px;padding:12px 16px;
  font-size:14.5px;font-weight:550;max-width:82%;margin-left:auto;width:fit-content}
.mkAMsg{margin-top:18px;background:var(--bg-surface);border:1px solid var(--line);border-radius:16px;
  box-shadow:var(--shadow);padding:18px 20px;font-size:14.5px}
.mkAMsg h4{font-size:15.5px;font-weight:750;margin:10px 0 6px;letter-spacing:-.01em;color:var(--text-1)}
.mkAMsg p{color:var(--text-2);margin:6px 0}
.mkAMsg p b,.mkAMsg li b{color:var(--text-1)}
.mkAMsg ul{margin:8px 0 8px 18px;color:var(--text-2);padding:0}
.mkAMsg li{margin:5px 0}
.mkAMsg .srcNote{font-size:11.5px;color:var(--text-3);border-top:1px solid var(--line);margin-top:14px;padding-top:10px}
/* CTA band */
.mkCta{max-width:1240px;margin:64px auto 0;padding:0 20px}
.mkCtaIn{background:var(--pz-navy);color:var(--pz-navy-ink);border-radius:24px;padding:52px 30px;text-align:center;
  box-shadow:var(--pz-shadow-lg)}
.mkCtaIn h2{font-family:var(--font-display);font-size:clamp(28px,3.8vw,42px);font-weight:600;letter-spacing:-.015em;margin:0}
.mkCtaIn p{opacity:.78;font-size:15px;margin:12px 0 0}
.mkCtaIn .btnHero{margin-top:26px}
/* footer */
.mkFoot{border-top:1px solid var(--line);margin-top:64px;background:var(--bg-surface-2)}
.mkFootIn{max-width:1240px;margin:0 auto;padding:44px 20px 34px;display:grid;
  grid-template-columns:1.4fr 1fr 1fr 1fr;gap:30px}
@media(max-width:900px){.mkFootIn{grid-template-columns:1fr 1fr}}
.mkFootBrand{font-weight:800;font-size:15.5px;letter-spacing:-.02em;color:var(--text-1)}
.mkFoot .about{font-size:13px;color:var(--text-2);max-width:280px;margin-top:12px;line-height:1.6}
.mkFoot h5{font-size:11px;font-weight:800;letter-spacing:.09em;text-transform:uppercase;color:var(--text-3);margin:0 0 12px}
.mkFoot a{display:block;font-size:13.5px;color:var(--text-2);text-decoration:none;padding:4px 0}
.mkFoot a:hover{color:var(--text-1)}
/* Footer "Sign in" opens a modal rather than navigating, so it is a <button>
   (2026-07-27, semantic-nav pass). It has to sit in the same column as its
   sibling links without looking like a form control, hence the reset. Kept
   next to the .mkFoot a rules it mirrors so the two cannot drift apart. */
.mkFoot .mkFootBtn{display:block;font-size:13.5px;color:var(--text-2);text-decoration:none;
  padding:4px 0;background:none;border:0;font-family:var(--font);font-weight:inherit;
  text-align:left;cursor:pointer;width:auto}
.mkFoot .mkFootBtn:hover{color:var(--text-1)}
/* Strategy receipt honesty (round-2 audit P0-01 / P2-02). The per-category
   dollar contribution and the "these two are N% of the gain" note are the
   two things whose absence let a correct $1,012.25 headline sit above a
   receipt explaining $45.90. Quiet, but never hidden. */
.st2BCatGain{font-weight:700;color:var(--text-1);white-space:nowrap}
.st2BCatMore{color:var(--text-3)}
.st2BBasis{font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
  color:var(--text-3);margin-left:4px;vertical-align:middle}
.mkLegal{border-top:1px solid var(--line);font-size:11.5px;color:var(--text-3);padding:16px 20px 22px;
  text-align:center;line-height:1.6}
/* === end REDESIGN-B PASS 2 === */

/* === WS-AIDEMO (A4): animated Ask P&P AI demo on the homepage ===
   Armed by assets/home-demo.js adding .mkDemoArmed to #mkAiDemo; without
   JS (or with prefers-reduced-motion) that class never lands and none of
   this hiding applies — the section stays fully static. Every hide here
   is opacity/transform only, so the answer card keeps its layout box for
   the whole animation and nothing below the section shifts. */
#mkAiDemo{position:relative}
/* A4 follow-up: five .mkDemoItem demos loop continuously. Statically
   (no JS / reduced motion) only the FIRST renders — the rest stay
   display:none and the page reads like the single-demo version. Armed,
   home-demo.js moves .cur between items; .out is the fade-down exit.
   All items get a shared min-height (JS, tallest wins) so the swap
   never shifts anything below the section. */
#mkAiDemo .mkDemoItem{position:relative}
#mkAiDemo .mkDemoItem:not(:first-child){display:none}
#mkAiDemo.mkDemoArmed .mkDemoItem{display:none}
#mkAiDemo.mkDemoArmed .mkDemoItem.cur{display:block;animation:mkItemIn .45s ease}
#mkAiDemo.mkDemoArmed .mkDemoItem.cur.out{opacity:0;transform:translateY(-10px);
  transition:opacity .34s ease,transform .34s ease}
@keyframes mkItemIn{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
.mkDemoCaret{display:inline-block;width:2px;height:1.05em;background:currentColor;vertical-align:-.15em;
  margin-left:3px;animation:mkCaretBlink .85s steps(1,end) infinite}
@keyframes mkCaretBlink{50%{opacity:0}}
.mkDemoArmed .mkAMsg{opacity:0;transform:translateY(12px);transition:opacity .5s ease,transform .5s ease}
.mkDemoArmed .mkAMsg.on{opacity:1;transform:none}
.mkDemoArmed .mkAMsg .pzGrounded{opacity:0;transform:scale(.82);
  transition:opacity .35s ease,transform .45s cubic-bezier(.34,1.56,.64,1)}
.mkDemoArmed .mkAMsg .pzGrounded.on{opacity:1;transform:none}
.mkDemoArmed .mkAMsg li{opacity:0;transform:translateX(-8px);transition:opacity .35s ease,transform .35s ease}
.mkDemoArmed .mkAMsg li.on{opacity:1;transform:none}
.mkDemoArmed .mkAMsg .srcNote{opacity:0;transition:opacity .6s ease}
.mkDemoArmed .mkAMsg .srcNote.on{opacity:1}
.mkDemoArmed .mkDW{opacity:0;transition:opacity .22s ease}
.mkDemoArmed .mkDW.on{opacity:1}
.mkThinking{position:absolute;left:0;display:inline-flex;gap:5px;align-items:center;
  background:var(--bg-surface);border:1px solid var(--line);border-radius:16px 16px 16px 4px;
  padding:13px 15px;box-shadow:var(--shadow);opacity:0;transform:translateY(6px);
  transition:opacity .3s ease,transform .3s ease}
.mkThinking.on{opacity:1;transform:none}
.mkThinking span{width:6px;height:6px;border-radius:50%;background:var(--text-3);
  animation:mkDotPulse 1.05s ease-in-out infinite}
.mkThinking span:nth-child(2){animation-delay:.15s}
.mkThinking span:nth-child(3){animation-delay:.3s}
@keyframes mkDotPulse{0%,100%{opacity:.35;transform:translateY(0)}50%{opacity:1;transform:translateY(-3px)}}
/* A4 follow-up: dot-nav + pause control, built by home-demo.js only when
   the loop is armed — never present in static markup. */
.mkDemoNav{display:flex;align-items:center;gap:9px;margin-top:18px;opacity:0;transition:opacity .5s ease}
.mkDemoNav.on{opacity:1}
/* 44px-tall tap area with the visual dot centered as a pseudo-element, so
   the pagination dots stay small but are comfortably tappable on mobile. */
.mkDemoDot{display:inline-flex;align-items:center;justify-content:center;width:26px;height:44px;
  padding:0;border:0;background:none;cursor:pointer}
.mkDemoDot::before{content:"";width:8px;height:8px;border-radius:50%;background:var(--line);
  transition:background .25s ease,transform .25s ease}
.mkDemoDot:hover::before{background:var(--text-3)}
.mkDemoDot.cur::before{background:var(--text-2);transform:scale(1.3)}
.mkDemoPause{margin-left:8px;background:none;border:0;padding:4px 10px;min-height:44px;display:inline-flex;align-items:center;font:inherit;font-size:12.5px;
  color:var(--text-3);cursor:pointer;transition:color .15s ease}
.mkDemoPause:hover{color:var(--text-1)}
/* A3: the closing marketing band hides once signed in (chat.js renderHomeCtas
   toggles the [hidden] attribute on #mkCloseCtaBlock). */
.mkCta[hidden]{display:none}
@media(prefers-reduced-motion:reduce){
  .mkDemoArmed .mkAMsg,.mkDemoArmed .mkAMsg .pzGrounded,.mkDemoArmed .mkAMsg li,
  .mkDemoArmed .mkAMsg .srcNote,.mkDemoArmed .mkDW{opacity:1!important;transform:none!important;
    transition:none!important;animation:none!important}
  .mkDemoCaret,.mkThinking{display:none!important}
}
/* === end WS-AIDEMO === */

/* === WS-FOLD (2026-07-21): above-the-fold hero recomposition ===
   Jeremy's ask: tagline + wallet card + the live Ask P&P AI demo reel all
   inside the FIRST viewport, no scrolling. The hero is copy+wallet on the
   left, the relocated #mkAiDemo reel in a framed panel on the right; the
   base .mkHero/.mkSub/.mkWalletCard sizes above were tightened for the
   same reason. Below 1080px it stacks tagline → wallet → demo. The
   max-height media query squeezes vertical rhythm further on short
   laptop viewports (768/720 tall) so all three still land in-fold. */
.mkHeroCopy>.mkWalletCard{margin-top:22px;max-width:480px}
.mkWalletCard .rows{margin-top:8px}
.mkWalletCard .row{padding:7px 0;font-size:12.5px}
.mkHeroDemo{min-width:0;background:var(--bg-surface-2);border:1px solid var(--line);border-radius:22px;
  padding:16px 20px 14px;box-shadow:var(--pz-shadow-lg);display:flex;flex-direction:column}
/* the reel (#mkAiDemo is the .mkThread) fills the panel so its dot-nav sits
   at the panel's bottom edge — the panel itself stretches to match the copy
   column's height (align-items:stretch on .mkHero) for a balanced fold. The
   current demo item takes auto top/bottom margins, so the conversation
   floats vertically centered in whatever height the panel got. */
.mkHeroDemo .mkThread{flex:1;display:flex;flex-direction:column}
.mkHeroDemo .mkThread .mkDemoItem{margin:auto 0}
.mkHeroDemo .mkThread .mkDemoNav{padding-top:12px}
.mkHeroDemoHead{display:flex;align-items:baseline;gap:8px;font-size:11.5px;font-weight:800;
  letter-spacing:.09em;text-transform:uppercase;color:var(--text-1)}
.mkHeroDemoHead .star{color:var(--accent-text)}
.mkHeroDemoHead .live{margin-left:auto;font-weight:600;letter-spacing:.02em;text-transform:none;
  font-size:11.5px;color:var(--text-3)}
.mkHeroDemo .mkThread{padding:12px 0 0;max-width:none}
.mkHeroDemo .mkUMsg{font-size:13px;padding:9px 13px;border-radius:14px 14px 4px 14px}
.mkHeroDemo .mkAMsg{font-size:13px;padding:13px 15px;margin-top:13px;border-radius:14px}
.mkHeroDemo .mkAMsg h4{font-size:14px;margin:8px 0 5px}
.mkHeroDemo .mkAMsg p{margin:5px 0}
.mkHeroDemo .mkAMsg ul{margin:6px 0 6px 16px}
.mkHeroDemo .mkAMsg li{margin:4px 0}
.mkHeroDemo .mkAMsg .srcNote{font-size:10.5px;margin-top:10px;padding-top:8px}
.mkHeroDemo .mkDemoNav{margin-top:12px}
@media(max-width:1080px){
  .mkHeroCopy>.mkWalletCard{max-width:none}
  .mkHeroDemo{padding:14px 16px 12px}
}
/* short laptop viewports (e.g. 1366x768, 1280x720): tighten everything so
   tagline + wallet + demo genuinely fit without scrolling */
@media(min-width:1081px) and (max-height:830px){
  .mkHero{padding-top:18px;gap:40px}
  .mkHero h1{font-size:clamp(26px,2.4vw,33px)}
  .mkSub{font-size:14px;margin-top:10px;max-width:440px}
  .mkCtas{margin-top:14px}
  .mkCtas .btnHero{font-size:14px;padding:11px 18px;border-radius:12px}
  .mkCtas .btnHeroGhost{font-size:13.5px;padding:10px 15px;border-radius:12px}
  .mkTrust{margin-top:12px;font-size:11.5px}
  .mkHeroCopy>.mkWalletCard{margin-top:16px;padding:13px 16px 9px}
  .mkWalletCard .big{font-size:24px}
  .mkWalletCard .big .u{font-size:12px}
  .mkWalletCard .row{padding:5px 0;font-size:12px}
  .mkHeroDemo .mkThread{padding:10px 0 0}
  .mkHeroDemo .mkUMsg{font-size:12.5px}
  .mkHeroDemo .mkAMsg{font-size:12.5px;padding:12px 14px;margin-top:11px}
  .mkHeroDemo .mkAMsg h4{font-size:13.5px}
  .mkHeroDemo .mkAMsg .srcNote{margin-top:8px;padding-top:7px}
  .mkHeroDemo .mkDemoNav{margin-top:9px}
}
/* phones: tighten the stack so tagline + the whole wallet card land inside
   one ~844px viewport (the demo follows right below, one swipe away) */
@media(max-width:640px){
  .mkHero{padding-top:20px;gap:26px}
  .mkHero h1{font-size:28px;margin-top:10px}
  .mkSub{font-size:14.5px;margin-top:10px}
  .mkCtas{margin-top:14px}
  .mkCtas .btnHero{font-size:14px;padding:12px 20px}
  .mkCtas .btnHeroGhost{font-size:13.5px;padding:11px 17px}
  .mkTrust{margin-top:11px;font-size:11.5px;gap:8px 14px}
  .mkHeroCopy>.mkWalletCard{margin-top:14px;padding:13px 15px 9px}
  .mkWalletCard .big{font-size:26px}
  .mkWalletCard .big .u{font-size:12px;margin-top:2px}
  .mkWalletCard .row{padding:6px 0;font-size:12px}
}
/* === end WS-FOLD === */

/* === HOME-JOURNEY (2026-07-22): a numbered 3-step journey rail, a scroll
   cue, and a recessed "coming soon" section for the logged-out homepage.
   Token-only (works light + dark with no per-theme literals except the
   decorative arrow/check glyphs), restrained motion, and following the
   existing .mkSec idiom (centered 1240 column, kicker + display H2). Rail
   is 3-up on desktop, stacks vertically at <=760px. === */

/* --- 1. numbered 3-step journey rail --- */
.mkSteps{max-width:1240px;margin:0 auto;padding:60px 20px 0}
.mkSteps .secKick,.mkSoon .secKick{font-size:11.5px;font-weight:800;letter-spacing:.13em;
  text-transform:uppercase;color:var(--accent-text)}
.mkSteps>h2,.mkSoon>h2{font-family:var(--font-display);font-size:clamp(26px,3.4vw,38px);font-weight:600;
  letter-spacing:-.015em;line-height:1.12;margin:10px 0 0;max-width:640px;color:var(--text-1)}
.mkStepsRail{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:30px;align-items:stretch}
.mkStep{position:relative;background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--radius);
  padding:22px 22px 20px;transition:opacity .2s ease}
.mkStepNum{font-family:var(--font-display);font-size:34px;font-weight:600;line-height:1;letter-spacing:-.02em;
  color:var(--accent-text)}
.mkStepTitle{font-size:17px;font-weight:700;letter-spacing:-.01em;color:var(--text-1);margin:12px 0 0}
.mkStepBody{font-size:14px;color:var(--text-2);line-height:1.55;margin:7px 0 0}
.mkStepLink{display:inline-block;font-size:13px;font-weight:650;color:var(--accent-text);
  text-decoration:none;margin-top:12px;cursor:pointer}
.mkStepLink:hover{text-decoration:underline}
.mkStepTime{display:inline-block;font-size:11px;font-weight:700;color:var(--text-3);background:var(--bg-surface-2);
  border-radius:999px;padding:3px 9px;margin-top:14px;white-space:nowrap}
/* decorative connector sitting in the inter-card gap — desktop only, → */
.mkStepArrow{position:absolute;top:50%;right:-7px;transform:translate(50%,-50%);z-index:1;pointer-events:none;
  display:flex;align-items:center;gap:2px;color:var(--text-3);font-size:13px;line-height:1}
.mkStepArrow::before{content:"";width:12px;height:1px;background:var(--line-strong)}
.mkStepArrow::after{content:"\2192"}
.mkStep:last-child .mkStepArrow{display:none}
/* signed-in checklist variant: dim the card and turn the numeral green (JS
   swaps the glyph to a ✓; the token here supplies the check tone) */
.mkStep.is-done{opacity:.68}
.mkStep.is-done .mkStepNum{color:var(--good-text)}
@media(max-width:760px){
  .mkSteps{padding-top:48px}
  .mkStepsRail{grid-template-columns:1fr;gap:26px}
  /* rotate the connector to a downward ↓ centered in the vertical gap */
  .mkStepArrow{top:auto;bottom:-20px;left:50%;right:auto;transform:translateX(-50%) rotate(90deg)}
}

/* --- 2. scroll cue --- */
.mkScrollCue{display:flex;flex-direction:column;align-items:center;gap:6px;margin:26px auto 10px;
  padding:8px 14px;background:none;border:0;cursor:pointer;font:inherit;color:inherit}
.mkScrollCueText{font-size:12px;letter-spacing:.02em;color:var(--text-3)}
.mkScrollCueChev{font-size:20px;line-height:1;color:var(--text-2);animation:mkCueBob 2s ease-in-out infinite}
@keyframes mkCueBob{0%,100%{transform:translateY(0)}50%{transform:translateY(4px)}}
.mkScrollCue--gone{display:none}
@media(prefers-reduced-motion:reduce){.mkScrollCueChev{animation:none}}

/* --- 3. coming soon (recessed, "not yet live") --- */
.mkSoon{max-width:1240px;margin:0 auto;padding:60px 20px 0}
.mkSoonSub{color:var(--text-2);font-size:15px;max-width:600px;margin-top:12px;line-height:1.6}
.mkSoonGrid{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:30px}
.mkSoonCard{background:var(--bg-surface-2);border:1px dashed var(--line-strong);border-radius:var(--radius);
  padding:22px;box-shadow:none}
.soonPill{display:inline-block;font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
  color:var(--text-3);background:var(--bg-surface);border:1px solid var(--line);border-radius:999px;padding:3px 9px}
.mkSoonTitle{font-size:16px;font-weight:700;letter-spacing:-.01em;color:var(--text-1);margin:12px 0 0}
.mkSoonBody{font-size:13.5px;color:var(--text-2);line-height:1.5;margin:7px 0 0}
@media(max-width:760px){
  .mkSoon{padding-top:48px}
  .mkSoonGrid{grid-template-columns:1fr}
}

/* dataHealthChip inside .mkStrip: retired from view (2026-07-22). Its JS
   render ("35 programs · verified {date}") duplicated the strip's static
   line verbatim — two counts + a doubled "verified {date}" in one band.
   The element and id STAY in the DOM (chat.js renderDataHealthChip() and
   the prerendered subpages still target it); display:none outranks the
   el.hidden=false un-hide in chat.js, so JS keeps working and renders
   nothing here. The strip's static "how we verify →" link carries the
   chip's old tap-to-methodology affordance instead. */
.mkStrip .dataHealthChip{display:none}

/* Ask-AI gate (#/ask empty state, signed-in + wallet not built out yet) */
.askGateList{list-style:none;margin:14px 0 20px;padding:0;display:flex;flex-direction:column;gap:8px;text-align:left;max-width:340px}
.askGateItem{display:flex;align-items:center;gap:10px;font-size:14.5px;font-weight:600;color:var(--text-2)}
.askGateItem.is-done{color:var(--text-1)}
.askGateCheck{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;flex:none;
  border-radius:50%;font-size:13px;font-weight:800;background:var(--bg-surface-2);color:var(--text-3)}
.askGateItem.is-done .askGateCheck{background:rgba(15,118,110,.14);color:var(--good-text)}
.askGateBtn{margin-top:4px}
/* === end HOME-JOURNEY === */

/* === #5 HOMEPAGE HERO REDESIGN (2026-07-23) ===
   Jeremy: the very top was the one flat moment on a page whose lower
   sections earn their color and motion — and the hero-to-"Three steps"
   gap read as dead space. This block (a) gives the hero the same design
   language as the sections below it — token-only color, the shared
   cubic-bezier(.22,.9,.32,1) grow, one restrained gradient moment — and
   (b) tightens the hero→steps rhythm. Everything here is page-scoped
   (.mkHome / .mkHero--v2 / new mk* class names); no shared or base rule
   is edited, no :root token is redefined. Motion discipline matches the
   rest of the file: the no-motion default IS the final state, and every
   animation lives inside prefers-reduced-motion:no-preference. */

/* -- 1. rhythm: close the dead air between the hero and #mkSteps.
   Was 26px hero bottom + 60px steps top (=86px desktop); now ~38px.
   .mkHome-scoped so the .mkSteps base rule (also used by breakpoints)
   is outranked by specificity, not by edit. -- */
.mkHome .mkHero{position:relative;padding-bottom:8px}
@media(max-width:1080px){.mkHome .mkHero{padding-bottom:6px}}
.mkHome .mkSteps{padding-top:30px}
@media(max-width:760px){.mkHome .mkSteps{padding-top:24px}}

/* -- 2./3. RETIRED (concept-A aurora orbs + floating stat chips): concept
   C won the 2026-07-23 review, the .mkHeroFx/.mkOrb/.mkHeroChip markup is
   deleted from index.html, and their rules (plus their drift/float motion
   in part 7 and the mkOrbDrift/mkChipFloat keyframes) are gone. -- */

/* -- 4. kicker becomes a soft gold pill with the brand star (echoes
   .mkHeroDemoHead's ✦). Inline-flex, so it centers with the desktop
   text-align and left-aligns in the mobile stack unchanged. -- */
.mkHome .mkKicker{display:inline-flex;align-items:center;gap:7px;
  background:var(--accent-soft);border:1px solid var(--line);
  border-radius:999px;padding:6px 13px}
.mkKickStar{color:var(--accent-text);line-height:1}

/* -- 5. the one typographic moment: "really" carries a bronze→gold
   gradient (the page's single background-clip:text use, same trick as
   the wallet finish headline) and a drawn underline that grows in with
   the spread-bars' easing. Fallback when background-clip:text is
   unsupported: plain --accent-text, exactly what it was before. -- */
.mkHome .mkHero h1 em{position:relative;display:inline-block;
  background:linear-gradient(97deg,var(--accent-text) 0%,var(--accent) 50%,var(--accent-text) 100%);
  background-size:200% 100%;-webkit-background-clip:text;background-clip:text;
  -webkit-text-fill-color:transparent;color:var(--accent-text)}
@supports not ((-webkit-background-clip:text) or (background-clip:text)){
  .mkHome .mkHero h1 em{background:none;-webkit-text-fill-color:currentColor;color:var(--accent-text)}
}
.mkHome .mkHero h1 em::after{content:"";position:absolute;left:2%;right:2%;bottom:-.06em;
  height:.08em;border-radius:99px;
  background:linear-gradient(90deg,var(--accent),var(--accent-text))}

/* -- 6. CTA polish: hover lift on both buttons, accent ring on the ghost.
   Applies equally to the static markup and the renderHomeCtas() rebuilds
   (same class names). -- */
.mkHome .btnHero{transition:transform .16s ease,box-shadow .16s ease}
.mkHome .btnHero:hover{transform:translateY(-1px)}
.mkHome .btnHeroGhost{transition:transform .16s ease,border-color .16s ease,background .16s ease}
.mkHome .btnHeroGhost:hover{transform:translateY(-1px);border-color:var(--accent);background:var(--accent-soft)}

/* -- 7. ALL motion, gated. Default state above is the finished frame;
   reduced-motion users get a static (but still colorful) hero. -- */
@media(prefers-reduced-motion:no-preference){
  /* staggered entrance: kicker → h1 → sub → CTAs → trust rise in on the
     same curve the page uses everywhere else */
  .mkHome .mkHeroCopy>*{animation:mkHeroRise .55s cubic-bezier(.22,.9,.32,1) both}
  .mkHome .mkHeroCopy>*:nth-child(2){animation-delay:.08s}
  .mkHome .mkHeroCopy>*:nth-child(3){animation-delay:.16s}
  .mkHome .mkHeroCopy>*:nth-child(4){animation-delay:.24s}
  .mkHome .mkHeroCopy>*:nth-child(5){animation-delay:.32s}
  /* kicker star twinkles like the demo panel's */
  .mkKickStar{animation:mkStarTwinkle 2.6s ease-in-out infinite}
  /* gradient word shimmers; its underline draws itself once, after the
     headline has risen */
  .mkHome .mkHero h1 em{animation:mkEmSheen 6s ease-in-out 1.4s infinite}
  .mkHome .mkHero h1 em::after{transform:scaleX(0);transform-origin:left center;
    animation:mkEmUnderline .7s cubic-bezier(.22,.9,.32,1) .6s forwards}
  /* one sheen sweep across the primary CTA, once, ~1s in */
  .mkHome .btnHero{position:relative;overflow:hidden}
  .mkHome .btnHero::after{content:"";position:absolute;top:0;bottom:0;left:-45%;width:36%;
    background:linear-gradient(105deg,transparent,rgba(255,255,255,.32),transparent);
    transform:skewX(-18deg);animation:mkCtaSheen 1.1s ease 1s 1 both;pointer-events:none}
}
@keyframes mkHeroRise{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
@keyframes mkStarTwinkle{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(.8)}}
@keyframes mkEmSheen{0%,100%{background-position:0% 0}50%{background-position:100% 0}}
@keyframes mkEmUnderline{to{transform:scaleX(1)}}
@keyframes mkCtaSheen{from{transform:translateX(0) skewX(-18deg)}to{transform:translateX(460%) skewX(-18deg)}}

/* -- 8. THE HERO PANEL — concept C, "Valued, live", FINALIZED 2026-07-23.
   Jeremy reviewed three switchable concepts (?hero=a|b|c) on one preview
   and chose C, so the data-hero attribute, the switcher script, and the
   concept A (aurora orbs/chips) + concept B ("The Ledger" masthead/ghost)
   markup and rules are all deleted; every selector below is de-keyed from
   [data-hero="c"] and applies unconditionally. Product-forward split: copy
   left; on the right, the example wallet being valued in real time — the
   total counts up (inline script after the hero markup), the per-program
   bars grow with the exact track/fill construction + easing of the spread
   bars in #mkMath, and the panel ends on the hand-off to Strategy. The
   panel's own soft accent glow is the only atmosphere. -- */
/* no margin-top: below 1081px the .mkHero grid's own 34px row-gap is the
   copy→panel air; above, the split's align-items:center handles it */
.mkHero .mkcPanel{display:block;position:relative;background:var(--bg-surface);
  border:1px solid var(--line);border-radius:20px;box-shadow:var(--pz-shadow-lg);
  padding:20px 22px 16px;overflow:hidden}
.mkHero .mkcPanel::before{content:"";position:absolute;top:-70px;right:-60px;
  width:230px;height:230px;border-radius:50%;pointer-events:none;
  background:radial-gradient(circle,var(--accent) 0%,transparent 68%);opacity:.14;filter:blur(6px)}
.mkcHead{display:flex;align-items:center;gap:8px;font-size:11px;font-weight:700;
  letter-spacing:.08em;text-transform:uppercase;color:var(--text-3)}
.mkcTotal{margin-top:8px}
.mkcTotalNum{font-family:var(--font-display);font-size:38px;font-weight:600;
  letter-spacing:-.02em;color:var(--text-1);font-variant-numeric:tabular-nums}
.mkcTotalU{display:block;font-size:13px;font-weight:600;color:var(--text-2);margin-top:2px}
.mkcRows{margin-top:14px;border-top:1px solid var(--line);padding-top:4px}
.mkcRow{display:grid;grid-template-columns:158px 1fr 56px;gap:10px;align-items:center;
  padding:7px 0;font-size:12.5px;font-weight:600;color:var(--text-1)}
.mkcRow .n{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mkcRow .track{height:8px;border-radius:99px;background:var(--bg-deep);overflow:hidden}
.mkcRow .track i{display:block;height:100%;border-radius:99px;background:var(--accent);
  transform-origin:left center}
.mkcRow .track i.hi{background:var(--good)}
.mkcRow .v{text-align:right;font-weight:750;font-variant-numeric:tabular-nums}
.mkcPlan{margin-top:12px;border-top:1px solid var(--line);padding-top:12px;
  display:flex;align-items:baseline;gap:8px;font-size:12.5px;line-height:1.5;color:var(--text-2)}
.mkcPlan b{color:var(--text-1)}
.mkcPlan .arr{color:var(--accent-text);font-weight:800;flex:0 0 auto}
@media(max-width:480px){.mkcRow{grid-template-columns:118px 1fr 50px;gap:8px;font-size:12px}}
/* desktop: true split — copy left-aligned, panel right, both centered on
   the shared axis (outranks the centered-copy base rule, ~line 3536, by
   specificity: these .mkHome .mkHero-scoped selectors beat the bare
   .mkHeroCopy rules) */
@media(min-width:1081px){
  .mkHome .mkHero{grid-template-columns:1.02fr .98fr;gap:56px;align-items:center}
  .mkHome .mkHero .mkHeroCopy{max-width:560px;margin:0;text-align:left}
  .mkHome .mkHero .mkHeroCopy .mkSub{margin-left:0;margin-right:0}
  .mkHome .mkHero .mkHeroCopy .mkCtas,
  .mkHome .mkHero .mkHeroCopy .mkTrust{justify-content:flex-start}
  .mkHero .mkcPanel{max-width:520px;width:100%;justify-self:end}
}

/* -- 9. panel motion, same gate + curve as everything else in this block
   (concept B's masthead/ghost motion + mkbGhostIn keyframe deleted with
   the concept). Defaults above are the finished frames; reduced-motion =
   static. Panel rises, rows stagger in, bars grow after their row lands,
   the plan line arrives last — the wallet visibly becomes a plan. -- */
@media(prefers-reduced-motion:no-preference){
  .mkHero .mkcPanel{animation:mkHeroRise .55s cubic-bezier(.22,.9,.32,1) .18s both}
  .mkHero .mkcRow{animation:mkHeroRise .45s cubic-bezier(.22,.9,.32,1) both}
  .mkHero .mkcRow:nth-child(2){animation-delay:.47s}
  .mkHero .mkcRow:nth-child(3){animation-delay:.56s}
  .mkHero .mkcRow:nth-child(4){animation-delay:.65s}
  .mkHero .mkcRow:nth-child(1){animation-delay:.38s}
  .mkHero .mkcRow .track i{animation:mkcGrow .8s cubic-bezier(.22,.9,.32,1) both}
  .mkHero .mkcRow:nth-child(1) .track i{animation-delay:.5s}
  .mkHero .mkcRow:nth-child(2) .track i{animation-delay:.59s}
  .mkHero .mkcRow:nth-child(3) .track i{animation-delay:.68s}
  .mkHero .mkcRow:nth-child(4) .track i{animation-delay:.77s}
  .mkHero .mkcPlan{animation:mkHeroRise .5s cubic-bezier(.22,.9,.32,1) 1.05s both}
}
@keyframes mkcGrow{from{transform:scaleX(0)}to{transform:scaleX(1)}}

/* -- 10. VERIFIED-DATA PROOF BAND (#mkStrip), 2026-07-23. Jeremy: the
   old strip "seems so lost... a leftover from an older version". Two
   real bugs behind that read: (a) the vertical mis-centering was
   align-items:baseline across four fragments in three different font
   sizes, and (b) it was the only band on the page with no kicker/claim
   structure — just comma-separated fragments. Reframed as the proof
   header for the math sections below it (same slot; #mkScrollCue still
   lands here): kicker + one bold claim + methodology link left, three
   aligned stat cells right, the whole thing grid-CENTERED. Chrome
   matches the #5 language — surface card, 20px radius (.mkBigEq /
   .mkcPanel), --pz-shadow-lg lift, one soft token-colored glow — so it
   holds up under hero concepts a, b, and c (it is page-scoped, not
   hero-keyed). Token-only color; no new motion (mid-page, static
   finished frame is the correct no-JS/reduced-motion state anyway).
   The old .mkStripIn/.lead/.stat/.mkStripLink rules (~line 3580) are
   now unreferenced by markup — safe to delete in a cleanup pass. -- */
.mkProof{position:relative;background:var(--bg-surface);border:1px solid var(--line);
  border-radius:20px;box-shadow:var(--pz-shadow-lg);padding:24px 26px;overflow:hidden;
  display:grid;grid-template-columns:1fr;gap:18px}
.mkProof::before{content:"";position:absolute;top:-80px;right:-70px;width:240px;height:240px;
  border-radius:50%;pointer-events:none;filter:blur(6px);
  background:radial-gradient(circle,var(--accent) 0%,transparent 68%);opacity:.12}
.mkProofLead{position:relative;min-width:0}
.mkProofLead .secKick{font-size:11px;font-weight:800;letter-spacing:.13em;
  text-transform:uppercase;color:var(--accent-text)}
.mkProofClaim{font-family:var(--font-display);font-size:19px;font-weight:600;
  letter-spacing:-.01em;line-height:1.3;margin:8px 0 0;color:var(--text-1)}
.mkProofLink{display:inline-block;margin-top:10px;font-size:12.5px;font-weight:650;
  color:var(--accent-text);cursor:pointer;text-decoration:none;white-space:nowrap}
.mkProofLink:hover{text-decoration:underline}
.mkProofStats{position:relative;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
  gap:16px 24px;align-items:center}
.mkProofStat{display:flex;flex-direction:column;gap:3px;min-width:0}
.mkProofStat .n{font-family:var(--font-display);font-size:21px;font-weight:600;
  letter-spacing:-.015em;line-height:1.15;color:var(--text-1);font-variant-numeric:tabular-nums}
.mkProofStat .c{font-size:12.5px;color:var(--text-2);line-height:1.5}
.mkProofStat .c b{color:var(--text-1);font-weight:650}
/* desktop: true 4-cell trust bar — lead | stat | stat | stat, one shared
   vertical center line, hairline dividers between every cell */
@media(min-width:1081px){
  .mkProof{grid-template-columns:1.25fr 2fr;gap:32px;align-items:center;padding:26px 34px}
  .mkProofStats{border-left:1px solid var(--line);padding-left:32px;gap:20px 26px}
  .mkProofStat+.mkProofStat{border-left:1px solid var(--line);padding-left:26px}
}
/* tablet (641–1080): lead full-width, the three stats keep a 3-up row
   under a hairline top rule */
@media(min-width:641px) and (max-width:1080px){
  .mkProofStats{border-top:1px solid var(--line);padding-top:16px}
  .mkProofStat+.mkProofStat{border-left:1px solid var(--line);padding-left:24px}
}
/* phone (<=640): stats become ruled rows reading as sentences —
   "35 | programs verified {date}" — baseline-set as ONE text line, so
   nothing is off-center in a single-axis row */
@media(max-width:640px){
  .mkProof{padding:20px;gap:14px}
  .mkProofStats{grid-template-columns:1fr;gap:0;border-top:1px solid var(--line);padding-top:2px}
  .mkProofStat{flex-direction:row;align-items:baseline;gap:10px;padding:10px 0;
    border-bottom:1px solid var(--line)}
  .mkProofStat:last-child{border-bottom:0;padding-bottom:2px}
  .mkProofStat .n{font-size:17px;flex:0 0 auto}
  .mkProofStat .c{flex:1 1 auto}
}
/* === end #5 HOMEPAGE HERO REDESIGN === */

/* === A2 SIGNED-IN HOME (2026-07-21): the signed-in dashboard ===
   Rendered by chat.js renderHomeSignedIn() into #homeSignedIn; the
   marketing page (#homeMarketing) hides via [hidden] while it shows.
   Everything themes through the semantic tokens (both palettes), and the
   only motion — a one-shot card entrance — is disabled under
   prefers-reduced-motion. Bottom padding clears the sitewide Ask dock. */
.shHome[hidden],.mkHome[hidden]{display:none}
.shHome{max-width:1240px;margin:0 auto;padding:34px 20px 150px;font-size:15px}
.shHome a{cursor:pointer}
.shHead{display:flex;align-items:flex-end;justify-content:space-between;gap:18px 26px;flex-wrap:wrap}
.shHeadCopy{min-width:0}
.shKicker{font-size:11.5px;font-weight:800;letter-spacing:.13em;text-transform:uppercase;color:var(--accent-text)}
.shHead h1{font-family:var(--font-display);font-size:clamp(27px,3vw,38px);font-weight:600;
  letter-spacing:-.02em;line-height:1.1;margin:10px 0 0;color:var(--text-1)}
.shSub{color:var(--text-2);font-size:14.5px;line-height:1.6;margin:8px 0 0;max-width:560px}
.shHeadCtas{display:flex;gap:10px;flex-wrap:wrap;padding-bottom:4px}
/* A2 refinement (2026-07-21): "Your next moves" outranks "Market updates".
   The grid now holds ONLY the wallet+budget summary row; the advice card
   sits directly under it as a prominent full-width band (.shBand.shPrime,
   accent top rule), and Market Updates is demoted to a plain full-width
   band below that. Below 981px everything is one column in DOM order
   (wallet, budget, moves, updates), discoverable from the top via the
   .shJump chips. */
.shGrid{display:grid;grid-template-columns:1.06fr .94fr;gap:16px;margin-top:22px}
@media(max-width:980px){ .shGrid{grid-template-columns:1fr} }
.shBand{margin-top:16px}
.shCard.shPrime{border-color:var(--line-strong);border-top:3px solid var(--accent)}
@media(min-width:981px){
  /* wide bands: lay the item lists out two-up so the full width earns its
     keep; the kickers/heads carry the separation, so drop the row rules */
  .shPrime .shAList{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:2px 36px}
  .shPrime .shAItem{border-bottom:0;padding:10px 0}
  #shdUpdates .shRows{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:2px 36px}
  #shdUpdates .shURow{border-bottom:0}
}
/* jump chips — a single-column (phone/tablet) affordance so the sections
   below the fold are reachable without blind scrolling; hidden on desktop
   where the hierarchy is visible at a glance. Motion comes only from
   shdJump's scrollIntoView, which drops to 'auto' under reduced motion. */
.shJump{display:flex;gap:8px;flex-wrap:wrap;margin-top:16px}
/* A-10 (2026-08-03, MEASURED on the signed-in dashboard at 390): 37.38px
   tall, under the 44px product standard (owner ruling #4). The dashboard
   only exists behind authUser, so the 16-route behavioural probe that found
   the other 141 targets never sampled it. build/check-tap-targets.mjs now
   guards this selector, and build/check-dashboard-ui.mjs measures the real
   box on a signed-in render. */
.shJumpChip{display:inline-flex;align-items:center;gap:6px;font-size:12.5px;font-weight:700;
  color:var(--text-1);background:var(--bg-surface);border:1px solid var(--line-strong);
  border-radius:999px;padding:8px 14px;min-height:44px;text-decoration:none}
.shJumpChip span{color:var(--accent-text)}
@media(min-width:981px){.shJump{display:none}}
#shdAdvice,#shdUpdates{scroll-margin-top:74px}
.shCard{background:var(--bg-surface);border:1px solid var(--line);border-radius:18px;
  box-shadow:var(--shadow);padding:20px 22px 18px;min-width:0;display:flex;flex-direction:column}
.shLbl{font-size:11px;font-weight:800;letter-spacing:.09em;text-transform:uppercase;color:var(--text-3)}
.shLbl .shStar{color:var(--accent-text)}
.shBig{font-family:var(--font-display);font-size:34px;font-weight:600;letter-spacing:-.02em;
  margin-top:6px;color:var(--text-1)}
.shBig .u{font-size:13.5px;color:var(--text-2);font-family:var(--font);font-weight:600;letter-spacing:0}
@media(max-width:640px){.shBig .u{display:block;margin-top:4px}}
.shRows{margin-top:14px;border-top:1px solid var(--line)}
.shRow{display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid var(--line);
  font-size:13.5px;font-weight:600;color:var(--text-1)}
.shRow:last-child{border-bottom:0}
.shMark{flex:0 0 auto;width:28px;height:28px;border-radius:9px;color:#fff;font-size:10px;
  font-weight:800;display:grid;place-items:center;letter-spacing:.02em}
.shRowName{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.shRowVal{margin-left:auto;text-align:right;font-weight:750;flex:0 0 auto}
.shRowVal span{display:block;font-size:11px;color:var(--text-3);font-weight:500}
.shMore{font-size:12px;color:var(--text-3);margin:8px 0 0}
.shFoot{display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin-top:auto;padding-top:16px}
/* A-10 (2026-08-03, MEASURED on the signed-in dashboard at 390): .shBtn.ghost
   was 42.92px tall and .shLink 20.14px — both under the 44px product standard
   (owner ruling #4), and neither was in check-tap-targets' REQUIRED list
   because the behavioural probe only sampled signed-OUT routes.
   .shLink becomes an inline-flex box so min-height can apply at all: it is a
   standalone control sitting in the card's .shFoot action row, NOT a link
   inside a sentence, so the WCAG 2.5.8 inline-target exemption does not
   cover it. (The one dashboard link that IS mid-sentence — "Add it →" in the
   advice card — deliberately keeps no class and is not enlarged.)
   .shBtn.ghost carries the min-height too, redundantly with the base rule:
   the compound selector is what was MEASURED, so it is what the gate names,
   and a compound rule beating the base one on specificity is exactly how a
   227x40 target survived a passing gate before. */
.shBtn{display:inline-flex;align-items:center;gap:7px;background:var(--pz-cta);color:var(--pz-cta-ink);
  font-weight:700;font-size:13.5px;border-radius:11px;padding:10px 16px;min-height:44px;text-decoration:none}
.shBtn.ghost{background:var(--bg-surface);color:var(--text-1);border:1px solid var(--line-strong);font-weight:650;min-height:44px}
.shLink{display:inline-flex;align-items:center;min-height:44px;font-size:13px;font-weight:650;color:var(--accent-text);text-decoration:none}
.shEmpty{margin-top:12px;border:1px dashed var(--line-strong);border-radius:14px;
  padding:16px 18px;background:var(--bg-surface-2)}
.shEmpty p{margin:0 0 14px;font-size:13.5px;line-height:1.6;color:var(--text-2)}
.shEmpty p b{color:var(--text-1)}
/* budget bars */
.shBRow{display:grid;grid-template-columns:118px 1fr 84px;gap:12px;align-items:center;
  padding:8px 0;border-bottom:1px solid var(--line);font-size:13px;color:var(--text-1);font-weight:600}
.shBRow:last-child{border-bottom:0}
.shBName{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.shBTrack{height:8px;border-radius:99px;background:var(--bg-deep);overflow:hidden}
.shBTrack i{display:block;height:100%;border-radius:99px;background:var(--accent)}
.shBVal{text-align:right;font-weight:750}
.shBVal span{color:var(--text-3);font-weight:500;font-size:11px}
@media(max-width:400px){.shBRow{grid-template-columns:96px 1fr 76px;gap:8px}}
/* advice */
.shAList{margin-top:6px}
.shAItem{padding:12px 0;border-bottom:1px solid var(--line)}
.shAItem:last-child{border-bottom:0}
.shAKick{font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--accent-text)}
.shAItem p{margin:5px 0 0;font-size:13.5px;line-height:1.6;color:var(--text-2)}
.shAItem p b{color:var(--text-1)}
.shAItem a{color:var(--accent-text);font-weight:650}
.shFine{font-size:11.5px;color:var(--text-3);border-top:1px solid var(--line);
  margin:14px 0 0;padding-top:10px;line-height:1.55}
/* market updates */
.shScope{font-size:11.5px;color:var(--text-3);margin:6px 0 0}
.shURow{padding:11px 0;border-bottom:1px solid var(--line)}
.shURow:last-child{border-bottom:0}
.shUHead{display:flex;justify-content:space-between;gap:6px 12px;align-items:baseline;
  flex-wrap:wrap;font-size:13.5px;color:var(--text-1)}
.shUDate{font-size:11.5px;color:var(--text-3);white-space:nowrap}
.shUWhat{margin:4px 0 0;font-size:12.5px;line-height:1.55;color:var(--text-2);
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.shSite{margin:12px 0 0;border-top:1px solid var(--line);padding-top:10px;font-size:12px;
  color:var(--text-2);line-height:1.55;display:-webkit-box;-webkit-line-clamp:2;
  -webkit-box-orient:vertical;overflow:hidden}
.shSite b{color:var(--text-1)}
/* one-shot entrance, staggered per card; motion-safe */
.shCard{animation:shdCardIn .38s ease both}
#shdBudget{animation-delay:.05s}
#shdAdvice{animation-delay:.1s}
#shdUpdates{animation-delay:.15s}
@keyframes shdCardIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
@media(prefers-reduced-motion:reduce){.shCard{animation:none}}
/* phones */
@media(max-width:640px){
  .shHome{padding-top:24px}
  .shHead h1{font-size:26px}
  .shHeadCtas .btnHero{font-size:14px;padding:12px 18px}
  .shHeadCtas .btnHeroGhost{font-size:13.5px;padding:11px 15px}
  .shBig{font-size:28px}
  .shCard{padding:16px 16px 14px}
  .shFoot{gap:12px}
}
/* === end A2 SIGNED-IN HOME === */

/* === ADMIN DASHBOARD (/admin, assets/admin.js) ===
   Admin-only: registered users, signups over time, active users, AI usage,
   top questions, feedback. Reuses existing tokens (--bg-surface, --line,
   --radius, --shadow, --text-*, --accent*) so it inherits light/dark for
   free -- no new theme variables. Div-based bar "charts" (no SVG/canvas,
   no new dependency) sized by inline style percentages set in admin.js. */
.adminGrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin:18px 0}
.adminStat{background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--radius);padding:14px 16px;box-shadow:var(--shadow)}
.adminStat .adminStatNum{font-size:24px;font-weight:800;color:var(--text-1);line-height:1.15}
.adminStat .adminStatLabel{font-size:12px;color:var(--text-3);margin-top:3px}
.adminSection{margin:26px 0}
.adminSection h3{font-size:14px;margin:0 0 10px;color:var(--text-1);font-weight:700}
.adminSection .muted{margin-top:-4px}
.adminBars{display:flex;align-items:flex-end;gap:3px;height:84px;padding:6px 0;border-bottom:1px solid var(--line)}
/* Admin v2 (Jeremy: "bars are too light"): solid --accent gold fill, not the
   near-invisible 14%-alpha --accent-soft tint — reads clearly on both the
   light and dark surfaces. Hover brightens + keys the bronze/gold text
   accent as an outline so the inspected day still stands out. */
.adminBar{flex:1;background:var(--accent);border-radius:2px 2px 0 0;min-height:2px;transition:filter .15s,box-shadow .15s}
.adminBar:hover{filter:brightness(1.12);box-shadow:inset 0 0 0 1px var(--accent-text)}
.adminBarsCaption{display:flex;justify-content:space-between;font-size:10.5px;color:var(--text-4);margin-top:4px}
.adminList{display:flex;flex-direction:column;gap:8px}
.adminListRow{background:var(--bg-surface);border:1px solid var(--line);border-radius:10px;padding:10px 14px;font-size:13px;color:var(--text-2)}
.adminListRow .adminListMeta{font-size:11px;color:var(--text-3);margin-top:4px;display:flex;gap:8px;flex-wrap:wrap}
.adminListEmpty{font-size:13px;color:var(--text-3);padding:10px 0}
.adminGeoRowTop{display:flex;align-items:baseline;justify-content:space-between;gap:10px;font-size:13px;color:var(--text-2)}
.adminGeoBarTrack{height:6px;border-radius:999px;background:var(--line);margin-top:6px;overflow:hidden}
.adminGeoBarFill{height:100%;background:var(--accent);border-radius:999px}
.adminStatusPill{display:inline-block;font-size:10px;font-weight:700;padding:1px 7px;border-radius:999px;background:var(--accent-soft);color:var(--accent-text);text-transform:uppercase;letter-spacing:.03em}
/* feedback triage (Feedback & Moderation tab): status pills by state + a
   filter strip + per-row New/In progress/Done set buttons. */
.adminStatusPill--new{background:var(--accent-soft);color:var(--accent-text)}
.adminStatusPill--in_progress{background:var(--brand-soft);color:var(--brand-ink)}
.adminStatusPill--done{background:var(--good, #1c7d4d);color:#fff}
.adminFbFilters{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px}
.adminFbFilter{appearance:none;font:inherit;font-size:12.5px;font-weight:600;color:var(--text-2);background:var(--bg-surface-2);border:1px solid var(--line);border-radius:999px;padding:6px 12px;cursor:pointer}
.adminFbFilter:hover{border-color:var(--accent)}
.adminFbFilter.on{color:var(--accent-on);background:var(--accent);border-color:var(--accent)}
.adminFbFilterN{opacity:.75;font-weight:700;margin-left:2px}
.adminFbRow{display:flex;flex-direction:column;gap:8px}
.adminFbMsgText{font-size:14px;color:var(--text-1);line-height:1.45;white-space:pre-wrap;word-break:break-word}
.adminFbActions{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:2px}
.adminFbSet{appearance:none;font:inherit;font-size:12px;font-weight:600;color:var(--text-2);background:var(--bg-surface-2);border:1px solid var(--line);border-radius:8px;padding:6px 12px;min-height:36px;cursor:pointer}
.adminFbSet:hover:not(:disabled){border-color:var(--accent);color:var(--text-1)}
.adminFbSet.on{color:var(--accent-on);background:var(--accent);border-color:var(--accent);cursor:default}
.adminFbSet:disabled{opacity:.6}
.adminFbSet.on:disabled{opacity:1}
.adminFbBusyTag{font-size:11.5px;color:var(--text-3)}
/* announcements tab: active/inactive pills, tone picker + tone pills */
.adminStatusPill--active{background:var(--good, #1c7d4d);color:#fff}
.adminStatusPill--inactive{background:var(--bg-surface-2);color:var(--text-3)}
.adminAnnToneRow{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:10px}
.adminAnnToneLabel{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--text-4);margin-right:2px}
.adminAnnTonePill{display:inline-block;font-size:10px;font-weight:700;padding:1px 7px;border-radius:999px;text-transform:uppercase;letter-spacing:.03em;background:var(--accent-soft);color:var(--accent-text)}
.adminAnnTone--success{background:var(--brand-soft);color:var(--brand-ink)}
.adminAnnTone--warn{background:rgba(199,120,59,.16);color:#a05a1e}
.adminErr{color:var(--text-3);font-size:13px;padding:24px 0}
.adminLoading{color:var(--text-3);font-size:13px;padding:24px 0}
.adminCfNote{font-size:12px;color:var(--text-3);margin-top:8px}
.adminCfNote a{color:var(--accent-text)}
.sidebarAdminLink{display:block;width:100%;text-align:left;background:none;border:0;padding:4px 0 0;margin:0;font-size:11px;font-weight:600;color:var(--accent-text);cursor:pointer}
.sidebarAdminLink:hover{text-decoration:underline}
/* Topnav (desktop): the account cluster is a horizontal row of just the
   round avatar; the admin link must sit inline BEFORE it with real spacing,
   not as a full-width block (which overlapped the avatar). */
.topnav .sidebarAccount{gap:14px}
.topnav .sidebarAdminLink{order:-1;width:auto;padding:0;font-size:12.5px;white-space:nowrap;align-self:center;text-align:right}
/* Below 900px the admin entry moves into the hamburger sheet instead
   (.navSheetAdmin, filled by renderNavSheetAdmin() in ui-shared.js).
   Jeremy, 2026-07-26: "Still too tight ... There's just a lot up there."
   Measured at 440 with an admin account: the topbar ran logo(92) + ask(44)
   + theme(68) + account(130) + hamburger(44), and this link wanted 122px of
   text inside an 88px box, so it truncated to "Admin dash...". Nothing
   overflowed the viewport -- #sidebarAccount has flex-shrink:1, so the row
   squeezed its contents rather than clipping them, which is why this read as
   crowding rather than as the off-screen-button bug we have had three times.

   900px is the same breakpoint that shows .menuBtn and hides .navSheet, so
   exactly one of the two admin entries is reachable at any width. Do not
   "simplify" this to display:none at all widths: the nav sheet does not
   exist on desktop, so that would remove the only remaining path to /admin
   for a desktop admin.

   SPECIFICITY, and this bit me writing it. The obvious selector,
   `.topnav .sidebarAdminLink` (0,2,0), does NOT win: `.topnav
   .sidebarAccount button{display:block}` further down is (0,2,1), and one
   type selector is enough to outrank it. The rule was present, the media
   query matched, and the element still computed display:block. So this
   carries a third class to reach (0,3,0) and win on class count. Not
   !important -- that ends the conversation for whoever comes next.

   This is the same shape as the gap in the sitewide focus-zoom rule
   (`input,select,textarea{font-size:16px}` losing to `.obSearchBar input`):
   a class-only selector quietly outranked by a class+type one. Worth
   knowing that this stylesheet has enough descendant rules that
   "specificity looks fine" is not a safe assumption -- measure the computed
   value. */
@media(max-width:899px){ .topnav .sidebarAccount .sidebarAdminLink{display:none} }
.navSheet .navSheetAdmin{color:var(--accent-text)}
.navSheet .navSheetAdmin .navIc{color:var(--accent)}

/* === Mobile topbar, second pass (Jeremy, 2026-07-26) ======================
   1. "We need to remove the ask P&P ai from the navbar on mobile as being
      persistent, it should only be a hamburger menu item."
   2. "The light / dark theme switcher should be in the hamburger menu as
      well in mobile."
   3. "my profile JS in a circle is slightly blocked by the hamburger menu."

   (3) turned out not to be the hamburger at all. Measured at 440 in the
   pre-fix state: .sidebarAvatar's right edge sat at 383 while its container
   #sidebarAccount ended at 378, so `overflow:hidden` (set further down for
   the topbar) cut 5px off a 30px circle. The hamburger began at 384 -- one
   pixel clear -- which is exactly why it read as "blocked by" rather than
   "clipped". Hiding the admin link already gives the container room, but
   relying on that is relying on a side effect: the moment anything else
   lands in there it clips again, silently. .sidebarAvatar{flex:none} below
   makes the circle non-shrinking, so it cannot be the thing that gives.

   With (1) and (2) the mobile bar is logo + avatar + hamburger. Both hidden
   controls keep working at >=900px, and both have a real home in the sheet:
   .navSheetAsk was already there, .navSheetTheme is added by this pass.
   Nothing becomes unreachable at any width.

   SPECIFICITY, again -- the rules being overridden are
   `body.askWidgetMode .topnavAskBtn` (0,2,1) and `.topnav .themeToggleBtn`
   (0,2,0), so these carry an extra class each rather than assuming a later
   rule wins. This stylesheet has bitten this exact way twice today. */
@media(max-width:899px){
  body.askWidgetMode .topnav .topnavAskBtn{display:none}
  .topnav .topnavIn .themeToggleBtn{display:none}
  .topnav .sidebarAccount .sidebarAvatar{flex:none}
}
/* The sheet's theme row: a peer of the Home / Wallet / Programs items, with
   the same switch the topnav pill uses pushed to the trailing edge. Inherits
   padding, radius, min-height and hover from `.navSheet a,.navSheet
   .navSheetFeedback` -- this only adds what differs. */
.navSheet .navSheetTheme{display:flex;align-items:center;gap:12px;width:100%;
  padding:12px 10px;border-radius:12px;font:600 15px var(--font);color:var(--text-1);
  cursor:pointer;border:none;background:none;text-align:left;min-height:48px}
.navSheet .navSheetTheme:hover{background:var(--bg-surface-2)}
.navSheet .navSheetTheme .navIc{width:17px;height:17px;flex:none;color:var(--text-3)}
.navSheet .navSheetTheme .navSheetThemeLabel{flex:1 1 auto;min-width:0}
.navSheet .navSheetTheme .themeToggleTrack{flex:none}
/* --- Admin v2: tabbed console shell --------------------------------------
   Top tab strip (horizontally scrollable on narrow screens), the live
   feature-flag control rows, the Users table, and the "coming soon"
   scaffold cards. Everything keys off the existing semantic tokens so
   light/dark are both covered with no new variables. */
body.viewIsAdmin #dock{display:none} /* Jeremy: no Ask P&P dock on the admin console (router.js toggles the class) */
.adminHead{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin:0 0 4px}
.adminHead h2{margin:0}
.adminHeadNote{font-size:12px;color:var(--text-3)}
.adminTabs{display:flex;gap:6px;margin:14px 0 20px;padding-bottom:2px;border-bottom:1px solid var(--line);overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}
.adminTabs::-webkit-scrollbar{display:none}
.adminTab{flex:0 0 auto;appearance:none;background:none;border:0;border-bottom:2px solid transparent;padding:8px 12px 10px;font:inherit;font-size:13px;font-weight:600;color:var(--text-3);cursor:pointer;white-space:nowrap;border-radius:8px 8px 0 0;transition:color .15s,background .15s}
.adminTab:hover{color:var(--text-1);background:var(--accent-soft)}
.adminTab.on{color:var(--text-1);border-bottom-color:var(--accent)}
.adminTab .adminSoonDot{display:inline-block;font-size:9px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;color:var(--accent-text);background:var(--accent-soft);border-radius:999px;padding:1px 6px;margin-left:6px;vertical-align:1px}
.adminFlagRow{background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--radius);padding:14px 16px;box-shadow:var(--shadow);margin-bottom:10px}
.adminFlagTop{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.adminFlagKey{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;font-weight:700;color:var(--text-1)}
.adminFlagDesc{font-size:12.5px;color:var(--text-3);margin-top:4px}
.adminFlagState{margin-left:auto;display:flex;align-items:center;gap:8px}
.adminFlagStateLabel{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--text-4)}
.adminFlagRow.isOn .adminFlagStateLabel{color:var(--good-text)}
/* checkbox styled as an iOS-style switch -- input stays a real checkbox for
   a11y/keyboard, appearance:none repaints it. */
.adminSwitch{appearance:none;-webkit-appearance:none;width:40px;height:24px;border-radius:999px;background:var(--line-strong);border:0;position:relative;cursor:pointer;transition:background .15s;flex:0 0 auto}
.adminSwitch::after{content:"";position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:var(--bg-surface);box-shadow:0 1px 3px rgba(0,0,0,.3);transition:left .15s}
.adminSwitch:checked{background:var(--accent)}
.adminSwitch:checked::after{left:19px}
.adminSwitch:disabled{opacity:.5;cursor:wait}
.adminFlagRollout{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:10px;padding-top:10px;border-top:1px dashed var(--line)}
.adminFlagRollout label{font-size:11px;font-weight:700;color:var(--text-4);text-transform:uppercase;letter-spacing:.04em;flex:0 0 auto}
.adminRolloutInput{flex:1 1 220px;min-width:0;font:inherit;font-size:12.5px;color:var(--text-1);background:var(--bg-surface-2);border:1px solid var(--line);border-radius:8px;padding:7px 10px}
.adminRolloutInput:focus{outline:2px solid var(--accent);outline-offset:1px}
/* JOB3 (iOS focus-zoom, cont'd -- see EOF): (0,1,0) outranks the sitewide
   guard's bare `input` (0,0,1). Computed 12.5px, zoomed on focus. Matched
   at (0,1,0) here so it wins on source order. This is admin-only chrome,
   not mobile-first, but the guard is a universal contract -- any
   focusable field under 16px zooms, no exceptions carved out. */
@media(max-width:899px){ .adminRolloutInput{font-size:16px} }
.adminBtnSm{appearance:none;font:inherit;font-size:12px;font-weight:700;color:var(--accent-on);background:var(--accent);border:0;border-radius:8px;padding:7px 14px;cursor:pointer}
.adminBtnSm:hover{filter:brightness(1.08)}
.adminBtnSm:disabled{opacity:.5;cursor:wait}
.adminFlagMsg{font-size:11.5px;color:var(--text-3);margin-top:6px}
.adminFlagMsg.err{color:var(--danger-ink)}
/* Rollout user picker (chips + typeahead) + audit log — admin console.
   Everything themes through the existing CSS variables, so light/dark both
   come for free; transitions are killed under prefers-reduced-motion below. */
.adminRolloutHead{display:flex;align-items:center;gap:8px;width:100%}
.adminRolloutHead label{font-size:11px;font-weight:700;color:var(--text-4);text-transform:uppercase;letter-spacing:.04em}
.adminDirtyPill{display:inline-block;font-size:9.5px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:var(--accent-text);background:var(--accent-soft);border-radius:999px;padding:2px 8px}
.adminChips{display:flex;flex-wrap:wrap;gap:6px;width:100%;min-width:0}
.adminChip{display:inline-flex;align-items:center;gap:4px;font-size:12px;color:var(--text-1);background:var(--accent-soft);border:1px solid var(--line);border-radius:999px;padding:3px 4px 3px 10px;max-width:100%;min-width:0}
.adminChip .adminChipEmail{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.adminChipX{appearance:none;-webkit-appearance:none;border:0;background:none;color:var(--text-3);font-size:14px;line-height:1;width:18px;height:18px;border-radius:50%;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;padding:0;transition:background .15s,color .15s}
.adminChipX:hover{background:var(--line);color:var(--text-1)}
.adminChipX:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.adminChipX:disabled{opacity:.5;cursor:wait}
.adminChipsEmpty{font-size:12px;color:var(--text-4)}
.adminPicker{position:relative;flex:1 1 240px;min-width:0}
.adminPickerInput{width:100%}
.adminPickerMatches{position:absolute;left:0;right:0;top:calc(100% + 4px);z-index:30}
.adminPickerList{background:var(--bg-surface);border:1px solid var(--line);border-radius:10px;box-shadow:var(--shadow);max-height:220px;overflow-y:auto}
.adminPickerOpt{display:block;width:100%;text-align:left;appearance:none;-webkit-appearance:none;background:none;border:0;font:inherit;font-size:12.5px;color:var(--text-2);padding:8px 12px;cursor:pointer;transition:background .15s,color .15s}
.adminPickerOpt:hover,.adminPickerOpt.sel{background:var(--accent-soft);color:var(--text-1)}
.adminPickerOpt.isNew{font-weight:600;color:var(--accent-text)}
.adminAuditTable td.adminAuditTarget{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px}
.adminAuditTable td.adminAuditDetail{white-space:normal;word-break:break-word;max-width:340px}
.adminAuditTable td.adminAuditDetail code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10.5px;background:var(--bg-surface-2);border:1px solid var(--line);border-radius:6px;padding:1px 5px}
@media(prefers-reduced-motion:reduce){ .adminChipX,.adminPickerOpt,.adminTab,.adminSwitch,.adminSwitch::after,.adminBar{transition:none} }
.adminTableWrap{overflow-x:auto;border:1px solid var(--line);border-radius:var(--radius);background:var(--bg-surface);box-shadow:var(--shadow)}
.adminTable{width:100%;border-collapse:collapse;font-size:12.5px}
.adminTable th{text-align:left;font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-4);padding:10px 14px;border-bottom:1px solid var(--line)}
.adminTable td{padding:9px 14px;border-bottom:1px solid var(--line);color:var(--text-2);white-space:nowrap}
.adminTable tr:last-child td{border-bottom:0}
.adminSoonCard{background:var(--bg-surface);border:1px dashed var(--line-strong);border-radius:var(--radius);padding:16px 18px;margin-bottom:12px}
.adminSoonCard h4{margin:0;font-size:13.5px;color:var(--text-1);display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.adminSoonCard p{margin:6px 0 0;font-size:12.5px;color:var(--text-3)}
.adminSoonCard .adminPlumb{display:block;margin-top:6px;font-size:11.5px;color:var(--text-4)}
.adminSoonPill{display:inline-block;font-size:9.5px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:var(--accent-text);background:var(--accent-soft);border-radius:999px;padding:2px 8px}
@media(max-width:640px){
  .adminGrid{grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:8px}
  .adminStat{padding:11px 12px}
  .adminStat .adminStatNum{font-size:20px}
  .adminFlagState{margin-left:0;width:100%;justify-content:space-between}
}
/* === end ADMIN DASHBOARD === */

/* === STRATEGY V2 (Earn / Use / Activate sub-pages — approved comp v2) ===
   Class prefix st2*. Tokens are the sitewide WS-THEME vars; the comp was
   built from these same variables, so light/dark both come for free. */
.st2Wrap{padding-bottom:60px}
.st2Head{margin-top:10px}
.st2Kicker{display:inline-block;font-size:11.5px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:var(--accent-text);background:var(--accent-soft);padding:4px 10px;border-radius:999px;margin-bottom:12px}
.st2Head h1{font-family:var(--font-display);font-size:clamp(24px,3.4vw,33px);font-weight:600;letter-spacing:-.015em;margin:0}
.st2Sub{color:var(--text-3);margin:7px 0 0;font-size:14.5px;max-width:600px;line-height:1.55}
.st2Card{background:var(--bg-surface);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)}
.st2Pill{display:inline-flex;align-items:center;gap:4px;font-size:11px;font-weight:700;padding:2.5px 9px;border-radius:999px;background:var(--accent-soft);color:var(--accent-text);white-space:nowrap}
.st2Pill.good{background:var(--good-soft,rgba(15,118,110,.10));color:var(--good-text)}
.st2Pill.warn{background:var(--danger-bg);color:var(--danger-ink);border:1px solid var(--danger-border)}
.st2SecHead{display:flex;align-items:baseline;gap:10px;margin:30px 0 12px;flex-wrap:wrap}
.st2SecHead h3{font-size:17px;font-weight:800;margin:0}
.st2SecHead .note{font-size:12.5px;color:var(--text-4);font-weight:600}
.st2Fineprint{font-size:12px;color:var(--text-4);line-height:1.6;margin:26px 2px 0}
.st2Fineprint a,.st2EAlt a{color:var(--accent-text);font-weight:700;text-decoration:none;cursor:pointer}
.st2Fineprint a:hover,.st2EAlt a:hover{text-decoration:underline}
.st2LinkG{color:var(--accent-text);font-weight:750;cursor:pointer;text-decoration:none;font-size:12.5px;white-space:nowrap;text-transform:none;letter-spacing:0}
.st2LinkG:hover{text-decoration:underline}
.st2BtnBig{padding:13px 24px;font-size:15px;font-weight:700;cursor:pointer}

/* sub-tabs */
.st2Tabs{display:flex;gap:6px;background:var(--bg-surface-2);border:1px solid var(--line);border-radius:999px;padding:5px;width:max-content;max-width:100%;margin:22px 0 20px}
.st2Tab{border:0;background:none;border-radius:999px;padding:9px 20px;min-height:44px;font:700 13.5px var(--font,inherit);color:var(--text-3);cursor:pointer;white-space:nowrap;display:flex;align-items:center;gap:8px;transition:color .15s}
.st2Tab .n{width:19px;height:19px;border-radius:50%;background:var(--line);color:var(--text-3);font-size:10.5px;font-weight:800;display:grid;place-items:center}
.st2Tab.on{background:var(--accent);color:var(--accent-on);box-shadow:0 2px 10px rgba(199,154,59,.35)}
.st2Tab.on .n{background:rgba(20,32,43,.16);color:var(--accent-on)}
@media(max-width:640px){.st2Tabs{width:100%}.st2Tab{flex:1;justify-content:center;padding:9px 8px}}
.st2Pane{display:none}
.st2Pane.on{display:block;animation:st2fade .22s ease}
@keyframes st2fade{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
@media(prefers-reduced-motion:reduce){.st2Pane.on{animation:none}}

/* EARN — grounding band */
.st2Ground{padding:20px 22px;margin-top:2px}
.st2GLede{font-size:15.5px;line-height:1.5;color:var(--text-2);margin:0}
.st2GLede b{color:var(--text-1)}
.st2GCols{display:grid;grid-template-columns:1fr 1fr;gap:22px;margin-top:18px}
@media(max-width:700px){.st2GCols{grid-template-columns:1fr;gap:16px}}
.st2GLbl{font-size:11px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:var(--text-4);display:flex;align-items:baseline;gap:8px;margin-bottom:10px}
.st2GLbl .st2LinkG{margin-left:auto}
.st2WChips{display:flex;flex-direction:column;gap:8px}
.st2WChip{display:flex;align-items:center;gap:10px;border:1px solid var(--line);border-radius:11px;padding:8px 12px;background:var(--bg-surface-2)}
[data-theme="dark"] .st2WChip{background:var(--bg-deep)}
/* Card-cell consistency (2026-07-22): fixed 28px logo slot; the text block
   shrinks (min-width:0) and the name caps at two clean lines. */
.st2WChip .plogo{flex:0 0 28px;width:28px;justify-content:center}
.st2WChip>span{flex:1 1 auto;min-width:0}
.st2WChip b{font-size:13.5px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;overflow-wrap:break-word;word-break:normal}
.st2WChip .sub{display:block;font-size:11.5px;color:var(--text-4);font-weight:600}
.st2BChips{display:flex;flex-wrap:wrap;gap:7px}
.st2BChip{display:inline-flex;align-items:baseline;gap:6px;border:1px solid var(--line);border-radius:999px;padding:6px 13px;background:var(--bg-surface-2);font-size:12.5px;font-weight:650;color:var(--text-2)}
[data-theme="dark"] .st2BChip{background:var(--bg-deep)}
.st2BChip b{font-size:12.5px;font-variant-numeric:tabular-nums;color:var(--text-1)}
.st2BTotal{display:inline-flex;align-items:baseline;gap:6px;border:1px solid var(--accent);border-radius:999px;padding:6px 13px;background:var(--accent-soft);font-size:12.5px;font-weight:700;color:var(--accent-text)}
.st2GFoot{display:flex;gap:26px;align-items:center;margin-top:18px;padding-top:16px;border-top:1px solid var(--line);flex-wrap:wrap}
.st2GStat .v{font-size:20px;font-weight:800;font-variant-numeric:tabular-nums;letter-spacing:-.01em}
.st2GStat .v .u{font-size:12px;font-weight:650;color:var(--text-4);margin-left:3px}
.st2GStat .v.gain,.st2GStat .gain{color:var(--good-text)}
.st2GStat .k{font-size:11.5px;color:var(--text-4);font-weight:650;margin-top:1px}
.st2GFootNote{margin-left:auto;font-size:12.5px;color:var(--text-3);max-width:270px;line-height:1.45}
@media(max-width:700px){.st2GFootNote{margin-left:0;max-width:none}}

/* EARN — category rows */
.st2CatList{padding:4px 18px}
.st2CatRow{display:flex;align-items:center;gap:14px;padding:14px 0;border-bottom:1px solid var(--line)}
.st2CatRow:last-child{border-bottom:0}
.st2CatIco{width:34px;height:34px;border-radius:10px;background:var(--bg-surface-2);display:grid;place-items:center;flex:none;color:var(--text-3)}
.st2CatIco svg{width:17px;height:17px}
.st2Cat{width:170px;flex:none}
.st2Cat b{font-size:14px;display:block}
.st2Cat .mo{font-size:12px;color:var(--text-4);font-weight:650;font-variant-numeric:tabular-nums}
.st2CardChip{flex:1;min-width:0;display:flex;align-items:center;gap:9px}
/* Card-cell consistency (2026-07-22): same fixed 28px logo slot + shrinkable
   text block + two-line name cap as every other card row. */
.st2CardChip .plogo{flex:0 0 28px;width:28px;justify-content:center}
.st2CardChip>span{flex:1 1 auto;min-width:0}
.st2CardChip .nm{font-weight:750;font-size:13.5px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;overflow-wrap:break-word;word-break:normal}
.st2CardChip .eq{display:block;font-size:12px;color:var(--text-4);font-variant-numeric:tabular-nums}
.st2CatEarn{text-align:right;flex:none}
.st2CatEarn b{font-size:14.5px;font-variant-numeric:tabular-nums;display:block}
.st2CatEarn span{font-size:11.5px;color:var(--text-4);font-weight:650;font-variant-numeric:tabular-nums}
.st2CatRow.weak .st2CatIco{background:var(--danger-bg);color:var(--danger-ink)}
@media(max-width:700px){
  .st2CatRow{flex-wrap:wrap;gap:10px 12px}
  .st2Cat{width:auto;flex:1}
  .st2CardChip{flex-basis:100%;order:3;padding-left:46px}
  .st2CatEarn{order:2}
}

/* EARN — boosts + caution */
.st2Caution{display:flex;gap:13px;padding:15px 17px;background:var(--accent-soft);border:1px solid var(--accent);border-radius:12px;font-size:13.5px;line-height:1.55;color:var(--text-2);margin-bottom:14px}
.st2Caution svg{width:19px;height:19px;flex:none;color:var(--accent-text);margin-top:2px}
.st2Caution b{color:var(--text-1)}
.st2BoostGrid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
@media(max-width:860px){.st2BoostGrid{grid-template-columns:1fr}}
.st2Boost{padding:16px 17px;display:flex;flex-direction:column;gap:9px}
.st2Boost .tagRow{display:flex;align-items:center;gap:8px}
.st2BTag{font-size:10px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;border-radius:99px;padding:3px 10px}
.st2BTag.add{background:var(--good-soft,rgba(15,118,110,.10));color:var(--good-text)}
.st2BTag.maybe{background:var(--accent-soft);color:var(--accent-text)}
.st2BTag.skip{background:var(--bg-surface-2);color:var(--text-3)}
.st2Boost .fee{margin-left:auto;font-size:11.5px;color:var(--text-4);font-weight:700;font-variant-numeric:tabular-nums}
.st2Boost h4{font-size:14.5px;font-weight:800;margin:0}
.st2Boost p{margin:0;font-size:12.5px;color:var(--text-3);line-height:1.5;flex:1}
.st2Boost p b{color:var(--text-2)}
.st2BVal{font-size:15px;font-weight:800;font-variant-numeric:tabular-nums}
.st2BVal.gain{color:var(--good-text)}
.st2BVal.neg{color:var(--danger-ink)}
.st2BVal .per{font-size:11px;color:var(--text-4);font-weight:650;margin-left:4px}

/* EARN — empty states */
.st2Empty{padding:34px 30px;text-align:center;margin-top:2px}
.st2EIcon{width:56px;height:56px;border-radius:16px;background:var(--accent-soft);color:var(--accent-text);display:grid;place-items:center;margin:0 auto 16px}
.st2EIcon svg{width:26px;height:26px}
.st2Empty h3{font-size:20px;font-weight:800;letter-spacing:-.01em;margin:0}
.st2Empty .sub{color:var(--text-3);font-size:14px;max-width:470px;margin:8px auto 0;line-height:1.55}
.st2ESteps{display:flex;align-items:center;justify-content:center;gap:10px;margin:22px auto 24px;flex-wrap:wrap}
.st2EStep{display:inline-flex;align-items:center;gap:8px;border:1.5px solid var(--line-strong);border-radius:999px;padding:8px 16px;font-size:13px;font-weight:750;color:var(--text-3);background:var(--bg-surface)}
.st2EStep.have{border-color:var(--good);color:var(--good-text);background:var(--good-soft,rgba(15,118,110,.10))}
.st2EStep.result{border-color:var(--accent);color:var(--accent-text);background:var(--accent-soft)}
.st2EOp{font-size:15px;font-weight:800;color:var(--text-4)}
.st2EAlt{font-size:13px;color:var(--text-3);margin:18px auto 0;max-width:440px;line-height:1.55}
.st2ECards{display:flex;justify-content:center;gap:8px;flex-wrap:wrap;margin:16px 0 0}
.st2Preview{margin-top:14px;position:relative}
.st2Preview .st2CatList{opacity:.55}
.st2Preview .st2CatEarn b{color:var(--text-4)}
.st2PrevTag{position:absolute;top:-9px;left:16px;font-size:10px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;background:var(--bg-surface-2);border:1px solid var(--line-strong);color:var(--text-3);border-radius:99px;padding:3px 10px;z-index:2}
@media(max-width:700px){.st2PrevTag{position:static;display:inline-block;margin:0 0 8px}}

/* EARN — collapsed full matrix */
.st2MatrixBox{margin-top:26px;background:var(--bg-surface);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)}
.st2MatrixBox>summary{cursor:pointer;padding:15px 18px;font-weight:750;font-size:14px;color:var(--text-2);list-style:none;display:flex;align-items:center;gap:10px}
.st2MatrixBox>summary::-webkit-details-marker{display:none}
.st2MatrixBox>summary::before{content:'▸';color:var(--accent-text);transition:transform .15s}
.st2MatrixBox[open]>summary::before{transform:rotate(90deg)}
.st2MatrixBox>summary:hover{color:var(--text-1)}
.st2MatrixInner{padding:0 18px 18px}
.st2MatrixInner .stratMatrix{margin-top:8px}

/* USE — goal chips */
.st2GoalBar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:2px}
.st2GoalBar .lbl{font-size:11px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:var(--text-4);margin-right:2px}
.st2GoalChip{display:inline-flex;align-items:center;gap:8px;border:1.5px solid var(--line-strong);background:var(--bg-surface);border-radius:999px;padding:8px 16px;font:700 13px var(--font,inherit);color:var(--text-2);cursor:pointer;transition:border-color .15s,background .15s}
.st2GoalChip:hover{border-color:var(--accent)}
.st2GoalChip.on{border-color:var(--accent);background:var(--accent-soft);color:var(--accent-text)}
.st2GoalChip.edit{border-style:dashed;color:var(--text-4);font-weight:650}

/* USE — portfolio strip */
.st2Portfolio{display:flex;align-items:center;gap:16px;padding:15px 18px;margin-top:14px;flex-wrap:wrap}
.st2Portfolio .tot b{font-size:17px;display:block;font-variant-numeric:tabular-nums;letter-spacing:-.01em}
.st2Portfolio .tot span{font-size:12px;color:var(--text-4);font-weight:650}
.st2PfChips{display:flex;gap:7px;margin-left:auto;flex-wrap:wrap}
.st2PfChip{display:inline-flex;align-items:center;gap:7px;font-size:12.5px;font-weight:750;font-variant-numeric:tabular-nums;background:var(--bg-surface-2);border:1px solid var(--line);border-radius:999px;padding:4px 12px 4px 5px}
[data-theme="dark"] .st2PfChip{background:var(--bg-deep)}
.st2PfChip .mark,.st2PfChip img,.st2PfChip .ico{width:22px;height:22px;font-size:8.5px;border-radius:6px}
@media(max-width:700px){.st2PfChips{margin-left:0}}

/* USE — moves */
.st2MoveList{display:flex;flex-direction:column;gap:10px;margin-top:14px}
.st2Move{padding:15px 18px}
.st2Move.dim{opacity:.62}
.st2Move .top{display:flex;align-items:center;gap:13px;flex-wrap:wrap}
.st2Move .from{display:flex;align-items:center;gap:10px;min-width:0}
.st2Move .from b{font-size:14px;display:block}
.st2Move .from .amt{display:block;font-size:12px;color:var(--text-4);font-weight:650;font-variant-numeric:tabular-nums}
.st2Move .arr{color:var(--accent);flex:none}
.st2Move .to b{font-size:14px;display:block}
.st2Move .to span{display:block;font-size:12px;color:var(--text-4);font-weight:650}
.st2Move .val{margin-left:auto;text-align:right;flex:none}
.st2Move .val b{font-size:15.5px;color:var(--good-text);font-variant-numeric:tabular-nums;display:block}
.st2Move .val span{font-size:11.5px;color:var(--text-4);font-weight:650}
.st2Move.dim .val b{color:var(--text-3)}
.st2Move .why{margin:9px 0 0;font-size:13px;color:var(--text-2);line-height:1.55}
.st2Move .alt{margin:8px 0 0;font-size:12.5px;color:var(--text-3);line-height:1.5;border-top:1px dashed var(--line);padding-top:8px}
.st2Move .tagRow{display:flex;gap:7px;margin-top:9px;flex-wrap:wrap}
@media(max-width:640px){.st2Move .val{margin-left:46px;text-align:left;flex:0 0 calc(100% - 46px);order:4}}
.st2UseFoot{margin-top:16px}
.st2CashIco{width:32px;height:32px;border-radius:9px;background:var(--bg-surface-2);color:var(--text-2);font-weight:800;font-size:14px;display:flex;align-items:center;justify-content:center;flex:none}

/* ACTIVATE */
.st2ActHero{display:flex;align-items:center;gap:22px;padding:20px 22px;margin-top:2px;flex-wrap:wrap}
.st2Ring{position:relative;width:88px;height:88px;flex:none}
.st2Ring svg{transform:rotate(-90deg)}
.st2Ring .pct{position:absolute;inset:0;display:grid;place-items:center;font-size:15px;font-weight:800;font-variant-numeric:tabular-nums}
.st2ActHead{flex:1;min-width:220px}
.st2ActHead .big{font-size:23px;font-weight:800;font-variant-numeric:tabular-nums;letter-spacing:-.015em}
.st2ActHead .big span{font-size:14.5px;font-weight:650;color:var(--text-3);letter-spacing:0}
.st2ActHead .sub{font-size:13px;color:var(--text-3);margin-top:5px;line-height:1.5}
.st2ActCnt{text-align:right;flex:none}
.st2ActCnt b{font-size:17px;font-variant-numeric:tabular-nums;white-space:nowrap}
.st2ActCnt .k{display:block;font-size:11.5px;color:var(--text-4);font-weight:650}
@media(max-width:640px){
  .st2ActCnt{text-align:left}
  .st2ActHero{gap:16px}
  .st2Ring{width:72px;height:72px}
  .st2ActHead{min-width:190px}
}
.st2CkGroup{margin-top:24px}
.st2CkGroup h4{font-size:14.5px;font-weight:800;margin:0 0 10px}
.st2CkGroup h4 span{font-weight:600;color:var(--text-4);font-size:12.5px;margin-left:8px}
.st2CkList{padding:2px 16px}
.st2Ck{display:flex;align-items:center;gap:13px;padding:12px 0;border-bottom:1px solid var(--line);cursor:pointer}
.st2Ck:last-child{border-bottom:0}
.st2Ck .ckBox{width:23px;height:23px;border-radius:7px;border:1.5px solid var(--line-strong);display:grid;place-items:center;flex:none;color:transparent;font-size:12px;font-weight:900;transition:background .15s,border-color .15s,color .15s;background:var(--bg-surface)}
.st2Ck:hover .ckBox{border-color:var(--accent)}
.st2Ck.done .ckBox{background:var(--good);border-color:var(--good);color:#fff}
.st2Ck .txt{flex:1;min-width:0;line-height:1.4}
.st2Ck .txt b{font-size:13.5px}
.st2Ck .txt span{font-size:12.5px;color:var(--text-4)}
.st2Ck.done .txt{text-decoration:line-through;text-decoration-color:var(--text-4);opacity:.62}
.st2Ck .ckVal{text-align:right;flex:none;font-weight:800;font-size:13.5px;font-variant-numeric:tabular-nums}
.st2Ck .ckVal .per{font-size:11px;color:var(--text-4);font-weight:650}
@media(max-width:560px){.st2Ck{flex-wrap:wrap}.st2Ck .txt{min-width:56%}}
/* === end STRATEGY V2 === */

/* ==== ONBOARD-V4: goals-first 5-step guided setup (assets/onboarding.js) ====
   Faithful port of the approved onboarding comp onto the app's own tokens.
   Layout: while body.ppOnboarding is on (first-run intake on /points), the
   points view becomes a full-height flex column so the flow's action dock
   can pin to the bottom of #viewsScroll (position:sticky) — the primary
   button never moves and never hides, on any step, at any content height. */
body.ppOnboarding #view-points.on{display:flex;flex-direction:column;flex:1 0 auto}
body.ppOnboarding #view-points>.wrap{display:flex;flex-direction:column;flex:1;width:100%}
body.ppOnboarding #view-points>.wrap>h2,
body.ppOnboarding #view-points>.wrap>p.muted,
body.ppOnboarding #view-points>.wrap>p.pageSub,
body.ppOnboarding #walletInterviewChipWrap{display:none}
body.ppOnboarding #wizWrap{flex:1;display:flex;flex-direction:column;padding:0 !important;border:0;background:none;box-shadow:none} /* !important: #wizWrap carries an inline style="padding:22px" (index.html); any leftover bottom padding holds the sticky dock 22px above the viewport edge on short steps = jitter */
/* FULL-SCREEN ONBOARDING (2026-07-26): setup takes over the whole viewport —
   hide the app chrome (desktop sidebar nav, mobile top nav, off-canvas scrim) so
   it reads as a focused, Kudos-style full-screen flow. Scoped strictly to
   body.ppOnboarding; the normal app layout is untouched. The dock is already
   hidden by the body.ppOnboarding .dock rule above; obTop/obDock become the only
   sticky chrome, top and bottom of the viewport. */
body.ppOnboarding .sidebar{display:none}
body.ppOnboarding .topnav{display:none}
body.ppOnboarding #menuScrim{display:none}
/* Roomier decision column now that it owns the full width. */
body.ppOnboarding .obMain{max-width:760px;padding-top:34px}
body.ppOnboarding .obTopInner{max-width:760px}
body.ppOnboarding .obDockInner{max-width:760px}
.obFlow{flex:1;display:flex;flex-direction:column;min-width:0}
.obMain{width:100%;max-width:680px;margin:0 auto;padding:26px 6px 34px;flex:1}
@media(max-width:640px){.obMain{padding:16px 2px 26px}}

/* -- top chrome: stepper (desktop) / progress bar (mobile), sticky -- */
.obTop{position:sticky;top:0;z-index:6;background:var(--glass);backdrop-filter:blur(10px) saturate(1.2);border-bottom:1px solid var(--line)}
/* wallet-budget-redesign: six steps now (Budget added) — the old 680px
   cap + 26px connector lines wrapped "Done" onto a second row on desktop.
   Wider cap + slightly tighter connectors keep all six on one line. */
.obTopInner{max-width:800px;margin:0 auto;display:flex;align-items:center;gap:14px;padding:11px 6px}
.obStepper{flex:1;display:flex;align-items:center;justify-content:center;flex-wrap:wrap;row-gap:6px}
.obSp{display:flex;align-items:center;gap:7px}
.obSpDot{width:26px;height:26px;border-radius:50%;border:1.5px solid var(--line-strong);color:var(--text-4);font-size:12px;font-weight:800;display:grid;place-items:center;background:var(--bg-surface);transition:background .2s,border-color .2s,color .2s;flex:none}
.obSp.on .obSpDot{background:var(--accent);border-color:var(--accent);color:var(--accent-on);box-shadow:0 2px 10px rgba(199,154,59,.35)}
.obSp.done .obSpDot{background:var(--good);border-color:var(--good);color:#fff}
.obSpLbl{font-size:12.5px;font-weight:700;color:var(--text-4);white-space:nowrap}
.obSp.on .obSpLbl{color:var(--text-1)}
.obSp.done .obSpLbl{color:var(--text-3)}
.obSpLine{width:18px;height:2px;background:var(--line-strong);margin:0 8px;border-radius:99px;flex:none;transition:background .2s}
.obSpLine.done{background:var(--good)}
.obMProg{display:none;flex:1;min-width:0;padding:2px 0}
.obMProg .obMLbl{font-size:12px;font-weight:750;color:var(--text-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.obMProg .obMLbl b{color:var(--text-1)}
.obMBar{height:4px;background:var(--line);border-radius:99px;margin-top:6px;overflow:hidden}
.obMBar i{display:block;height:100%;background:var(--accent);border-radius:99px;transition:width .3s ease}
@media(max-width:820px){.obStepper{display:none}.obMProg{display:block}}

/* -- step shell -- */
.obStep{animation:obFade .28s ease}
@keyframes obFade{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
@media(prefers-reduced-motion:reduce){.obStep{animation:none}}
.obKicker{display:inline-block;font-size:11.5px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:var(--accent-text);background:var(--accent-soft);padding:4px 10px;border-radius:999px;margin-bottom:12px}
.obKicker.sm{margin-bottom:0;flex:none}
.obHead h1{font-family:var(--font-display);font-size:clamp(25px,3.6vw,33px);font-weight:600;letter-spacing:-.015em;line-height:1.14;margin:0}
.obSub{color:var(--text-3);font-size:15px;margin:8px 0 0;max-width:580px;line-height:1.55}
.obSub.center{margin-left:auto;margin-right:auto;max-width:520px}
.obCard{background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:22px 24px;margin-top:18px}
@media(max-width:640px){.obCard{padding:16px}}
/* JOB2: primary action button living INSIDE an .obCard (not the dock). Sized
   like .btn.primary but self-contained since it isn't always paired with
   .btn. min-height:44px is an explicit floor, not just padding math, so it
   stays tappable even if the label wraps. */
.obCardCta{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;padding:12px 24px;border:0;border-radius:999px;font:700 14.5px var(--font);cursor:pointer;background:var(--accent);color:var(--accent-on);box-shadow:var(--shadow);margin-top:16px;transition:transform .05s,box-shadow .15s}
.obCardCta:active{transform:translateY(1px)}
.obCardCta:hover{background:#c79646}
@media(max-width:640px){.obCardCta{display:flex;width:100%;padding:13px 20px;margin-top:14px}}
.obQTitle{font-size:15.5px;font-weight:800;margin:0 0 2px}
.obQNote{display:block;font-size:12.5px;color:var(--text-4);font-weight:600;margin-bottom:14px}

/* -- step 1: goal + knowledge pickers -- */
.obGoalGrid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
@media(max-width:560px){.obGoalGrid{grid-template-columns:1fr}}
.obGoal{position:relative;border:1.5px solid var(--line);border-radius:12px;padding:14px 15px;cursor:pointer;background:var(--bg-surface);text-align:left;display:flex;gap:12px;align-items:flex-start;transition:border-color .15s,background .15s;font-family:var(--font);color:var(--text-1)}
.obGoal:hover{border-color:var(--line-strong)}
.obIc{width:36px;height:36px;border-radius:10px;background:var(--bg-surface-2);display:grid;place-items:center;flex:none;color:var(--text-2);transition:background .15s,color .15s}
.obIc svg{width:18px;height:18px}
.obGoal b{font-size:14px;display:block;padding-right:64px}
.obGoal .d,.obKnow .d{font-size:12.5px;color:var(--text-3);line-height:1.4;display:block;margin-top:2px}
.obGoal.sel,.obKnow.sel{border-color:var(--accent);background:var(--accent-soft)}
.obGoal.sel .obIc,.obKnow.sel .obIc{background:var(--accent);color:var(--accent-on)}
.obSelPill{position:absolute;top:10px;right:10px;display:none;font-size:11px;font-weight:700;padding:2.5px 9px;border-radius:999px;white-space:nowrap}
.obGoal.sel .obSelPill{display:inline-flex;background:var(--accent);color:var(--accent-on)}
.obKnowRow{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
@media(max-width:640px){.obKnowRow{grid-template-columns:1fr}}
.obKnow{border:1.5px solid var(--line);border-radius:12px;padding:15px;cursor:pointer;background:var(--bg-surface);text-align:left;display:flex;flex-direction:column;gap:9px;transition:border-color .15s,background .15s;font-family:var(--font);color:var(--text-1)}
.obKnow:hover{border-color:var(--line-strong)}
.obKnow .obIc{width:34px;height:34px}
.obKnow b{font-size:14px}
@media(max-width:640px){.obKnow{flex-direction:row;align-items:flex-start}.obKnow .obIc{flex:none}}

/* -- steps 2-4: search + pick lists -- */
.obSearchBar{display:flex;align-items:center;gap:10px;border:1.5px solid var(--line-strong);border-radius:12px;padding:12px 14px;background:var(--bg-surface);transition:border-color .15s,box-shadow .15s}
.obSearchBar:focus-within{border-color:var(--accent);box-shadow:0 0 0 4px var(--accent-soft)}
.obSearchBar svg{width:17px;height:17px;color:var(--text-4);flex:none}
.obSearchBar input{flex:1;border:0;background:none;outline:none;font:600 15px var(--font);color:var(--text-1);min-width:0}
.obSearchBar input::placeholder{color:var(--text-4);font-weight:500}
/* JOB3 (iOS focus-zoom, cont'd -- see EOF): this is the exact selector the
   EOF comment names as the discovery case -- (0,1,1) outranks the sitewide
   guard's bare `input` (0,0,1). Computed 15px via the `font` shorthand,
   zoomed on focus. A later longhand font-size at equal specificity wins
   the font-size sub-property without disturbing the shorthand's weight
   or family. */
@media(max-width:899px){ .obSearchBar input{font-size:16px} }
.obAddedWrap{margin:18px 0 2px}
.obAddedLbl{font-size:11.5px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--text-4);margin-bottom:9px;display:flex;gap:8px;align-items:center}
.obAddedLbl .ct{background:var(--good-soft);color:var(--good-text);border-radius:99px;padding:1px 8px;font-size:11px;letter-spacing:0}
.obChipRow{display:flex;flex-wrap:wrap;gap:8px}
/* max-width + a min-width:0 .cardName wrapper (added in obCardAddedHTML) let a
   long card name ellipsize inside the pill instead of wrapping it tall. */
.obChipItem{display:inline-flex;align-items:center;gap:8px;border:1.5px solid var(--accent);background:var(--accent-soft);border-radius:999px;padding:5px 6px 5px 5px;font-size:13px;font-weight:700;animation:obChipIn .18s ease;max-width:100%;min-width:0}
.obChipItem .cardName{flex:0 1 auto}
@keyframes obChipIn{from{opacity:0;transform:scale(.94)}to{opacity:1;transform:none}}
@media(prefers-reduced-motion:reduce){.obChipItem,.obProgRow,.obRowSaved{animation:none}}
.obChipItem .plogo{width:22px;height:22px}
/* Bank marks are 28px (.plogo.lg) elsewhere; inside the compact pill they must
   render at the pill's own 22px or they overflow the rounded border. */
.obChipItem .plogo img,.obChipItem .plogo .mono{width:22px;height:22px}
.obChipItem .x{border:0;background:none;cursor:pointer;color:var(--text-3);font-size:15px;font-weight:800;padding:0 6px 0 0;line-height:1}
.obChipItem .x:hover{color:var(--danger-ink)}
.obEmptyAdded{font-size:13px;color:var(--text-4);border:1.5px dashed var(--line-strong);border-radius:11px;padding:12px 14px}
.obPickHead{font-size:11.5px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--text-4);margin:20px 0 2px}
.obPickRow{display:flex;align-items:center;gap:12px;padding:11px 2px;border-bottom:1px solid var(--line);cursor:pointer;width:100%;background:none;border-left:0;border-right:0;border-top:0;text-align:left;font-family:var(--font);color:var(--text-1)}
.obPickList .obPickRow:last-child{border-bottom:0}
/* Card-cell consistency (2026-07-22): fixed 28px logo slot so 22px program
   marks and 28px bank marks share one name-column x, and names cap at two
   clean lines instead of wrapping messily on narrow screens. */
.obPickRow .plogo,.obProgRow .plogo{flex:0 0 28px;width:28px;justify-content:center}
.obPickRow .nm{flex:1;min-width:0}
.obPickRow .nm b{font-size:14.5px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;overflow-wrap:break-word;word-break:normal;hyphens:none}
.obPickRow .nm span{font-size:12.5px;color:var(--text-4)}
.obAddTag{flex:none;display:inline-flex;align-items:center;gap:5px;font-size:12.5px;font-weight:750;color:var(--accent-text);border:1.5px solid var(--line-strong);border-radius:999px;padding:5px 13px;transition:border-color .15s,background .15s}
.obPickRow:hover .obAddTag{border-color:var(--accent);background:var(--accent-soft)}
.obNoMatch{font-size:13.5px;color:var(--text-4);padding:14px 2px}
.obHintLine{font-size:12.5px;color:var(--text-4);margin:10px 2px 0;line-height:1.5}
/* "Show all" expander + "tell us" missing-item link in onboarding pickers */
.obShowAllBtn{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;min-height:44px;margin-top:4px;
  background:none;border:0;border-top:1px solid var(--line);color:var(--accent-text);font:700 13px var(--font);cursor:pointer}
.obShowAllBtn:hover{background:var(--accent-soft)}
.obLinkBtn{display:inline;background:none;border:0;padding:0;color:var(--accent-text);font:inherit;font-weight:700;text-decoration:underline;cursor:pointer}
.obLinkBtn:hover{text-decoration:none}
.obProgList{margin-top:2px}
/* JOB2 (obProgRow shape lock): was display:flex, so a committed row
   (logo+name+input+remove), a pending row and a suggestion row rendered as
   three different shapes stacked in one list -- reads as broken. Fixed grid
   instead: every row gets the SAME 4 columns no matter which control
   currently occupies the last one (remove icon / .obSaveBtn / a "saved"
   check via .obRowSaved) -- nothing can change width when a row's state
   changes. */
.obProgRow{display:grid;grid-template-columns:28px 1fr 110px 44px;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid var(--line);animation:obChipIn .18s ease}
.obProgList .obProgRow:last-child{border-bottom:0}
.obProgRow .nm{min-width:0}
.obProgRow .nm b{font-size:14.5px;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} /* one-line ellipsis per the grid spec -- the old 2-line clamp gave short and long names two different row heights */
.obProgRow .nm span{font-size:12px;color:var(--text-4);display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.obProgRow .obBalIn{width:100%} /* fills the fixed 110px column exactly -- the grid owns the width now, not the input's own 132px */
.obRowAction{width:44px;height:44px;display:flex;align-items:center;justify-content:center}
.obSaveBtn{width:32px;height:32px;border:1.5px solid var(--accent);border-radius:8px;background:var(--accent-soft);color:var(--accent-text);display:grid;place-items:center;cursor:pointer;flex:none}
.obSaveBtn svg{width:16px;height:16px}
.obSaveBtn:hover{background:var(--accent);color:var(--accent-on)}
.obRowSaved{animation:obRowSaveFlash 1.1s ease}
@keyframes obRowSaveFlash{0%{background:var(--good-soft)}100%{background:transparent}}
.obBalIn{width:132px;text-align:right;font:700 14.5px var(--font);font-variant-numeric:tabular-nums;border:1.5px solid var(--line-strong);border-radius:10px;padding:9px 12px;background:var(--bg-surface);color:var(--text-1);outline:none;transition:border-color .15s,box-shadow .15s}
.obBalIn:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.obBalIn::placeholder{color:var(--text-4);font-weight:600;font-size:13px}
.obRmBtn{border:0;background:none;color:var(--text-4);cursor:pointer;font-size:16px;font-weight:800;padding:4px 6px;line-height:1;border-radius:8px}
.obRmBtn:hover{color:var(--danger-ink)}
@media(max-width:480px){
  .obBalIn{width:106px;padding:9px 10px}
  .obProgRow .nm span{display:none}
}

/* -- the fixed action dock: sticky to the scroller's bottom edge -- */
.obDock{position:sticky;bottom:0;z-index:7;margin-top:auto;background:var(--glass);backdrop-filter:blur(10px) saturate(1.2);border-top:1px solid var(--line)}
/* JOB2 (revised 2026-07-29, owner iPhone check): iOS shrinks only the visual
   viewport when the keyboard is up, so a sticky-bottom dock floats over the
   middle of the list. We USED to hide the dock entirely, but that stranded
   the user with no visible "Save & Continue" (the numeric keypad has no
   Return key). Now we PIN the dock just above the keyboard: fixed position
   with bottom = the live keyboard gap (--obKbGap, set from visualViewport in
   obKbBind). Autosave-on-keystroke still stands, so the button is a
   convenience, not the only commit path. */
/* W-02(a): the switch from sticky to fixed also moved the dock SIDEWAYS.
   Sticky, the dock lives inside .obFlow and therefore inside #view-points>.wrap,
   which carries the sitewide .wrap{padding:0 20px} gutter. Fixed with
   left:0;right:0 it spans the whole viewport instead, so on any screen narrower
   than the 760px .obDockInner cap the inner column widens by 40px and the
   right-aligned primary jumps 20px outward. Measured at 390x844: #obNext
   x=198.5 -> 218.5 on focus. Restoring the same 20px gutter on the pinned bar
   puts the inner column back on the exact axis it had while sticky. The bar's
   own background/border still spans edge to edge. On screens wide enough for
   .obDockInner to hit its 760px cap the inner is centred either way, so the
   extra padding changes nothing there. */
body.obKeyboardOpen .obDock{position:fixed;left:0;right:0;bottom:var(--obKbGap,0px);z-index:60;margin-top:0;padding-left:20px;padding-right:20px}
/* Item 4 (round 17): at 390 the dock reserves a 2-row 122px height (wrap-jitter
   guard). Pinned above the keyboard that tall dock overlapped the focused input
   by ~29px. While the keyboard is open the dock only ever carries Back / Skip /
   Save & Continue, which fit one row — so drop the reserved height and keep it a
   single compact row so the input clears the dock top. */
body.obKeyboardOpen .obDockInner{min-height:0;flex-wrap:nowrap;row-gap:0}
body.obKeyboardOpen .obDock .btn.obBig{padding:11px 16px}
body.obKeyboardOpen .obDock .obSkip{padding:10px 10px}
.obDockInner{max-width:680px;margin:0 auto;display:flex;align-items:center;gap:14px;padding:13px 6px;padding-bottom:calc(13px + env(safe-area-inset-bottom,0px))}
@media(max-width:640px){
  /* JOB1 / WS-DOCK-OVERFLOW: .obDock .btn{flex:none} below (unchanged, see its own
     comment) makes every dock BUTTON fixed-size and non-shrinking -- fine on its
     own, but combined with a row that could never wrap, a 4th child rendered 112px
     past the right edge at 440px, unreachable because body{overflow-x:clip} turns
     clipped content invisible rather than merely off-screen. This is the 3rd time
     this exact shape has broken. A parallel brief is capping the dock at 3
     children, but that's a policy, not a structural guarantee, so this makes the
     row itself incapable of overflowing regardless of child count:
       - Back (.obBackBtn) stays flex:none (from the base .obDock .btn rule) --
         fixed, never moves, never shrinks.
       - .obSkip is allowed to shrink and ellipsize (below) rather than push the
         row wider.
       - The primary (.btn.obBig) keeps white-space:nowrap (base rule, unchanged)
         so its label is never split mid-word; if it doesn't fit on the first
         line it drops WHOLE to a second line via flex-wrap, and .obDockRight's
         own margin-left:auto still right-aligns it on whichever line it lands.
     min-height on .obDockInner reserves 2-row height UNCONDITIONALLY (present
     whether 1 or 2 rows actually render) so the 1-row<->2-row transition itself
     cannot jitter -- this is the same failure mode the original nowrap-only rule
     was guarding against (a wrapped label changing dock height), solved here
     without reintroducing the overflow. 122px = 13px+13px padding (border-box,
     see *{box-sizing:border-box} line 132) + two ~44px button rows + 8px row-gap. */
  .obDockInner{gap:10px;flex-wrap:wrap;row-gap:8px;min-height:122px;align-content:center}
  .obSkip{flex-shrink:1;min-width:0;overflow:hidden;text-overflow:ellipsis}
}
.obDock .btn{white-space:nowrap;flex:none} /* a wrapped "Continue →" changes the dock's height = jitter */
.obDock .btn.obBig{padding:13px 26px;font-size:15px;font-weight:700;border-radius:999px}
.obDock .btn.primary[disabled]{opacity:.45;cursor:default;box-shadow:none}
[data-theme="dark"] .obDock .btn.primary:not([disabled]){background:var(--accent);color:var(--accent-on)}
.obBackBtn{border-radius:999px}
.obBackBtn.hid{visibility:hidden}
.obDockRight{margin-left:auto;display:flex;align-items:center;gap:14px;min-width:0}
.obDockHint{font-size:12.5px;color:var(--text-4);text-align:right;line-height:1.35}
.obSkip{border:0;background:none;color:var(--text-3);font:650 13.5px var(--font);cursor:pointer;padding:8px 12px;white-space:nowrap;min-height:44px;display:inline-flex;align-items:center;justify-content:center}
.obSkip:hover{color:var(--text-1);text-decoration:underline}
@media(max-width:480px){
  .obDockHint{display:none}
  .obDock .btn.obBig{padding:12px 16px;font-size:14.5px}
  .obDock .obBackBtn{padding:10px 13px}
  .obSkip{font-size:13px}
}

/* -- step 5: done / handoff -- */
.obDone{text-align:center;padding-top:10px}
.obBigCheck{width:76px;height:76px;border-radius:50%;background:var(--good-soft);display:grid;place-items:center;margin:0 auto 18px;color:var(--good-text)}
.obBigCheck svg{width:36px;height:36px}
.obBigCheck .obCkPath{stroke-dasharray:30;stroke-dashoffset:30;animation:obCkDraw .5s ease .15s forwards}
@keyframes obCkDraw{to{stroke-dashoffset:0}}
@media(prefers-reduced-motion:reduce){.obBigCheck .obCkPath{animation:none;stroke-dashoffset:0}}
.obDone h1{font-size:clamp(26px,3.8vw,34px);font-weight:800;letter-spacing:-.015em;margin:0}
.obSumCard{padding:4px 22px;text-align:left;margin-top:24px}
@media(max-width:640px){.obSumCard{padding:2px 16px}}
.obSumRow{display:flex;align-items:center;gap:13px;padding:14px 0;border-bottom:1px solid var(--line)}
.obSumCard .obSumRow:last-child{border-bottom:0}
.obSumIco{width:36px;height:36px;border-radius:10px;background:var(--bg-surface-2);display:grid;place-items:center;flex:none;color:var(--text-2)}
.obSumIco svg{width:17px;height:17px}
.obSumRow .grow{flex:1;min-width:0}
.obSumRow .grow b{font-size:14.5px;display:block}
.obSumRow .grow span{font-size:12.5px;color:var(--text-3);line-height:1.45}
.obSumRow .val{text-align:right;font-weight:800;font-size:14.5px;font-variant-numeric:tabular-nums;white-space:nowrap}
.obSumRow .val span{display:block;font-size:11px;color:var(--text-4);font-weight:650}
/* inline-edit affordance on the end-of-setup summary rows (#16) */
.obSumEdit{flex:none;margin-left:4px;min-height:36px;padding:6px 12px;border-radius:999px;border:1px solid var(--line-strong);
  background:none;color:var(--accent-text);font:700 12.5px var(--font);cursor:pointer}
.obSumEdit:hover{background:var(--accent-soft);border-color:var(--accent)}
.obCtaCard{margin-top:18px;display:flex;gap:16px;align-items:center;border:1.5px solid var(--accent);text-align:left;flex-wrap:wrap;padding:20px 22px}
.obCtaCard .ic{width:42px;height:42px;border-radius:12px;background:var(--accent);color:var(--accent-on);display:grid;place-items:center;flex:none}
.obCtaCard .ic svg{width:20px;height:20px}
.obCtaCard .grow{flex:1;min-width:200px}
.obCtaCard .grow>b{font-size:15.5px;display:block}
.obCtaCard .grow p{margin:3px 0 0;font-size:13.5px;color:var(--text-2);line-height:1.5}
[data-theme="dark"] .obCtaCard .btn.primary{background:var(--accent);color:var(--accent-on)}
.obNextCard{margin-top:10px;display:flex;gap:14px;align-items:center;text-align:left;flex-wrap:nowrap;padding:16px 22px;width:100%;cursor:pointer;font-family:var(--font);transition:border-color .15s,background .15s}
.obNextCard:hover{border-color:var(--accent);background:var(--accent-soft)}
.obNextArrow{flex:none;margin-left:auto;font-size:20px;font-weight:700;color:var(--accent-text)}
.obNextCard .grow{flex:1;min-width:200px}
.obNextCard .grow>b{font-size:14.5px;display:block}
.obNextCard .grow p{margin:2px 0 0;font-size:13px;color:var(--text-3);line-height:1.5}
.obDoneNote{margin-top:16px;font-size:12.5px;color:var(--text-4);text-align:center}
.obDoneNote b{color:var(--text-3)}
/* === end ONBOARD-V4 === */

/* ===== ROUND-11 (2026-07-28): Coral Compass palette, scoped to onboarding
   ONLY via the #obFlow ancestor selector — redeclaring the token names here
   (not at :root) means every existing onboarding component built on the
   --bg, --text, --accent, --good, --line and --font tokens above picks up the
   new values automatically, without touching the other 6 pages that share those
   same token names. Dark mode mirrors via [data-theme="dark"] #obFlow, since
   theming is the [data-theme] attribute on <html>, not a class/media query. */
#obFlow{
  --bg-canvas:#F4F1E8; --bg-surface:#FFFEFA; --bg-surface-2:#E4EFEB; --bg-deep:#EFEAD9;
  --text-1:#173330; --text-2:#526A66; --text-3:#6B8480; --text-4:#86A19C;
  --accent:#E75C3C; --accent-on:#0C1F1E; --accent-text:#2B7A78; --accent-soft:rgba(231,92,60,.14);
  --good:#2B7A78; --good-text:#2B7A78; --good-soft:#D9EEEA; --good-line:rgba(43,122,120,.35);
  --line:#C9D6CF; --line-strong:#97B3AB;
  --font:"Avenir Next",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --font-display:Charter,"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
  --shadow:0 1px 2px rgba(23,51,48,.08),0 10px 30px rgba(23,51,48,.10);
  background:var(--bg-canvas);color:var(--text-1);font-family:var(--font);
}
[data-theme="dark"] #obFlow{
  --bg-canvas:#0C1F1E; --bg-surface:#132B29; --bg-surface-2:#1B3B37; --bg-deep:#0C1F1E;
  --text-1:#F7F3E8; --text-2:#B6C7C2; --text-3:#93A7A2; --text-4:#7A8F8A;
  --accent:#FF7A5C; --accent-on:#0C1F1E; --accent-text:#6FD1C8; --accent-soft:rgba(255,122,92,.16);
  --good:#6FD1C8; --good-text:#6FD1C8; --good-soft:#1D4642; --good-line:rgba(111,209,200,.35);
  --line:#315451; --line-strong:#4C706C;
  --shadow:0 10px 34px rgba(0,0,0,.45);
}
body.ppOnboarding{background:var(--bg-canvas)}
/* Coral Compass primary action: the global .btn.primary fills with --brand (navy)
   and hardcodes white text; inside onboarding it must be coral with EVERGREEN text
   (white-on-coral is 3.47:1, fails AA — evergreen-on-coral is 4.87:1). Scoped to
   #obFlow so the other pages' navy primary buttons are untouched. */
#obFlow .btn.primary{background:var(--accent);color:var(--accent-on)}
#obFlow .btn.primary:hover:not([disabled]){background:#C9472D;color:var(--accent-on)}

/* -- Round-11: 4-stage route bar (replaces the numbered stepper for the new
   12-screen-family flow; .obTop/.obTopInner sticky positioning is reused
   as-is from the ONBOARD-V4 block above) -- */
.obRouteWrap{flex-direction:column;align-items:stretch;padding:11px 6px}
.obRoute{display:flex;gap:7px}
.obRoute span{height:5px;flex:1;border-radius:99px;background:var(--line);transition:background .2s}
.obRoute span.done{background:var(--good)}
.obRoute span.now{background:var(--accent)}
.obRouteLabels{display:flex;justify-content:space-between;color:var(--text-4);font-size:10.5px;font-weight:800;margin-top:6px}
.obRouteLabels span.on{color:var(--text-1)}

/* -- Round-11: one-item-per-screen balance/spend input, kept HIGH in the
   viewport (immediately under the heading, nothing above it) so the mobile
   keyboard never has to scroll it into view -- */
.obSeq{font-size:12px;color:var(--text-4);font-weight:800;margin:14px 0 0}
.obPointsBox{padding:18px;border:1px solid var(--line);border-radius:var(--radius);background:var(--bg-surface-2);margin-top:8px}
.obFieldLbl{display:block;font-size:13px;font-weight:800;color:var(--text-2);margin-bottom:8px}
.obBigInput{width:100%;min-height:52px;border:1.5px solid var(--line-strong);border-radius:12px;background:var(--bg-surface);padding:12px 14px;font:800 24px var(--font);color:var(--text-1);font-variant-numeric:tabular-nums;outline:none;transition:border-color .15s,box-shadow .15s}
/* W-02(d), owner walkthrough 2026-08-02: "the focused state has the field
   outlined in red, which reads as an error on a valid $2,300."

   Measured on the built site before changing anything: focusing #obSpendInput
   with the valid value 2,300 computed border-color rgb(231,92,60) and
   box-shadow rgba(231,92,60,.14) 0 0 0 3px, with el.validity.valid === true.
   So this was never :invalid -- there is no :invalid rule anywhere in this
   stylesheet. It was THIS rule resolving --accent, which #obFlow above
   redefines to the Coral Compass coral #E75C3C. Coral is the flow's PRIMARY
   ACTION colour (it fills .btn.primary), and a 3px coral halo around a filled
   field is indistinguishable from the red error treatment used elsewhere
   (--danger-ink #b3261e / #e08668 dark).

   Focus still needs a clearly visible indicator, so this does not remove the
   ring -- it recolours it to the palette's other half, --accent-text
   (evergreen #2B7A78 light / #6FD1C8 dark), with --good-line as its
   translucent form (the same hue at .35 alpha, both themes). Contrast of the
   ring against the field's own --bg-surface: 5.0:1 light, 8.1:1 dark, both
   well past the 3:1 WCAG 1.4.11 asks of a non-text UI indicator. */
.obBigInput:focus{border-color:var(--accent-text);box-shadow:0 0 0 3px var(--good-line)}
.obFieldNote{display:block;margin-top:8px;font-size:12.5px;color:var(--text-4)}
.obDollarField{display:flex;align-items:center;gap:8px;position:relative}
.obDollarField .obDollarSign{position:absolute;left:14px;font:800 24px var(--font);color:var(--text-3);pointer-events:none}
.obDollarField .obBigInput{padding-left:28px}

/* -- Round-11: IS / IS NOT category definitions -- */
.obDefinition{margin-top:12px;padding:12px 14px;border-radius:12px;background:var(--bg-surface);border:1px solid var(--line);font-size:13px;color:var(--text-2);line-height:1.5}
.obDefinition b{display:block;color:var(--good-text);font-size:10px;letter-spacing:.1em;font-weight:800;margin-bottom:3px}
.obDefinition.not b{color:var(--danger-ink,#a83232)}

/* -- Round-11: multi-select "selected" rows on the Select cards/airlines/
   hotels screens (full-width list row, not the compact chip pill) -- */
.obSelStack{display:grid;grid-template-columns:minmax(0,1fr);gap:9px;margin:14px 0}
.obSelRow2{display:flex;align-items:center;justify-content:space-between;gap:10px;min-width:0;border:1px solid var(--line);border-radius:14px;padding:11px 13px;background:var(--bg-surface)}
.obSelRowNm{display:flex;align-items:center;gap:10px;min-width:0;overflow:hidden}
.obSelRowNm b{font-size:14.5px;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
.obSelRowNm small{display:block;color:var(--text-4);font-size:12px}
.obSelRowActs{display:flex;align-items:center;gap:4px;flex:none}
.obSelBadge{font-size:11px;font-weight:800;color:var(--good-text);white-space:nowrap}
.obSelX{min-width:44px;min-height:44px;display:inline-flex;align-items:center;justify-content:center;border:0;background:none;color:var(--text-3);font-size:18px;font-weight:800;cursor:pointer;border-radius:8px}
.obSelX:hover{color:var(--danger-ink,#a83232);background:var(--danger-bg,rgba(168,50,50,.08))}

/* -- Round-11: Map cards screen -- */
.obMapRow{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:12px 2px;border-bottom:1px solid var(--line)}
.obMapRow:last-child{border-bottom:0}
.obMapNm b{display:block;font-size:14px}
.obMapNm small{color:var(--text-4);font-size:12.5px}

/* -- Round-11: Ready screen — accessible disclosures + per-item deep edit -- */
.obReviewStack{display:grid;gap:12px;margin-top:16px}
.obReviewGroup{border:1px solid var(--line);border-radius:15px;overflow:hidden;background:var(--bg-surface)}
.obReviewToggle{width:100%;min-height:56px;border:0;background:var(--bg-surface);padding:11px 13px;display:grid;grid-template-columns:1fr auto auto;gap:10px;align-items:center;text-align:left;font:800 14.5px var(--font);color:var(--text-1);cursor:pointer}
.obReviewToggle small{display:block;color:var(--text-4);font-weight:650;font-size:12px}
.obReviewBadge{font-size:11px;font-weight:800;color:var(--accent-text)}
.obReviewChev{color:var(--accent);font-size:19px;transition:transform .15s}
.obReviewToggle[aria-expanded="true"] .obReviewChev{transform:rotate(90deg)}
.obReviewItems{border-top:1px solid var(--line)}
.obReviewItems[hidden]{display:none}
.obReviewItem{min-height:56px;padding:10px 12px;border-bottom:1px solid var(--line);display:grid;grid-template-columns:1fr auto;gap:10px;align-items:center}
.obReviewItem:last-child{border-bottom:0}
.obReviewItem>span:first-child{display:flex;align-items:center;gap:9px;min-width:0}
.obReviewItem b{font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1 1 auto;min-width:0}
/* value never wraps (owner request 2026-07-29); the name truncates instead.
   Darker than the old --text-4 so the balance reads clearly. */
.obReviewItem small{color:var(--text-2);font-size:12.5px;white-space:nowrap;flex:none}
.obEditBtn{min-width:48px;min-height:44px;border:0;border-radius:10px;background:transparent;color:var(--accent-text);font:800 12.5px var(--font);cursor:pointer}
.obEditBtn:hover{background:var(--accent-soft)}
/* Ready total (owner request 2026-07-29): make the "worth $X" line vibrant,
   not a plain card. Coral→evergreen soft gradient, accent border, big number. */
.obReadyTotal{margin-top:16px;padding:15px 18px;border-radius:16px;background:linear-gradient(135deg,var(--accent-soft),var(--good-soft));border:1px solid var(--accent);display:grid;gap:3px}
/* R14-01: the small label + subtext must clear WCAG 4.5:1 across BOTH gradient
   endpoints in BOTH themes. --accent-text / --text-3 failed in light; --text-1
   clears both endpoints in both themes (Codex-measured known-safe). The card
   keeps its vibrancy from the gradient, border, and the large $ figure. */
.obReadyTotalLabel{font-size:10.5px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--text-1)}
.obReadyTotalVal{font-size:20px;font-weight:800;color:var(--text-1);font-variant-numeric:tabular-nums}
.obReadyTotalVal .obReadyTotalEq{color:var(--text-1);font-weight:600;margin:0 2px}
.obReadyTotalVal b{color:var(--accent-text)}
.obReadyTotalSub{font-size:12px;font-weight:600;color:var(--text-1)}
/* Item 5 (owner iPhone check): transient "Saved ✓" confirmation shown on
   Save & Continue from a value-entry screen, so the save registers before the
   next (identical-looking) screen appears. */
.obSavedToast{position:fixed;left:50%;top:calc(env(safe-area-inset-top,0px) + 62px);transform:translate(-50%,-8px);z-index:80;display:inline-flex;align-items:center;gap:7px;padding:9px 16px;border-radius:999px;background:var(--good-soft);color:var(--good-text);border:1px solid var(--good);font:800 13.5px var(--font);box-shadow:var(--shadow);opacity:0;pointer-events:none;transition:opacity .18s ease,transform .18s ease}
.obSavedToast.on{opacity:1;transform:translate(-50%,0)}
.obSavedTick{display:inline-grid;place-items:center;width:18px;height:18px;border-radius:50%;background:var(--good);color:var(--good-on,#fff);font-size:12px;font-weight:900}
@media(prefers-reduced-motion:reduce){.obSavedToast{transition:opacity .18s ease}}
/* Item 3 (owner iPhone check): inline manual option under the Plaid button. */
.obManualInline{display:block;width:100%;margin-top:10px;text-align:center}

/* -- Round-11: multi-select / single-select sheet (cards, airlines, hotels,
   map) — bottom sheet on mobile, centered modal on desktop -- */
.obPickerOverlay{position:fixed;inset:0;z-index:70;background:rgba(12,31,30,.55);display:flex;align-items:flex-end;justify-content:center}
[data-theme="dark"] .obPickerOverlay{background:rgba(0,0,0,.7)}
.obPickerSheet{width:100%;max-width:560px;max-height:88vh;display:flex;flex-direction:column;background:var(--bg-surface);border-radius:22px 22px 0 0;box-shadow:var(--shadow);color:var(--text-1)}
/* Owner iPhone check 2026-07-29: with the soft keyboard up, the bottom sheet used
   to stay pinned to the layout-viewport bottom, so the keyboard covered the
   filtered results and search was unusable. Shrink the overlay to the visible
   area (bottom = --obKbGap from obKbBind) so the flex-end sheet rides on top of
   the keyboard, and cap the sheet to that height so its results list scrolls. */
body.obKeyboardOpen .obPickerOverlay{bottom:var(--obKbGap,0px)}
body.obKeyboardOpen .obPickerSheet{max-height:100%}
@media(min-width:641px){
  .obPickerOverlay{align-items:center}
  .obPickerSheet{max-height:80vh;border-radius:22px}
}
.obPickerHead{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;padding:16px 18px;border-bottom:1px solid var(--line)}
.obPickerHead h3{font-family:var(--font-display);font-size:22px;margin:4px 0 0}
.obPickerClose{width:44px;height:44px;border:0;border-radius:50%;background:transparent;color:var(--text-3);font-size:20px;cursor:pointer;flex:none}
.obPickerClose:hover{background:var(--bg-surface-2)}
.obPickerBody{padding:14px 18px;overflow-y:auto;overflow-x:hidden;flex:1}
/* grid track must be constrained (minmax(0,1fr)) or a long card name makes the
   row exceed the viewport → horizontal scroll (owner iPhone check 2026-07-29). */
.obPickerResults{margin-top:12px;display:grid;grid-template-columns:minmax(0,1fr);gap:8px}
.obPickerRow2{width:100%;min-width:0;min-height:62px;border:1px solid var(--line);background:var(--bg-surface);border-radius:13px;padding:10px 12px;display:flex;align-items:center;gap:10px;text-align:left;cursor:pointer;font-family:var(--font);color:var(--text-1)}
.obPickerRow2>:first-child{flex:none}
.obPickerRow2[aria-pressed="true"]{background:var(--accent-soft);border-color:var(--accent)}
.obPickerRow2 .nm{flex:1;min-width:0}
.obPickerRow2 .nm b{font-size:14.5px;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.obPickerRow2 .nm span{font-size:12.5px;color:var(--text-4);display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.obPickerRow2 strong{flex:none;font-size:12.5px;font-weight:800;color:var(--accent-text)}
.obPickerFoot{padding:14px 18px;padding-bottom:calc(14px + env(safe-area-inset-bottom,0px));border-top:1px solid var(--line)}
/* === end ROUND-11 Coral Compass onboarding === */

/* === Monetization #3: pricing/upgrade surface (assets/pricing.js) =========
   Semantic-token driven, so it's light+dark safe by construction (no raw
   colors except the gold accent fills that already carry their own on-color
   token). Cards live in #view-pricing's #pricingWrap. */
.pricingHead{margin:10px 0 22px}
/* h1 AND h2 — /pricing was one of four routes with NO visible <h1> at all,
   measured 2026-08-01, which is both an SEO defect and a real accessibility one
   (screen-reader users navigate by heading level) on the page we are about to
   send paying customers to. renderPricingHTML now emits this as <h1>. The rule
   sets margin, font-size and color; font-weight and line-height come from the
   shared h1,h2,h3,h4 rule, so the promotion would change SIZE if this selector
   did not carry h1 explicitly. It does, so the change is visually neutral. */
.pricingHead h1,.pricingHead h2{margin:0 0 6px;font-size:26px;color:var(--text-1);font-weight:800}
.pricingHead .pageSub{max-width:640px}
.pricingYouAre{display:inline-block;margin-top:6px;padding:5px 12px;border-radius:999px;font-size:12.5px;font-weight:650;background:var(--bg-surface-2);color:var(--text-2);border:1px solid var(--line)}
.pricingYouAre--pro{background:var(--accent-soft);color:var(--accent-text);border-color:var(--accent)}
.pricingManageNote{margin:0 0 16px;font-size:13.5px;color:var(--text-2)}
.pricingError{margin:0 0 16px;padding:12px 16px;border-radius:12px;background:var(--danger-bg);border:1px solid var(--danger-border);color:var(--danger-ink);font-size:14px;font-weight:600}

.pricingGrid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;align-items:stretch}
.pricingCard{position:relative;display:flex;flex-direction:column;padding:22px;border-radius:16px;background:var(--bg-surface);border:1px solid var(--line);box-shadow:var(--shadow)}
.pricingCard--best{border-color:var(--accent);box-shadow:0 4px 20px var(--accent-soft),var(--shadow)}
.pricingCard--current{border-color:var(--line-strong)}
.pricingBestTag{position:absolute;top:-11px;left:22px;padding:4px 12px;border-radius:999px;font-size:11.5px;font-weight:750;letter-spacing:.02em;text-transform:uppercase;background:var(--accent);color:var(--accent-on)}
.pricingTier{font-size:14px;font-weight:700;color:var(--text-3);text-transform:uppercase;letter-spacing:.04em}
.pricingPrice{margin:8px 0 2px;font-size:38px;font-weight:800;color:var(--text-1);line-height:1}
.pricingPer{font-size:15px;font-weight:600;color:var(--text-3)}
.pricingLede{margin:4px 0 14px;font-size:14px;color:var(--text-2);line-height:1.5}
.pricingLede b{color:var(--text-1)}
.pricingFeatures{list-style:none;margin:0 0 18px;padding:0;flex:1}
.pricingFeatures li{position:relative;padding:0 0 0 24px;margin:0 0 10px;font-size:14px;color:var(--text-2);line-height:1.45}
.pricingFeatures li::before{content:"";position:absolute;left:2px;top:6px;width:12px;height:7px;border-left:2px solid var(--good-text);border-bottom:2px solid var(--good-text);transform:rotate(-45deg)}
.pricingCta{margin-top:auto}
.pricingCurrentTag{margin-top:auto;padding:11px 0;text-align:center;font-size:14px;font-weight:650;color:var(--text-3);border-top:1px solid var(--line)}

.pricingFounding{display:flex;align-items:center;gap:20px;flex-wrap:wrap;margin-top:20px;padding:22px;border-radius:16px;background:var(--accent-soft);border:1px solid var(--accent)}
.pricingFoundingBody{flex:1;min-width:240px}
.pricingFoundingTag{display:inline-block;margin-bottom:6px;padding:3px 10px;border-radius:999px;font-size:11px;font-weight:750;letter-spacing:.03em;text-transform:uppercase;background:var(--accent);color:var(--accent-on)}
.pricingFounding h3{margin:0 0 6px;font-size:19px;color:var(--text-1)}
.pricingFounding p{margin:0;font-size:14px;color:var(--text-2);line-height:1.55;max-width:640px}
.pricingFounding p b{color:var(--text-1)}
.pricingFounding .pricingCta{flex:none;margin-top:0}
.pricingFounding .pricingCurrentTag{flex:none;margin-top:0;border-top:0;padding:0}

.pricingWhy{margin-top:24px;padding:22px;border-radius:16px;background:var(--bg-surface);border:1px solid var(--line)}
.pricingWhy h3{margin:0 0 8px;font-size:19px;color:var(--text-1)}
.pricingWhy p{margin:0 0 12px;font-size:14px;color:var(--text-2);line-height:1.55;max-width:680px}
.pricingWhyList{list-style:none;margin:0 0 12px;padding:0}
.pricingWhyList li{position:relative;padding:0 0 0 20px;margin:0 0 9px;font-size:14px;color:var(--text-2);line-height:1.5}
.pricingWhyList li::before{content:"";position:absolute;left:2px;top:8px;width:6px;height:6px;border-radius:50%;background:var(--accent)}
.pricingWhyList li b{color:var(--text-1)}
.pricingWhyFoot{font-size:13.5px;color:var(--text-3)}

@media(max-width:820px){
  .pricingGrid{grid-template-columns:1fr}
  .pricingBestTag{left:auto;right:22px}
}
/* === end Monetization #3 === */

/* === Upload flow (#1) + Plaid connect (#2) ================================
   Statement/CSV/image upload -> auto-build wallet, plus an opt-in Plaid
   auto-sync affordance. Built to the same idioms as the rest of the app:
   .autoSyncCard for the card shell, the site's one .scrim/.modal pattern
   for the dialog, semantic tokens throughout so it is light+dark safe by
   construction. The only raw colors are the two confidence-chip tints,
   which carry explicit [data-theme="dark"] overrides below. */

/* 1. Two OPTIONAL buttons inside the existing .autoSyncCard actions row.
   Deliberately quieter than the card's .btn.primary -- ghost/secondary so
   they read as "also available", not competing calls to action. Sized a
   touch smaller than .btn (14px/11px vs 15px/13px) but keep a 44px tap
   target. Self-contained so they work with or without a .btn class. */
.autoSyncUploadBtn,.autoSyncPlaidBtn{
  display:inline-flex;align-items:center;justify-content:center;gap:8px;
  border:1px solid var(--line-strong);background:var(--bg-surface);color:var(--brand-ink);
  border-radius:999px;padding:11px 18px;font:650 14px var(--font);cursor:pointer;
  min-height:44px;text-decoration:none;transition:border-color .12s,background .12s}
.autoSyncUploadBtn:hover,.autoSyncPlaidBtn:hover{border-color:var(--accent);background:var(--accent-soft)}
.autoSyncUploadBtn svg,.autoSyncPlaidBtn svg{width:16px;height:16px;flex:none}

/* 2. Upload modal -- mirrors the site's .scrim/.modal shell (same z-index
   band 50, same dim backdrop, rounded panel, close affordance) so Esc /
   backdrop-close conventions come for free. Wider than the base .modal
   (~520px) with a scrollable body for the review step. */
.uploadScrim{display:none;position:fixed;inset:0;background:rgba(20,32,43,.5);z-index:50;
  align-items:center;justify-content:center;padding:20px}
.uploadScrim.on{display:flex}
.uploadModal{background:var(--bg-surface);border-radius:20px;max-width:520px;width:100%;
  max-height:86vh;overflow:auto;padding:26px;box-shadow:0 20px 60px rgba(0,0,0,.3);position:relative}
.uploadModal h3{margin:0 0 6px;color:var(--text-1)}
.uploadModal .x,.uploadClose{position:absolute;top:14px;right:16px;float:none;cursor:pointer;
  color:var(--text-3);font-size:20px;line-height:1;border:0;background:none;padding:6px;min-height:36px;min-width:36px}
.uploadModal .x:hover,.uploadClose:hover{color:var(--text-1)}

/* File-picker drop zone: dashed border, quiet hover, tap-friendly. */
.uploadDrop{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;
  text-align:center;border:1.5px dashed var(--line-strong);border-radius:14px;
  background:var(--bg-surface-2);color:var(--text-2);padding:28px 20px;cursor:pointer;
  font:650 14px var(--font);min-height:120px;transition:border-color .12s,background .12s}
.uploadDrop:hover,.uploadDrop.dragover{border-color:var(--accent);background:var(--accent-soft);color:var(--accent-text)}
.uploadDrop svg{width:26px;height:26px;color:var(--text-3)}
.uploadDrop:hover svg,.uploadDrop.dragover svg{color:var(--accent-text)}

/* Small, honest muted copy under the drop zone. */
.uploadHint{font-size:12.5px;color:var(--text-3);line-height:1.5;margin:10px 0 0}

/* PP-078 (D-040 B.4/dispatch requirement 4): preview screen -- shows the
   ACTUAL redacted bytes back to the user before Send is clickable-to-
   effect. A <pre> block defaults to white-space:pre (no wrap), which
   would overflow horizontally on a narrow viewport for any long redacted
   line -- these three rules force wrapping and a bounded scroll height so
   the preview never pushes the modal wider than its own 520px cap. */
.uploadPreviewBox{max-height:38vh;overflow-y:auto;margin-top:14px;border:1px solid var(--line);
  border-radius:12px;background:var(--bg-surface-2)}
.uploadPreviewFile{padding:12px 14px;border-bottom:1px solid var(--line)}
.uploadPreviewFile:last-child{border-bottom:0}
.uploadPreviewName{font-size:12.5px;font-weight:650;color:var(--text-2);margin-bottom:6px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.uploadPreviewText{margin:0;font:12.5px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  color:var(--text-1);white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere}

/* Chosen-file chip: name + size + remove. */
.uploadFileRow{display:flex;align-items:center;gap:10px;margin-top:12px;padding:10px 12px;
  border:1px solid var(--line);border-radius:12px;background:var(--bg-surface-2)}
.uploadFileRow .uploadFileName{flex:1;min-width:0;font-size:13.5px;font-weight:650;color:var(--text-1);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.uploadFileRow .uploadFileSize{flex:none;font-size:12px;color:var(--text-3);font-variant-numeric:tabular-nums}
.uploadFileRemove{flex:none;border:0;background:none;color:var(--text-3);font-size:16px;line-height:1;
  cursor:pointer;padding:6px;min-height:36px;min-width:36px;border-radius:999px;transition:background .12s,color .12s}
.uploadFileRemove:hover{background:var(--line);color:var(--text-1)}

/* 3. Confirm-before-save review card inside the modal. */
.uploadReview{margin-top:16px}
.uploadReviewHead{font-size:11.5px;font-weight:750;letter-spacing:.04em;text-transform:uppercase;
  color:var(--text-3);margin:18px 0 8px;padding-top:14px;border-top:1px solid var(--line)}
.uploadReviewHead:first-child{margin-top:0;padding-top:0;border-top:0}
.uploadReviewRow{display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid var(--line)}
.uploadReviewRow:last-child{border-bottom:0}
.uploadReviewRow .uploadReviewLabel{flex:1;min-width:0;font-size:13.5px;color:var(--text-2);line-height:1.4}
.uploadReviewRow input,.uploadReviewRow .uploadReviewInput{flex:none;width:130px;padding:8px 10px;
  border:1px solid var(--line);border-radius:9px;background:var(--bg-surface);color:var(--text-1);
  font:600 14px var(--font);text-align:right;font-variant-numeric:tabular-nums}
.uploadReviewRow input:focus,.uploadReviewRow .uploadReviewInput:focus{outline:2px solid var(--accent);outline-offset:1px;border-color:var(--accent)}

/* Confidence chip: amber tone = low (needs a look), green = high. The amber
   reuses the already-themed gold accent pair (subtle, honest "review me");
   the green needs literals, so it carries a dark override just below. */
.uploadConf{flex:none;display:inline-flex;align-items:center;font-size:10.5px;font-weight:750;
  letter-spacing:.02em;text-transform:uppercase;border-radius:999px;padding:3px 8px;border:1px solid transparent}
.uploadConf--low{color:var(--accent-text);background:var(--accent-soft);border-color:var(--accent)}
.uploadConf--high{color:var(--good-text);background:rgba(15,118,110,.12);border-color:rgba(15,118,110,.32)}

/* Unmatched-item picker (a chosen dropdown, matched to the input idiom). */
.uploadReviewPick{width:100%;padding:9px 11px;border:1px solid var(--line);border-radius:9px;
  background:var(--bg-surface);color:var(--text-1);font:600 14px var(--font);cursor:pointer}
.uploadReviewPick:focus{outline:2px solid var(--accent);outline-offset:1px;border-color:var(--accent)}

/* Confirm / cancel action row -- prominent primary, ghost cancel. */
.uploadReviewActions{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:22px;
  padding-top:16px;border-top:1px solid var(--line)}
.uploadReviewConfirm{display:inline-flex;align-items:center;justify-content:center;gap:8px;
  border:0;background:var(--brand);color:#fff;border-radius:999px;padding:13px 22px;
  font:700 15px var(--font);cursor:pointer;min-height:44px;box-shadow:var(--shadow);transition:transform .05s}
.uploadReviewConfirm:active{transform:translateY(1px)}
.uploadReviewCancel{border:0;background:none;color:var(--text-3);font:650 13.5px var(--font);
  cursor:pointer;padding:8px 4px;min-height:44px;transition:color .12s}
.uploadReviewCancel:hover{color:var(--text-1)}

/* 4. Optional Plaid connect card -- same calm, low-key tone as .autoSyncCard,
   so it reuses that shell's tokens verbatim (no louder styling). */
.plaidConnectCard{background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--radius);
  padding:18px 20px;box-shadow:var(--shadow)}
.plaidConnectCard h3,.plaidConnectHead{margin:0 0 6px;font-size:16px;font-weight:700;color:var(--text-1);line-height:1.35}
.plaidConnectCard p,.plaidConnectBody{margin:0 0 12px;font-size:13.5px;color:var(--text-2);line-height:1.5;max-width:520px}
.plaidConnectReassure{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:650;
  color:var(--accent-text);background:var(--accent-soft);border-radius:999px;padding:5px 12px;margin:0 0 14px}
.plaidConnectActions{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
@media(max-width:480px){.plaidConnectCard{padding:16px}.plaidConnectActions{gap:10px}}

/* 5. Success toast line after a save. */
.uploadToast{display:inline-flex;align-items:center;gap:8px;font-size:13.5px;font-weight:650;
  color:var(--good-text);background:rgba(15,118,110,.12);border:1px solid rgba(15,118,110,.28);
  border-radius:12px;padding:10px 14px;line-height:1.4}
.uploadToast svg{width:16px;height:16px;flex:none}

/* Dark overrides -- only where a literal green tint is used. Everything else
   above is semantic-token driven and needs no override. */
[data-theme="dark"] .uploadConf--high{background:rgba(52,211,153,.13);border-color:rgba(52,211,153,.32)}
[data-theme="dark"] .uploadToast{background:rgba(52,211,153,.12);border-color:rgba(52,211,153,.28)}

/* Bottom-sheet on small screens, matching the interview/nav sheet idiom. */
@media(max-width:560px),(pointer:coarse){
  .uploadScrim{align-items:flex-end;padding:0}
  .uploadModal{max-width:none;border-radius:20px 20px 0 0;max-height:90vh;padding:22px 18px 20px}
  .uploadReviewRow input,.uploadReviewRow .uploadReviewInput{width:110px}
}

/* Respect reduced-motion: drop every transition added above. */
@media(prefers-reduced-motion:reduce){
  .autoSyncUploadBtn,.autoSyncPlaidBtn,.uploadDrop,.uploadFileRemove,
  .uploadReviewConfirm,.uploadReviewCancel{transition:none}
}
/* === end Upload flow (#1) + Plaid connect (#2) === */

/* === #4 PROGRAMS REDESIGN === */
/* Header — mirrors Strategy v2's st2Head language. .st2Kicker is reused
   directly in the markup; the h1/sub get page-scoped clones so no shared
   rule is touched. */
/* The Best Card extension callout under the beta CTAs.
   Owner-reported 2026-08-01 with a screenshot: at 440px "Best Card" rendered as
   a narrow two-line orange column beside the sentence, with the comma orphaned.

   Cause, from the markup: the anchor carried inline `display:inline-flex;
   align-items:center`. Flex makes every child a flex ITEM, so "New: ", the
   <b>Best Card</b>, and the rest of the sentence became three items laid out in
   a row. At phone width the bold item got squeezed and wrapped inside its own
   column instead of flowing as part of one sentence.

   inline-flex was presumably reached for to get `min-height:44px` to apply for
   the tap target. It is not needed for that: inline-block honours min-height
   too, and padding does the vertical centring without turning prose into flex
   items. So the text is text again. */
.extCallout{display:inline-block;min-height:44px;padding:11px 0;margin-top:10px;
  color:var(--muted);font-size:12px;font-weight:750;text-decoration:none;line-height:1.5}
.extCallout b{color:var(--coral);margin:0 4px 0 3px}
.extCallout:hover{color:var(--text-2)}

.exHead{margin-top:10px}
/* h1 AND h2: the Programs header shipped as <h1> in the shell, which meant every
   prerendered page in this single-document SPA carried it, and 15 of 16 routes
   led with someone else's headline in DOM order. It is an <h2> in the source
   now and the prerender promotes only the active route's heading. This selector
   carries both so the demotion moves no pixels — the rule sets font-family,
   font-size, font-weight, letter-spacing and margin, i.e. every property the
   bare h1/h2 element rules would otherwise supply. Do not split it back apart. */
.exHead h1,.exHead h2{font-family:var(--font-display);font-size:clamp(24px,3.4vw,33px);font-weight:600;letter-spacing:-.015em;margin:0}
.exHeadSub{color:var(--text-3);margin:7px 0 0;font-size:14.5px;max-width:640px;line-height:1.55}
.exHeadSub .pill{vertical-align:middle}

/* Search — same .searchbar control, with a magnifier affordance. */
.exSearchWrap{position:relative;margin:18px 0 8px}
.exSearchIcon{position:absolute;left:14px;top:50%;transform:translateY(-50%);width:18px;height:18px;color:var(--text-4);pointer-events:none}
.exSearchInput{padding-left:42px;margin-bottom:0}

/* Tab rail — Strategy v2's pill tabs, kept horizontally scrollable because
   Programs has five. .tabs/.tab class names are preserved (JS contract);
   .exTabs only restyles them. */
.tabs.exTabs{background:var(--bg-surface-2);border:1px solid var(--line);border-radius:999px;padding:5px;gap:4px;width:max-content;max-width:100%;margin:14px 0 18px}
.exTabs .tab{border-bottom:0;border-radius:999px;padding:9px 16px;min-height:40px;font-size:13.5px;font-weight:700;color:var(--text-3);display:inline-flex;align-items:center;gap:7px;transition:color .15s}
.exTabs .tab.on{background:var(--accent);color:var(--accent-on);box-shadow:0 2px 10px rgba(199,154,59,.35)}
.exTabs .exTabCount{font-size:10.5px;font-weight:800;background:var(--line);color:var(--text-3);border-radius:999px;padding:1.5px 7px;line-height:1.4}
.exTabs .exTabCount:empty{display:none}
.exTabs .tab.on .exTabCount{background:rgba(20,32,43,.16);color:var(--accent-on)}

/* Overview stat tiles — the landing's "what's in here" scent, same card
   surface + hover-lift as the homepage/Strategy tiles. */
.exStatGrid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:4px 0 26px}
@media(max-width:900px){.exStatGrid{grid-template-columns:repeat(2,1fr)}}
.exStatTile{position:relative;text-align:left;background:var(--bg-surface);border:1px solid var(--line);border-radius:16px;box-shadow:var(--shadow);padding:16px 18px;cursor:pointer;font-family:var(--font);display:flex;flex-direction:column;gap:2px;transition:border-color .12s,box-shadow .12s,transform .08s}
.exStatTile:hover{border-color:var(--brand);box-shadow:0 3px 12px rgba(20,32,43,.10);transform:translateY(-1px)}
.exStatTile>b{font-size:24px;font-weight:800;letter-spacing:-.01em;color:var(--text-1);font-variant-numeric:tabular-nums}
.exStatLabel{font-size:13px;font-weight:750;color:var(--text-2)}
.exStatSub{font-size:11.5px;color:var(--text-4);font-weight:600}
.exStatArrow{position:absolute;right:14px;top:14px;color:var(--text-4);font-weight:700;transition:transform .12s,color .12s}
.exStatTile:hover .exStatArrow{color:var(--accent-text);transform:translateX(2px)}

/* Group heads — chevron disclosure instead of the dated hamburger/x circle,
   plus an optional brand mark (Transfers passes the bank logo). Page-scoped
   overrides; base .exGroup rules untouched. */
#view-explore .exGroupToggle{background:transparent;color:var(--text-3);width:18px;height:18px;font-size:16px}
#view-explore .exGroupToggle::before{content:"›";display:block;transition:transform .15s}
#view-explore .exGroup[open] .exGroupToggle::before{content:"›";transform:rotate(90deg)}
.exGroupMark{display:inline-flex;flex:none}

/* Detail modals — feeding-currency rows (logo + name) and the partner chip
   grid (allyChip + a ratio pill; static variant for untracked partners). */
.exFeedCur{display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap}
.exFeedCur .plogo img,.exFeedCur .plogo .mono{width:20px;height:20px}
.exPartnerGrid{grid-template-columns:repeat(auto-fill,minmax(104px,1fr))}
.allyChip.exPartnerChip{height:auto;min-height:94px}
span.allyChip.exPartnerChip--static{cursor:default}
span.allyChip.exPartnerChip--static:hover{border-color:var(--line);box-shadow:0 1px 2px rgba(20,32,43,.05);transform:none}
.exChipRatio{font-size:10.5px;font-weight:700;color:var(--text-4);background:var(--bg-surface-2);border-radius:999px;padding:1px 8px;font-variant-numeric:tabular-nums}

/* Overview empty state (classes replace the old inline styles). */
.exEmptyState{padding:34px 22px;text-align:center}
.exEmptyTitle{margin:0 0 6px;font-weight:750}
.exEmptySub{margin:0 auto 18px;max-width:440px;color:var(--text-3);font-size:13.5px;line-height:1.55}

/* --- Detail card (card rework) ----------------------------------------
   Jeremy's note on the first pass: the right-edge slide-over read as "a
   fixed window pinned to the top right", not a card opening. Reworked:
   ≥641px the detail is a CENTERED floating card that springs open FROM THE
   ROW THE USER CLICKED — openDetail (ui-shared.js) writes the click point
   into --exCardOx/--exCardOy, which drives transform-origin here, so the
   scale-up visually grows out of the tapped row/chip (shared-element feel)
   over a blurred, dimmed backdrop. The overshoot cubic-bezier gives it a
   small spring settle; depth comes from a 3-layer shadow + hairline border
   instead of an edge-bolted rail. ≤640px stays the bottom sheet. Same
   .scrim/.modal open/close contract as before (Esc, ✕, outside tap);
   content classes (.exDetail*) unchanged, shared by showCard/showProgram
   (router.js) + exStatusCardHTML (explore.js). */
#detailScrim{align-items:center;justify-content:center;padding:28px 20px;background:rgba(15,23,32,.42);-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px)}
#detailScrim.on{animation:exScrimIn .26s ease}
@keyframes exScrimIn{from{opacity:0}to{opacity:1}}
.modal.detailPanel{position:relative;max-width:560px;width:100%;max-height:min(84vh,780px);border-radius:24px;overflow-y:auto;overscroll-behavior:contain;padding:24px 28px 30px;border:1px solid var(--line);box-shadow:0 1px 2px rgba(0,0,0,.10),0 14px 34px rgba(0,0,0,.18),0 44px 96px rgba(0,0,0,.30);transform-origin:var(--exCardOx,50%) var(--exCardOy,58%);animation:exCardIn .46s cubic-bezier(.22,1.3,.32,1)}
.modal.detailPanel:focus{outline:none}
@keyframes exCardIn{from{transform:scale(.55) translateY(10px);opacity:0}55%{opacity:1}to{transform:none;opacity:1}}
.modal.detailPanel .x{position:sticky;top:0;z-index:2;margin:-4px -8px 0 0;font-size:22px;padding:4px 8px}
@media(max-width:640px){
  #detailScrim{align-items:flex-end;justify-content:stretch;padding:0;-webkit-backdrop-filter:none;backdrop-filter:none}
  .modal.detailPanel{max-width:none;height:auto;max-height:88vh;border-radius:22px 22px 0 0;border:0;box-shadow:0 -14px 44px rgba(0,0,0,.3);transform-origin:50% 100%;animation:exSheetIn .24s ease;padding:12px 20px 26px}
  .modal.detailPanel::before{content:"";display:block;width:44px;height:4px;border-radius:99px;background:var(--line);margin:0 auto 10px}
}
@keyframes exSheetIn{from{transform:translateY(30px);opacity:.35}to{transform:none;opacity:1}}

/* Hero head: brand mark + display-face name + type/alliance meta. */
.exDetailHero{display:flex;align-items:center;gap:14px;padding-right:30px}
.exDetailHero .plogo img,.exDetailHero .plogo .mono{width:46px;height:46px;border-radius:12px}
.exDetailHero .ico{width:46px;height:46px;font-size:15px;border-radius:12px;display:inline-flex;align-items:center;justify-content:center;flex:none}
.exDetailHeroText h3{margin:0;font-family:var(--font-display);font-size:21px;font-weight:600;letter-spacing:-.012em}
.exDetailMeta{color:var(--text-3);font-size:13px;margin-top:3px}

/* Stat blocks (typical value / pricing / surcharges / fee). */
.exDetailStats{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:8px;margin:16px 0 4px}
.exDetailStat{display:flex;flex-direction:column;gap:2px;background:var(--bg-surface-2);border:1px solid var(--line);border-radius:12px;padding:10px 12px}
.exDetailStatLabel{font-size:10px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:var(--text-4)}
.exDetailStat b{font-size:14px;color:var(--text-1)}
.exDetailStat .receipt-trig{font-size:14px;font-weight:700}
.exDetailStatFoot{font-size:11px;color:var(--text-4)}

/* Sweet-spot callout. */
.exDetailSweet{background:var(--accent-soft);border:1px solid var(--accent);color:var(--accent-text);border-radius:12px;padding:11px 14px;font-size:13.5px;line-height:1.5;margin:12px 0 0}
.exDetailSweetLabel{display:block;font-size:10px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;margin-bottom:3px}

/* Elite-status link card (task 2) + the compact row badge variant. */
.exDetailStatusCard{display:flex;align-items:center;justify-content:space-between;gap:12px;background:var(--bg-surface);border:1px solid var(--line);border-radius:14px;padding:12px 14px;margin:14px 0 0;text-decoration:none;color:inherit;transition:border-color .12s,box-shadow .12s}
.exDetailStatusCard:hover{border-color:var(--brand);box-shadow:0 3px 12px rgba(20,32,43,.10)}
.exDetailStatusText{display:flex;flex-direction:column;gap:1px}
.exDetailStatusText b{font-size:13.5px}
.exDetailStatusText .muted{font-size:12px}
.exDetailStatusArrow{color:var(--text-4);font-weight:700;transition:transform .12s,color .12s}
.exDetailStatusCard:hover .exDetailStatusArrow{color:var(--accent-text);transform:translateX(2px)}
.badge.badge--statusLink{cursor:pointer;text-decoration:none;color:var(--accent-text);border:1px solid var(--accent);background:var(--accent-soft)}
.badge.badge--statusLink:hover{filter:brightness(.96)}

/* Sectioned body (earning / credits / transfer in / bookable partners). */
.exDetailSection{margin-top:18px}
/* ---- STRATEGY ⇄ EXPLORE HANDOFF (Jeremy 2026-07-25 decision 1) ----
   Three link sites, one visual language. The Explore-side link is the last
   block in the detail panel and gets its own rule above it so it reads as
   an exit rather than another fact. .explore-link is `margin-left:auto`
   for its usual flex-row home in chat, which does nothing useful here, so
   the handoff sets its own block layout and a 44px tap height. */
.exDetailHandoff{border-top:1px solid var(--line);padding-top:14px}
.exDetailHandoff .explore-link{display:inline-flex;align-items:center;min-height:44px;
  margin-left:0;font-size:13.5px;font-weight:700;color:var(--accent-text)}
/* Strategy side: a quiet trailing link inside a card, not a button. */
.st2BoostSrc{align-self:flex-start;display:inline-flex;align-items:center;min-height:44px}
/* The sweet-spot program name is an 11px uppercase label, and it has to stay
   that size — it is a label above the spot's real title, not a heading. So the
   TEXT stays 13px tall and the HIT AREA is grown to 44 with a transparent
   ::after, rather than padding the label out and adding ~30px to each of 24
   rows. Measured at 440×956: the row is 121px tall with 12px above the label
   and 4px below it, so a 44px box centred on the label spills only into the
   row's own padding and its non-interactive title text. It cannot reach the
   neighbouring row's link, and there is no other clickable thing inside a
   sweet-spot row for it to shadow. Verified with elementFromPoint at the top
   and bottom edges of the expanded box. */
.sweetSpotProgLink{position:relative;display:inline-block;
  font-size:11px;font-weight:700;letter-spacing:.02em;
  text-transform:uppercase;color:var(--accent-text)}
.sweetSpotProgLink::after{content:"";position:absolute;left:0;right:0;top:50%;
  transform:translateY(-50%);height:44px}
.exDetailSection h4{margin:0 0 8px;font-size:11px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:var(--text-4)}
.exDetailSection p{margin:0;font-size:13.5px;line-height:1.55}

@media(prefers-reduced-motion:reduce){
  .exStatTile,.exStatArrow,#view-explore .exGroupToggle::before,.exTabs .tab{transition:none}
  .modal.detailPanel{animation:none}
  #detailScrim.on{animation:none}
  .exDetailStatusCard,.exDetailStatusArrow{transition:none}
}
/* === end #4 PROGRAMS REDESIGN === */
/* === #6 DATA & RESOURCES REDESIGN === */
/* Fable design pass (Jeremy 2026-07-22): the Data & Resources landing + its
   three sub-pages (Methodology / Updates / Elite Status) join the same design
   language as the homepage and Points Strategy — st2-style kicker pill, one
   serif display title per page (--font-display), token cards with hairline
   rows, and the whole landing card is now ONE real <a> link. Every colour is
   a WS-THEME token, so light + dark both come for free. New page-scoped
   classes only; the older .resCard/.dataCrumb base rules (~line 3124) still
   apply and are refined here by later-cascade overrides, not edited. */

/* ---- shared head idiom: kicker pill + serif display title + sub ---- */
.resKicker{display:inline-block;font-size:11.5px;font-weight:800;letter-spacing:.07em;
  text-transform:uppercase;color:var(--accent-text);background:var(--accent-soft);
  padding:4px 10px;border-radius:999px}
.resPageTitle{font-family:var(--font-display);font-size:clamp(24px,3.4vw,33px);
  font-weight:600;letter-spacing:-.015em;line-height:1.12;margin:8px 0 0;color:var(--text-1)}
.resPageSub{margin:8px 0 18px;max-width:640px}
.resHead{margin:10px 0 26px}
.resHead .resIntro{margin:8px 0 0}

/* ---- the pill breadcrumb atop each sub-page: same accent-soft chip idiom
   as .resKicker, but it IS the back link — one element does both jobs. ---- */
.resCrumb{margin:12px 0 2px;min-height:0;padding:6px 12px 6px 8px;border-radius:999px;
  background:var(--accent-soft);color:var(--accent-text);
  font:800 11.5px var(--font);letter-spacing:.07em;text-transform:uppercase;
  transition:filter .12s,color .12s}
.resCrumb:hover{color:var(--accent-text);filter:brightness(1.06);text-decoration:none}
.resCrumb svg{width:14px;height:14px}
.resCrumb:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* ---- landing: the WHOLE card is the link now ---- */
a.resCard{color:inherit;text-decoration:none;cursor:pointer;position:relative;
  transition:border-color .14s,box-shadow .14s,transform .14s}
a.resCard:hover{border-color:var(--accent);box-shadow:var(--shadow);transform:translateY(-2px)}
a.resCard:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.resGrid{gap:14px}
.resCardIco{width:40px;height:40px;border-radius:12px;background:var(--accent-soft);
  color:var(--accent-text);display:grid;place-items:center;margin-bottom:14px;flex:none}
.resCardIco svg{width:20px;height:20px}
.resCardTitle,.resCardBody{display:block}
.resCardGo{margin-top:auto;display:inline-flex;align-items:center;gap:6px;
  font-size:13.5px;font-weight:750;color:var(--accent-text)}
.resCardArrow{width:15px;height:15px;flex:none;transition:transform .18s ease}
a.resCard:hover .resCardArrow{transform:translateX(3px)}
.resFootNote{margin:18px 2px 0;font-size:12.5px;color:var(--text-4)}
/* Below tablet the three cards stack as full-width rows: icon rail on the
   left, copy beside it — less scrolling than three tall stacked cards. */
@media(max-width:860px){
  .resGrid{grid-template-columns:1fr;gap:12px}
  a.resCard{display:grid;grid-template-columns:40px minmax(0,1fr);
    grid-template-rows:auto auto auto;column-gap:14px;row-gap:0;
    align-items:start;padding:16px 18px}
  .resCardIco{grid-column:1;grid-row:1 / span 3;margin:0}
  .resCardTitle{grid-column:2;font-size:16px;margin:1px 0 4px}
  .resCardBody{grid-column:2;margin:0 0 10px}
  .resCardGo{grid-column:2}
}

/* ---- Elite Status sub-page ---- */
/* The old inline-styled .banner becomes a calm accent-edged note card. */
.resNote{background:var(--bg-surface-2);border:1px solid var(--line);
  border-left:3px solid var(--accent);border-radius:12px;padding:12px 16px;
  font-size:13.5px;line-height:1.55;color:var(--text-2);margin:0 0 16px;max-width:860px}
#view-status .searchbarLg{margin:2px 0 16px}

/* ---- Elite Status: tier comparison columns (Fable pass 2, 2026-07-23) ----
   statusLadderHTML (wallet.js) renders one column per tier, lowest → highest,
   in a horizontally scrollable, snap-aligned rail. The rail is a 4-row grid
   (head / requirement / delta / full perks) and every column subgrids into it,
   so the same section sits at the same height in every column — the row
   alignment is what makes it a comparison, not just cards in a row. Browsers
   without subgrid drop that one declaration and get auto rows, still columns.
   The delta panel (what this tier introduces over its left neighbour) is the
   loud element — accent-soft fill, accent hairline; everything else is the
   quiet surface + hairline idiom of the rest of the site. */
.statusLadder{margin-top:2px}
/* Reciprocal "View in Programs →" pill (statusLadderHTML head) — the mirror
   of the Programs page's "Elite status →" affordance. Rides the existing
   .statusProgHead wrapping flex row: margin-left:auto pushes it to the right
   end of the header on wide screens; on phones the row wraps and it sits on
   its own line under the name, left-aligned. Quiet pill in the same
   surface-plus-hairline idiom as the tier columns, accent text like the
   site's other in-app links; warms to the accent on hover. */
.statusProgLink{margin-left:auto;display:inline-flex;align-items:center;
  padding:8px 14px;border:1px solid var(--line);border-radius:999px;
  background:var(--bg-surface);box-shadow:var(--shadow);
  font-size:13px;font-weight:650;color:var(--accent-text);
  text-decoration:none;white-space:nowrap}
.statusProgLink:hover{border-color:var(--accent);background:var(--accent-soft)}
@media(max-width:640px){.statusProgLink{margin-left:0}}
/* Job 5 (MOBILE-SPEC issue 9): right-edge "more to scroll" fade, pure CSS,
   no JS hook needed. Two background layers on .tierRail itself:
   - a `local`-attached cover (transparent -> --bg-canvas) glued to the END
     of the SCROLLING CONTENT, not the viewport;
   - a `scroll`-attached radial shadow glued to the CONTAINER's visible
     right edge, so it always renders at the same on-screen spot.
   Away from the end these sit at different physical positions, so the
   shadow shows through the cover's transparent side. Scrolled all the way
   right, the cover's solid tail lands on exactly the same on-screen pixels
   as the shadow and paints over it -- so it disappears at the exact moment
   there's nothing left to scroll to. (The classic Lea Verou scroll-shadow
   technique, rotated 90deg for this row's horizontal scroll; only the
   trailing edge is done since the leading edge is never the ambiguous one
   here -- the peeking next column already signals "keep going" going in.)
   Colors are both existing tokens: --bg-canvas matches the page backdrop
   .tierRail already sits on, and --line-strong is the file's standard
   hairline tint, which is already themed correctly in both modes, so no
   [data-theme="dark"] override is needed here. No arrow buttons (per the
   brief); dots below are the discrete indicator, this is the continuous one. */
.tierRail{display:grid;grid-auto-flow:column;grid-auto-columns:250px;
  grid-template-rows:auto auto auto 1fr;column-gap:12px;
  overflow-x:auto;padding:4px 2px 16px;margin:0 -2px;
  scroll-snap-type:x proximity;-webkit-overflow-scrolling:touch;
  background:
    linear-gradient(to right, transparent, var(--bg-canvas) 90%) 100% 0/36px 100% no-repeat local,
    radial-gradient(farthest-side at 100% 50%, var(--line-strong), transparent) 100% 0/14px 100% no-repeat scroll;
  background-color:var(--bg-canvas)}
.tierCol{grid-row:1/-1;display:grid;grid-template-rows:subgrid;
  background:var(--bg-surface);border:1px solid var(--line);
  border-radius:var(--radius);box-shadow:var(--shadow);overflow:hidden;
  scroll-snap-align:start;min-width:0}
.tierCol--top{border-color:var(--accent)}
.tierColHead{padding:14px 16px 0}
.tierColStep{font-size:10.5px;font-weight:800;letter-spacing:.08em;
  text-transform:uppercase;color:var(--text-4)}
.tierColName{margin:3px 0 0;font-size:16.5px;font-weight:800;
  letter-spacing:-.01em;color:var(--text-1)}
.tierColReq{padding:5px 16px 12px;font-size:12.5px;line-height:1.5;
  font-weight:600;color:var(--text-3);border-bottom:1px solid var(--line)}
.tierColDelta{margin:12px 12px 14px;padding:10px 12px;border-radius:10px;
  background:var(--accent-soft);border-left:3px solid var(--accent)}
.tierColDelta--base{background:var(--bg-surface-2);border-left-color:var(--line)}
.tierDeltaLabel{font-size:11px;font-weight:800;letter-spacing:.06em;
  text-transform:uppercase;color:var(--accent-text)}
.tierColDelta--base .tierDeltaLabel{color:var(--text-4)}
.tierDeltaList{margin:6px 0 0;padding-left:16px;font-size:13px;line-height:1.55;
  color:var(--text-1)}
.tierDeltaList li{margin:3px 0}
.tierDeltaNone{margin-top:5px;font-size:12.5px;line-height:1.5;color:var(--text-3)}
.tierColPerks{padding:11px 16px 15px;border-top:1px solid var(--line)}
.tierColPerksLabel{font-size:10.5px;font-weight:800;letter-spacing:.08em;
  text-transform:uppercase;color:var(--text-4);margin-bottom:6px}
.tierColPerks .tierPerks{font-size:13px;line-height:1.55;color:var(--text-2);
  padding-left:16px}
.tierColNone{list-style:none;margin-left:-16px;color:var(--text-4)}
.tierRailHint{font-size:12.5px;margin:0 0 8px}
.tierSrcLine{font-size:12.5px;margin:2px 2px 0}
.tierSrcLine a{color:var(--accent-text);font-weight:650}
/* Job 2 / MOBILE-SPEC issue 9: quiet position dots under the rail, wired by
   tierRailWireDots() in wallet.js (one .tierDot per .tierCol). Passive
   indicator only -- wallet.js never wires a click handler, so this is
   deliberately not sized as a tap target. The markup starts display:none;
   JS clears the inline style (display:'') only once it confirms the rail
   actually overflows, so this default display:flex only ever shows up
   alongside real dots, never an empty bar. */
.tierDots{display:flex;align-items:center;justify-content:center;gap:6px;margin-top:10px}
.tierDot{width:6px;height:6px;border-radius:50%;background:var(--line-strong);flex:none;transition:background .15s,transform .15s}
.tierDot.is-active{background:var(--accent);transform:scale(1.35)}
@media(prefers-reduced-motion:reduce){.tierDot{transition:none}}
/* ≥861px: up to 4 columns (4×250 + 3×12 = 1036px) fit the 1100px wrap, so the
   scroll hint only earns its place on 5-tier-plus programs there. */
@media(min-width:861px){.tierRailHint--fits{display:none}}
/* Mobile: one full column per screen with a peek of the next; firmer snap. */
@media(max-width:640px){
  .tierRail{grid-auto-columns:min(82vw,300px);scroll-snap-type:x mandatory}
}

/* ---- Updates sub-page (markup classes from wallet.js's renderUpdatesHTML) ---- */
.updSecTitle{font-size:17px;font-weight:800;letter-spacing:-.01em;margin:28px 0 8px}
#updatesWrap>.updSecTitle:first-child{margin-top:4px}
.updIntro{font-size:13.5px;line-height:1.55;margin:0 0 8px;max-width:720px}
.updToggle{display:flex;gap:8px;margin:12px 0 2px;flex-wrap:wrap}
.updGroup{margin-top:18px}
.updGroupTitle{font-size:11.5px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;
  color:var(--text-4);margin:0 0 8px}
.updGroupCard{background:var(--bg-surface);border:1px solid var(--line);
  border-radius:var(--radius);padding:2px 18px}
.updItem{padding:13px 0;border-bottom:1px solid var(--line)}
.updItem:last-child{border-bottom:0}
.updItemTop{display:flex;justify-content:space-between;align-items:baseline;
  gap:4px 12px;flex-wrap:wrap}
.updItemTop strong{font-size:14.5px;color:var(--text-1)}
.updDate{font-size:12.5px;white-space:nowrap;font-variant-numeric:tabular-nums}
.updWhat{margin:3px 0 0;font-size:14px;line-height:1.55;color:var(--text-2)}
.updWhat a{color:var(--accent-text);font-weight:650}
.updWhy{margin:4px 0 0;font-size:13px;line-height:1.5}
.updatesAppChangelog{margin-top:30px;padding-top:22px;border-top:1px solid var(--line)}
.updSummary{cursor:pointer;font-size:17px;font-weight:800;letter-spacing:-.01em;color:var(--text-1)}
.updatesAppChangelog .updGroupCard{margin-top:12px}
#view-updates .wrap{padding-bottom:40px}

/* ---- Methodology sub-page: readable prose inside the panel ---- */
.resProse{padding:24px 26px}
.resProse p{font-size:14.5px;line-height:1.65;color:var(--text-2);max-width:820px;margin:0 0 12px}
.resProse p b{color:var(--text-1)}
.resProse ul{margin:0 0 12px;padding-left:20px}
.resProse li{font-size:14px;line-height:1.6;color:var(--text-2);margin:3px 0}
.resProse a{color:var(--accent-text);font-weight:650;text-decoration:none;cursor:pointer}
.resProse a:hover{text-decoration:underline}
.resProseH3{font-size:16px;font-weight:800;letter-spacing:-.01em;color:var(--text-1);
  margin:24px 0 10px;padding-top:20px;border-top:1px solid var(--line)}
.resProse .resProseH3:first-child{margin-top:0;padding-top:0;border-top:0}
.resSrcGroup{margin-top:14px}
.resSrcNote{margin:2px 0 6px;font-size:13.5px}
.resSrcList{margin:0;padding-left:20px}
.resMoreLink{margin:22px 0 0;padding-top:16px;border-top:1px solid var(--line);font-size:13.5px}
.resMoreLink a{font-weight:750}
@media(max-width:640px){.resProse{padding:18px 16px}}

/* Respect reduced-motion: no lift/slide on the landing cards. */
@media(prefers-reduced-motion:reduce){
  a.resCard,.resCardArrow,.resCrumb{transition:none}
  a.resCard:hover{transform:none}
  a.resCard:hover .resCardArrow{transform:none}
}
/* === end #6 DATA & RESOURCES REDESIGN === */

/* === RANGE DISPLAY === floor→typical value ranges (backlog: honest-range
   feature). Everything here supports the range treatment added in
   wallet.js / router.js / chat.js / explore.js / index.html: ranges must
   stay readable at 390/430/768/1440, the typical value stays visually
   primary, and no :root token is touched — the one grid widening below is
   a container-level custom-property override, not a token change. */

/* -- Wallet table (>=1000px): the rate + worth tracks each grew to hold a
   range ("1.30–2.01¢/pt", "$600–$1,206"); Balance hands back the pixels.
   Scoped to .wBucket so it wins over the :root --wCols without editing it. */
@media(min-width:1000px){
  .wBucket{ --wCols: minmax(180px,1fr) 200px 118px 116px 48px; }
}
/* Range strings are longer than the single figures they replaced — keep
   them on one line and let the row's right edge breathe. */
.hubRowVal{white-space:nowrap}
.wRate{white-space:nowrap}

/* -- Wallet totals bar: "$1,569–$2,805" in a 50%-width card at 390px needs
   a size step down; single figures shrink imperceptibly with them. The
   green Estimated-value card keeps its emphasis hierarchy. */
@media(max-width:640px){
  .hubTotalNum{font-size:17px}
  .hubTotalItem.hubTotalValue .hubTotalNum{font-size:19px}
}

/* -- Programs list (Explore): the floor tick on the value bar — a short
   contrasting notch at the program's conservative floor value, on the same
   scale as the typical-value fill. Fixed-value programs render no tick. */
.valBar{position:relative}
/* Full-height 2px notch INSIDE the bar (the bar is overflow:hidden, so the
   tick must not extend past its box) — dark enough to read against both
   the accent fill and the empty track. */
.valBarFloor{position:absolute;top:0;bottom:0;width:2px;
  background:var(--ink);opacity:.55;transform:translateX(-1px);pointer-events:none}

/* -- Receipt sheet: the floor→typical explainer paragraph. */
.receiptRange{margin:10px 0 0;font-size:13.5px;line-height:1.5;color:var(--ink2);
  border-left:3px solid var(--line-strong);padding-left:10px}
.receiptRange b{color:var(--ink)}

/* -- Homepage hero panel: the range total is ~5 characters longer than the
   old single figure — step the display size down a notch so it never
   crowds the panel edge at 390px, and style the one-line honesty key. */
.mkcTotalNum--range{font-size:31px;letter-spacing:-.01em;white-space:nowrap}
@media(max-width:430px){ .mkcTotalNum--range{font-size:27px} }
.mkcRangeNote{margin-top:10px;font-size:12px;line-height:1.45;color:var(--text-3)}

/* -- Step-1 exemplar wallet card: per-row ranges + the explainer foot. The
   .big range reuses the card's own display font at a step down. */
.mkWalletCard .big{font-size:26px;white-space:nowrap}
@media(max-width:430px){ .mkWalletCard .big{font-size:22px} }
.mkRangeFoot{margin:10px 0 2px;font-size:12px;line-height:1.5;color:var(--text-3);
  border-top:1px solid var(--line);padding-top:9px}

/* -- Signed-in home dashboard: one-line range key under the wallet rows. */
.shRangeNote{margin:8px 0 0;font-size:12px}

/* === end RANGE DISPLAY === */

/* ============ PLAID BANK AUTO-SYNC — promo + wallet placement + onboarding ============ */

/* --- Homepage promo (#mkPlaidPromo, replaces half the old #mkSoon) --- */
.plaidPromoGrid{display:grid;grid-template-columns:1.02fr .98fr;gap:44px;margin-top:34px;align-items:start}
.plaidPromoFeat{display:flex;gap:14px;padding:15px 0;border-bottom:1px solid var(--line)}
.plaidPromoFeat:first-child{padding-top:2px}
.plaidPromoFeat--last{border-bottom:0}
.plaidPromoFeatIc{width:38px;height:38px;border-radius:11px;background:var(--accent-soft);color:var(--accent-text);display:grid;place-items:center;flex:none}
.plaidPromoFeatIc svg{width:18px;height:18px}
.plaidPromoFeat h3{font-size:15.5px;font-weight:750;margin:0;letter-spacing:-.01em;color:var(--text-1)}
.plaidPromoFeat p{font-size:13.5px;color:var(--text-2);margin:4px 0 0;line-height:1.55;max-width:440px}
.plaidPromoSyncCard{background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:20px 22px 16px}
.plaidPromoSyncLabel{font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--text-3);display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.plaidPromoTag{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;font-weight:700;border-radius:999px;padding:3px 9px;letter-spacing:0;text-transform:none;background:var(--accent-soft);color:var(--accent-text)}
.plaidPromoSyncRows{margin-top:14px}
.plaidPromoSyncRow{display:grid;grid-template-columns:128px 1fr 58px;gap:12px;align-items:center;padding:8px 0;font-size:13.5px;font-weight:600}
.plaidPromoTrack{height:8px;border-radius:99px;background:var(--bg-deep);overflow:hidden}
.plaidPromoTrack i{display:block;height:100%;border-radius:99px;background:var(--accent)}
.plaidPromoV{text-align:right;font-weight:750}
.plaidPromoSyncFoot{margin-top:12px;border-top:1px solid var(--line);padding-top:11px;font-size:12px;color:var(--text-3);line-height:1.55}
.plaidPromoSyncFoot b{color:var(--text-1)}
.plaidPromoPromise{display:grid;grid-template-columns:repeat(3,1fr);gap:0;border:1px solid var(--line);border-radius:14px;overflow:hidden;margin-top:40px;background:var(--bg-surface)}
.plaidPromoPromiseCol{padding:18px 20px;border-right:1px solid var(--line)}
.plaidPromoPromiseCol:last-child{border-right:0}
.plaidPromoPromiseCol h3{display:flex;align-items:center;gap:7px;margin:0 0 8px;font-size:12px;font-weight:800;letter-spacing:.07em;text-transform:uppercase}
.plaidPromoDot{width:9px;height:9px;border-radius:999px;flex:none;display:inline-block}
.plaidPromoPromiseCol--login h3{color:var(--danger-ink)} .plaidPromoPromiseCol--login .plaidPromoDot{background:var(--danger-ink)}
.plaidPromoPromiseCol--txns h3{color:var(--good-text)} .plaidPromoPromiseCol--txns .plaidPromoDot{background:var(--good-text)}
.plaidPromoPromiseCol--opt h3{color:var(--accent-text)} .plaidPromoPromiseCol--opt .plaidPromoDot{background:var(--accent)}
.plaidPromoPromiseCol p{font-size:13px;color:var(--text-2);margin:0;line-height:1.55}
.plaidPromoPromiseCol p b{color:var(--text-1);font-weight:650}
@media(max-width:900px){
  .plaidPromoGrid{grid-template-columns:1fr;gap:26px}
  .plaidPromoPromise{grid-template-columns:1fr}
  .plaidPromoPromiseCol{border-right:0;border-bottom:1px solid var(--line)}
  .plaidPromoPromiseCol:last-child{border-bottom:0}
}
@media(max-width:480px){
  .plaidPromoSyncRow{grid-template-columns:104px 1fr 52px;gap:8px;font-size:12.5px}
}

/* --- Wallet slot: State A connect card + State B connected/disconnect --- */
.plaidWalletSlot:empty{display:none}
.bankSyncCard{background:linear-gradient(135deg,var(--accent-soft),var(--bg-surface));border:1px solid var(--line-strong);border-radius:var(--radius);box-shadow:var(--shadow);padding:18px 20px;margin:12px 0 4px;display:grid;grid-template-columns:auto 1fr;gap:0 16px;align-items:start}
.bankSyncCard .bsIc{width:48px;height:48px;border-radius:14px;background:var(--bg-surface);border:1px solid var(--line);color:var(--accent-text);display:grid;place-items:center}
.bankSyncCard .bsIc svg{width:22px;height:22px}
.bsTitle{font-size:16px;font-weight:750;letter-spacing:-.01em;margin:0}
.bsBody{font-size:13.5px;color:var(--text-2);margin:5px 0 0;line-height:1.55;max-width:540px}
.bsReassure{display:inline-flex;align-items:center;gap:7px;font-size:12px;font-weight:650;color:var(--accent-text);background:var(--bg-surface);border:1px solid var(--line);border-radius:999px;padding:5px 12px;margin:11px 0 0;max-width:100%}
.bsReassure svg{width:13px;height:13px;flex:none}
.bsActions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:14px}
.plaidConnected{background:var(--good-soft,rgba(15,118,110,.10));border:1px solid rgba(15,118,110,.22);border-radius:14px;padding:14px 16px;margin:12px 0 4px}
.plaidConnTop{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}
.plaidConnHead{display:flex;align-items:center;gap:11px}
.plaidConnMain{display:flex;align-items:center;gap:11px}
.plaidConnDot{width:9px;height:9px;border-radius:999px;background:var(--good-text,#0f766e);flex:none;box-shadow:0 0 0 4px var(--good-soft,rgba(15,118,110,.12))}
.plaidConnT{font-size:14.5px;font-weight:750;color:var(--good-text,#0f766e)}
.plaidConnSub{font-size:12.5px;color:var(--text-2);margin-top:8px;max-width:560px}
.plaidConnActs{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:10px}
/* which banks are connected — named chips */
.plaidBankChips{display:flex;flex-wrap:wrap;gap:7px;margin-top:10px}
.plaidBankChip{display:inline-flex;align-items:center;gap:7px;font-size:13px;font-weight:650;color:var(--text-1);background:var(--bg-surface);border:1px solid var(--line-strong,rgba(15,118,110,.3));border-radius:999px;padding:5px 12px 5px 10px}
.plaidBankChipDot{width:7px;height:7px;border-radius:999px;background:var(--good-text,#0f766e);flex:none}
.plaidBankChip--generic{color:var(--text-2);font-weight:600}
/* "Secured by Plaid" brand pill */
.plaidBrandPill{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:700;letter-spacing:.01em;color:var(--text-2);white-space:nowrap}
.plaidBrandLogo{height:14px;width:auto;display:inline-block;vertical-align:middle;color:var(--text-1)}
.plaidBrandPill .plaidBrandLogo{height:13px;color:inherit}
.budgetBankBadgePlaid .plaidBrandLogo{height:12px;color:var(--pp-accent,#2f6df6)}
.plaidDiscBtn{border:none;background:none;color:var(--text-3);font:600 13px var(--font);cursor:pointer;padding:7px 10px;border-radius:8px}
.plaidDiscBtn:hover{color:var(--danger-ink,#b3261e);background:var(--danger-soft,rgba(179,38,30,.08))}
@media(max-width:560px){
  .bankSyncCard{grid-template-columns:1fr;gap:2px}
  .bankSyncCard .bsIc{display:none}
  .bsActions .btn{width:100%;justify-content:center}
}

/* --- Onboarding budget step: three ways to add spend --- */
.obWaysRow{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin:0 0 16px}
.obWayCard{position:relative;background:var(--bg-surface,#fff);border:1px solid var(--line,rgba(30,40,51,.10));border-radius:var(--radius,16px);box-shadow:var(--shadow,0 1px 2px rgba(20,32,43,.06));padding:16px 16px 14px;display:flex;flex-direction:column}
.obWayCard--current{border-color:var(--accent,#c79a3b);box-shadow:0 0 0 3px var(--accent-soft,rgba(199,154,59,.14)),var(--shadow,0 1px 2px rgba(20,32,43,.06))}
.obWayHead{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.obWayTitle{font-size:14.5px;font-weight:750;margin:0;letter-spacing:-.01em}
.obWayTag{font-size:9.5px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;border-radius:999px;padding:2px 7px;white-space:nowrap}
.obWayTag--fast{background:var(--accent-soft,rgba(199,154,59,.14));color:var(--accent-text,#7d5b1d)}
.obWayTag--opt{background:var(--bg-surface-2,#f6f2e8);color:var(--text-3,#5c6a77);border:1px solid var(--line,rgba(30,40,51,.10))}
.obWayText{font-size:12.5px;color:var(--text-2,#46525e);margin:6px 0 0;line-height:1.5}
.obWayDoing{font-size:11.5px;font-weight:700;color:var(--accent-text,#7d5b1d);margin:10px 0 0}
.obWayBtnRow{margin-top:10px}
.obWayMicro{margin:8px 0 0;font-size:11px;color:var(--text-4,#75828f);line-height:1.5}
.obWayMicro b{color:var(--text-3,#5c6a77)}
@media(max-width:480px){.obWaysRow{grid-template-columns:1fr}}
/* Connect-first reorder — issuer identify chips (Cards sub-step 1a) */
.obIssuerGrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin:2px 0 4px}
.obIssuerChip{display:flex;align-items:center;justify-content:space-between;gap:8px;text-align:left;border:1.5px solid var(--line-strong,rgba(30,40,51,.18));background:var(--bg-surface,#fff);border-radius:12px;padding:14px 15px;font:650 14.5px var(--font);color:var(--ink,var(--text-1));cursor:pointer;min-height:52px;transition:border-color .12s,background .12s,box-shadow .12s}
.obIssuerChip:hover{background:var(--bg-surface-2,#f6f4ee)}
.obIssuerChip.sel{border-color:var(--accent,#c79a3b);background:var(--accent-soft,rgba(199,154,59,.10));box-shadow:0 0 0 1px var(--accent,#c79a3b)}
.obIssuerTick{color:var(--good-text,#1f8a6a);font-weight:800;flex:none}
@media(max-width:480px){.obIssuerGrid{grid-template-columns:1fr 1fr}}
/* shared card monogram (ppCardLogo fallback when no bank/card logo) */
.ppCardMark{display:inline-grid;place-items:center;width:26px;height:26px;border-radius:7px;color:#fff;font-size:10px;font-weight:800;letter-spacing:.02em;flex:none}
.ppCardMark.lg{width:34px;height:34px;font-size:12px;border-radius:9px}
/* onboarding_v2 sync/loading screen */
.obSyncWrap{display:flex;align-items:center;justify-content:center;min-height:min(70vh,560px);text-align:center}
.obSyncCard{max-width:440px;margin:0 auto;display:flex;flex-direction:column;align-items:center}
.obSyncCard h1{margin:18px 0 6px}
.obSyncSpin{width:52px;height:52px;border-radius:50%;border:4px solid var(--line,rgba(30,40,51,.12));border-top-color:var(--accent,#c79a3b);animation:obSpin .9s linear infinite}
@keyframes obSpin{to{transform:rotate(360deg)}}
@media(prefers-reduced-motion:reduce){.obSyncSpin{animation-duration:2.4s}}
.obSyncList{list-style:none;margin:22px 0 0;padding:0;text-align:left;display:flex;flex-direction:column;gap:11px;width:100%;max-width:340px}
.obSyncList li{position:relative;padding-left:28px;font-size:14px;color:var(--text-2,#5c6a77);line-height:1.4}
.obSyncList li.done{color:var(--text-1,#1e2833)}
.obSyncList li.done::before{content:"";position:absolute;left:2px;top:2px;width:18px;height:18px;border-radius:50%;background:var(--good,#1f8a6a)}
.obSyncList li.done::after{content:"";position:absolute;left:7.5px;top:6px;width:5px;height:9px;border:solid #fff;border-width:0 2px 2px 0;transform:rotate(45deg)}
.obSyncList li.doing{color:var(--text-1,#1e2833);font-weight:650}
.obSyncList li.doing::before{content:"";position:absolute;left:2px;top:2px;width:18px;height:18px;border-radius:50%;border:2px solid var(--accent,#c79a3b);border-top-color:transparent;animation:obSpin .9s linear infinite}
.obSyncDots{display:inline-flex;gap:3px;margin-left:6px;vertical-align:middle}
.obSyncDots i{width:5px;height:5px;border-radius:50%;background:var(--accent,#c79a3b);animation:obBlink 1.2s infinite both}
.obSyncDots i:nth-child(2){animation-delay:.2s}.obSyncDots i:nth-child(3){animation-delay:.4s}
@keyframes obBlink{0%,80%,100%{opacity:.25}40%{opacity:1}}
/* advise: single Connect card + de-emphasized "add by hand" link beneath it */
.obWaysRow--solo{grid-template-columns:1fr;max-width:520px}
.obWaySolo{align-items:stretch}
.obWayHandLink{display:inline-block;margin:12px 0 0;background:none;border:0;padding:4px 2px;font:650 13px var(--font);color:#b23b3b;cursor:pointer;text-decoration:underline;text-underline-offset:2px;align-self:flex-start}
.obWayHandLink:hover{color:#8f2e2e}
/* advise: description-only Connect card, same width as the trust box below it */
.obConnectCard{margin:0 0 12px}
.obConnectCard .obWayHead{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:0 0 8px}
.obConnectPill{margin-left:auto}
/* green primary (Connect with Plaid) — owner: green, rightmost in the dock */
.btn.btnGreen{background:var(--good,#1f8a6a);color:#fff;box-shadow:0 2px 12px rgba(31,138,106,.32)}
.btn.btnGreen:hover{filter:brightness(1.05)}
.btn[aria-busy="true"]{opacity:.9;cursor:default}
.obBtnSpin{display:inline-block;width:14px;height:14px;border-radius:50%;border:2px solid rgba(255,255,255,.45);border-top-color:#fff;animation:obSpin .8s linear infinite;vertical-align:-2px;margin-right:2px}
/* #1: breathing room under the "Back to Home" pill during onboarding */
body.ppOnboarding .walletBackBar{margin:0 0 14px}
/* intro progress dots */
.obIntroDots{display:flex;gap:7px;justify-content:center;margin:0 0 22px}
.obIntroDot{width:8px;height:8px;border-radius:999px;background:var(--line-strong,rgba(30,40,51,.2))}
.obIntroDot.on{background:var(--accent,#c79a3b);width:22px}
.obIntroDot.done{background:var(--accent,#c79a3b);opacity:.5}
.obValueList--card{margin:0;padding:0 0 0 2px;list-style:none;display:flex;flex-direction:column;gap:12px}
.obValueList--card li{position:relative;padding-left:26px;font-size:15px;color:var(--text-1,var(--ink))}
.obValueList--card li::before{content:"✓";position:absolute;left:0;top:0;color:var(--good-text,#1f8a6a);font-weight:800}
/* connect hub */
.obHubRow{display:flex;align-items:center;gap:12px;padding:12px 4px;border-bottom:1px solid var(--line)}
.obHubRow:last-of-type{border-bottom:0}
.obHubNm{flex:1;font-size:15px;font-weight:700;color:var(--ink,var(--text-1))}
.obHubTag{font-size:12px;font-weight:750;color:var(--good-text);background:var(--good-soft,var(--bg-surface-2));border:1px solid var(--good-line,var(--good));border-radius:999px;padding:3px 11px;white-space:nowrap}
.obHubCover{font-size:12.5px;color:var(--text-3);margin:10px 2px}
.obHubCover b{color:var(--text-1)}
.obHubActs{margin-top:12px}
/* connected banner: one card per row (logo, name, ·last4) */
.obConnCardList{display:flex;flex-direction:column;gap:8px;margin:12px 0 0}
.obConnCardRow{display:flex;align-items:center;gap:10px;font-size:14px;font-weight:700;color:var(--ink,var(--text-1))}
.obConnMask{color:var(--text-4);font-weight:650}
/* airline/hotel balance rows: input + per-program "Not now" */
.obBalCell{display:flex;align-items:center;gap:8px}
.obBalNotNow{background:none;border:0;padding:2px;font:650 12.5px var(--font);color:var(--text-4);cursor:pointer;text-decoration:underline;text-underline-offset:2px;white-space:nowrap}
.obBalNotNow:hover{color:var(--text-2)}
.obBalDeferred{font-size:12.5px;color:var(--text-3);white-space:nowrap}
/* Done summary: sections with per-item rows (logos, sync badges, expandable) */
.obDoneSec{border-top:1px solid var(--line);padding:12px 2px}
.obDoneSec:first-child{border-top:0}
.obDoneSecHd{display:flex;align-items:center;gap:10px;margin:0 0 8px}
.obDoneSecHd b{font-size:13px;font-weight:800;letter-spacing:.02em}
.obDoneCt{margin-left:auto;font-size:12px;font-weight:650;color:var(--text-3)}
.obDoneItem{display:flex;align-items:center;gap:10px;padding:6px 0;font-size:13.5px}
.obDoneNm{flex:1;font-weight:700;color:var(--ink,var(--text-1));min-width:0}
.obDoneMask{color:var(--text-4);font-weight:600}
.obDoneBal{font-weight:750;color:var(--text-2);white-space:nowrap}
.obDoneSync{font-size:11px;font-weight:750;border-radius:999px;padding:2px 9px;white-space:nowrap}
.obDoneSync.on{color:var(--good-text);background:var(--good-soft,var(--bg-surface-2));border:1px solid var(--good-line,var(--good))}
.obDoneSync.off{color:var(--text-4);background:var(--bg-surface-2);border:1px dashed var(--line-strong)}
.obDoneEmpty{font-size:12.5px;color:var(--text-4);margin:2px 0 0}
.obDoneDetails summary{cursor:pointer;font-size:12.5px;font-weight:700;color:var(--blue,var(--accent-text));padding:4px 0;list-style:none}
.obDoneDetails summary::-webkit-details-marker{display:none}
.obDoneSec--total .obDoneCt{color:var(--good-text)}
/* ============ end PLAID BANK AUTO-SYNC ============ */

/* Budget "synced from your bank" badge (Plaid) — shown on the budget section
   when a bank is connected, so the numbers visibly read as bank-sourced. */
.budgetBankBadge{display:flex;align-items:flex-start;gap:10px;margin:0 0 14px;padding:11px 13px;
  border:1px solid var(--pp-accent,#2f6df6);border-radius:12px;
  background:linear-gradient(180deg,rgba(47,109,246,.09),rgba(47,109,246,.045));}
.budgetBankBadgeIc{flex:0 0 auto;width:30px;height:30px;display:grid;place-items:center;border-radius:8px;
  background:var(--pp-accent,#2f6df6);color:#fff;}
.budgetBankBadgeIc svg{width:17px;height:17px;}
.budgetBankBadgeTx{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1;}
.budgetBankBadgeTx b{font-size:13.5px;font-weight:650;color:var(--pp-ink,#0f1220);}
.budgetBankBadgeTx span{font-size:12px;line-height:1.45;color:var(--pp-muted,#5b6472);}
.budgetBankBadgePlaid{flex:0 0 auto;align-self:center;font-size:11px;font-weight:650;letter-spacing:.02em;
  color:var(--pp-accent,#2f6df6);white-space:nowrap;}
@media(max-width:520px){.budgetBankBadgePlaid{display:none;}}

/* Plaid prominence + logo rows (wallet connect card, onboarding, homepage banner) */
.bsTitleRow{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:2px}
.bankSyncCard--prominent{border-width:1.5px;border-color:var(--accent,#2f6df6);box-shadow:0 3px 16px rgba(47,109,246,.12)}
.mkPlaidKickRow{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-bottom:6px}
.obWayBtnRow{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.obWayBtnRow .plaidBrandPill{margin-left:2px}

/* Signed-in dashboard Plaid banner (#dashPlaidSlot) */
.dashPlaidBand{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin:14px 0;padding:15px 18px;
  border:1.5px solid var(--accent,#2f6df6);border-radius:14px;
  background:linear-gradient(180deg,rgba(47,109,246,.08),rgba(47,109,246,.03))}
.dashPlaidBand--on{border-color:rgba(15,118,110,.35);background:linear-gradient(180deg,rgba(15,118,110,.08),rgba(15,118,110,.03))}
.dashPlaidIc{flex:0 0 auto;width:40px;height:40px;display:grid;place-items:center;border-radius:11px;background:var(--accent,#2f6df6);color:#fff}
.dashPlaidBand--on .dashPlaidIc{background:var(--good-text,#0f766e)}
.dashPlaidIc svg{width:20px;height:20px}
.dashPlaidTx{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1 1 260px}
.dashPlaidTx b{font-size:14.5px;font-weight:650;color:var(--text-1)}
.dashPlaidTx span{font-size:12.5px;line-height:1.5;color:var(--text-2)}
.dashPlaidActs{display:flex;align-items:center;gap:12px;flex-wrap:wrap;flex:0 0 auto}
@media(max-width:560px){ .dashPlaidActs{width:100%} .dashPlaidActs .btn{flex:1} }

/* ===== onboarding polish (2026-07-25) — Fable: intro checklist, value strip,
   trust line, and elevated .obWay* two-ways cards (matches the approved
   onboarding-redesign mockup). Scoped to onboarding classes only; the .obWay*
   rules below EXTEND the base rules at "Onboarding budget step" above (same
   specificity, later in the file, so they win where they overlap). The
   .obWay* pattern is shared with wallet.js's chatBudgetWaysHTML — the
   elevation applies there too, deliberately, so the pattern stays one
   pattern. ===== */

/* --- Intro: "What we'll set up" numbered checklist --- */
.obSetupList{display:flex;flex-direction:column;gap:14px;margin-top:12px}
.obSetupItem{display:flex;gap:14px;align-items:flex-start}
.obSetupN{flex:none;width:30px;height:30px;border-radius:50%;background:var(--bg-surface-2);border:1px solid var(--line-strong);display:grid;place-items:center;font-family:var(--font-display);font-weight:700;font-size:14.5px;color:var(--text-1);margin-top:1px}
.obSetupTx{min-width:0}
.obSetupTx b{display:block;font-size:15px;font-weight:700;letter-spacing:-.005em;color:var(--text-1)}
.obSetupTx span{display:block;font-size:13px;color:var(--text-3);line-height:1.5;margin-top:1px}
/* the optional item (Goals) is honest about being optional: dashed marker, muted title */
.obSetupItem--opt .obSetupN{background:transparent;border-style:dashed;color:var(--text-3);font-size:16px;font-weight:600}
.obSetupItem--opt .obSetupTx b{color:var(--text-3);font-weight:650}

/* --- Intro: "What you get in return" value strip --- */
.obValueStrip{background:var(--bg-surface-2);border:1px solid var(--line);border-radius:var(--radius);padding:16px 18px;margin-top:14px}
.obValueHead{font-size:11px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--accent-text);margin:0 0 9px}
.obValueList{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}
.obValueList li{position:relative;padding-left:17px;font-size:13.5px;line-height:1.5;color:var(--text-2)}
.obValueList li::before{content:"";position:absolute;left:0;top:.5em;width:7px;height:7px;border-radius:50%;background:var(--accent)}

/* --- Intro: trust / privacy line (replaces the inline-styled obSub+svg) --- */
.obTrustLine{display:flex;gap:13px;align-items:flex-start;background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:15px 17px;margin-top:14px}
.obTrustLock{flex:none;width:34px;height:34px;border-radius:10px;background:var(--good-soft,rgba(15,118,110,.10));color:var(--good-text);display:grid;place-items:center;margin-top:1px}
.obTrustLock svg{width:16px;height:16px}
.obTrustTx{min-width:0}
.obTrustTx b{display:block;font-size:13.5px;font-weight:750;color:var(--text-1);margin-bottom:2px}
.obTrustTx span{display:block;font-size:12.5px;color:var(--text-3);line-height:1.55}

/* --- Two-ways cards: elevation pass over the base .obWay* rules --- */
.obWaysRow{gap:14px;margin:0 0 18px}
.obWayCard{padding:18px 18px 16px;transition:transform .16s ease,box-shadow .16s ease}
@media(hover:hover){
  .obWayCard:hover{transform:translateY(-1px);box-shadow:0 4px 14px rgba(20,32,43,.10),0 14px 34px rgba(20,32,43,.08)}
  .obWayCard--current:hover{box-shadow:0 0 0 3px var(--accent-soft),0 4px 14px rgba(20,32,43,.10),0 14px 34px rgba(20,32,43,.08)}
}
@media(prefers-reduced-motion:reduce){.obWayCard{transition:none}.obWayCard:hover{transform:none}}
/* the recommended path: gold ring (from base rule) + a soft gold wash at the top */
.obWayCard--current{background:linear-gradient(180deg,var(--accent-soft),rgba(199,154,59,0) 46%),var(--bg-surface)}
.obWayHead{gap:8px}
.obWayTitle{font-family:var(--font-display);font-size:17px;font-weight:700;letter-spacing:-.01em}
.obWayTag{font-size:10px;padding:3px 9px;letter-spacing:.08em}
/* "Fastest" goes solid gold — the one loud element on the card */
.obWayTag--fast{background:var(--accent);color:var(--accent-on)}
.obWayText{font-size:13px;margin-top:7px}
.obWayBtnRow{margin-top:13px}
.obWayMicro{font-size:11.5px;margin-top:10px}
/* the "you're on this path" line sits at the card's bottom edge so side-by-side
   cards align (cards are flex columns; auto margin absorbs the slack) */
.obWayDoing{margin:auto 0 0;padding-top:12px}

/* --- Monthly Spending, already-connected state: green "done" treatment --- */
.obWayCard--synced{border-color:var(--good);box-shadow:0 0 0 3px var(--good-soft,rgba(15,118,110,.10)),var(--shadow);background:linear-gradient(180deg,var(--good-soft,rgba(15,118,110,.10)),rgba(15,118,110,0) 46%),var(--bg-surface)}
.obWayTag--done{background:var(--good);color:#fff}
/* ===== end onboarding polish (2026-07-25) ===== */

/* ===== onboarding intro security block (2026-07-25) ===== */
.obSecCard{background:var(--good-soft,#eaf6f1);border-color:var(--good-line,rgba(31,138,106,.30))}
.obSecHead{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.obSecList{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:9px}
.obSecList li{position:relative;padding-left:20px;font-size:14px;color:var(--text-2);line-height:1.5}
.obSecList li:before{content:"";position:absolute;left:2px;top:8px;width:7px;height:7px;border-radius:999px;background:var(--good,#1f8a6a)}

/* ===== onboarding balance nudge (2026-07-26) ===== */
.btn.obBtnEqual{background:var(--bg-surface);border:1.6px solid var(--brand-ink,var(--ink,#1f2d4d));color:var(--brand-ink,var(--ink,#1f2d4d))}
.btn.obBtnEqual:hover{background:var(--bg-surface-2)}
.obNeedsBal{outline:2px solid var(--warn-line,#e6b34d);outline-offset:3px;border-radius:12px;background:var(--warn-soft,#fdf3dd)}
.obBalInNeeds{border-color:var(--warn-ink,#b7791f)!important;box-shadow:0 0 0 3px var(--warn-soft,rgba(230,179,77,.35))}
.obNeedsTag{color:var(--warn-ink,#b7791f);font-weight:700}

/* ===== 2026-07-25 deferred-UI wave =====
   Four contracts styled below, markup owned by wallet.js/explore.js/onboarding.js
   (other agents, running concurrently). Additive only -- nothing above this
   block was touched. Tokens reused throughout: --accent (focus rings, matches
   .matrixSortBtn/.receipt-trig/.pillChip/.adminChipX/.resCrumb etc.),
   --danger-ink/--danger-bg/--danger-border (matches .authErrorNote/.pricingError),
   --text-1/--text-2/--text-3/--text-4, --line, --radius, --shadow, --bg-surface. */

/* --- Contract 1: external links (wallet.js row links, explore.js) ---
   Anchor inherits the row's own color/weight (no "big blue link" treatment --
   base a{color:var(--brand-ink)} already isn't blue, but rows use several
   different text tokens, so `inherit` is the only way to always match).
   The ↗ glyph is muted via opacity (not a text-4/text-3 token pick) so it
   auto-matches whatever ink color the surrounding row happens to use, in
   both themes, without a new per-context override. */
.ppExtLink{color:inherit;font-weight:inherit;text-decoration:none}
.ppExtLink:hover{text-decoration:underline}
.ppExtLink:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:4px}
.ppExtIco{display:inline-block;margin-left:.3em;font-size:.8em;line-height:1;
  opacity:.55;transform:translateY(-.12em);white-space:nowrap;transition:opacity .15s ease}
.ppExtLink:hover .ppExtIco,.ppExtLink:focus-visible .ppExtIco{opacity:1}
@media(prefers-reduced-motion:reduce){.ppExtIco{transition:none}}

/* --- Contract 2: collapsible expiration banner (wallet.js, wraps .expWarn) ---
   .expToggle reuses .expWarn's own literal tokens (see "Expiration-warning
   inline row" block above -- #a05a1e / rgba(199,120,59,.14) / #e2a24d dark,
   8px radius, 12.5px type) so the collapsed summary reads as the exact same
   one-line warning it replaces, just full-width and clickable. .expPanel uses
   a plain display:none/block toggle rather than the grid-rows trick: the
   contract requires content removed from the a11y tree when closed, and
   display:none is the only one of the two options that guarantees that
   without pairing it with inert/aria-hidden bookkeeping this pass doesn't
   own. */
.expCollapse{margin:0}
.expToggle{display:flex;align-items:center;justify-content:space-between;gap:8px;
  width:100%;min-height:44px;margin:0;border:0;border-radius:8px;cursor:pointer;
  font:inherit;font-size:12.5px;text-align:left;color:#a05a1e;background:rgba(199,120,59,.14);
  padding:6px 12px}
.expToggle:hover{filter:brightness(.97)}
.expToggle:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
[data-theme="dark"] .expToggle{color:#e2a24d}
.expToggleIco{flex:none;display:inline-block;font-size:11px;transition:transform .18s ease}
.expToggle[aria-expanded="true"] .expToggleIco{transform:rotate(180deg)}
@media(prefers-reduced-motion:reduce){.expToggleIco{transition:none}}
.expPanel{display:none}
.expPanel.is-open{display:block;margin-top:6px}

/* --- Contract 3: onboarding policy-consent step (onboarding.js, flag-gated) ---
   Sits inside the existing .obCard system (see .obSecCard above for the same
   pattern -- a modifier/child block nested in a base .obCard). Error copy
   matches .authErrorNote's idiom exactly (--danger-ink/--danger-bg/--danger-border,
   10px radius). .obConsentErr intentionally sets no `display` property --
   it's a <p>, block by default -- so the [hidden] attribute the driver
   toggles it with is never fought over specificity. */
.obConsent{margin-top:14px}
.obConsentRow{display:flex;align-items:flex-start;gap:12px;min-height:44px;
  padding:5px 2px;cursor:pointer}
.obConsentCheck{flex:none;width:18px;height:18px;margin-top:3px;accent-color:var(--accent);cursor:pointer}
.obConsentText{font-size:13.5px;color:var(--text-2);line-height:1.5}
.obConsentText a{color:var(--accent-text);font-weight:700;text-decoration:underline;text-underline-offset:2px}
.obConsentText a:hover{text-decoration:none}
.obConsentText a:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:3px}
.obConsentErr{margin:8px 0 0;padding:9px 12px;border-radius:10px;font-size:12.5px;font-weight:600;
  color:var(--danger-ink);background:var(--danger-bg);border:1px solid var(--danger-border)}

/* --- Contract 4: onboarding intro, desktop blank-space fix (onboarding.js obIntroHTML) ---
   .obMain is ALREADY capped wider than its 680px default during onboarding --
   body.ppOnboarding .obMain{max-width:760px} above fires unconditionally
   whenever the onboarding flow is active (router.js toggles ppOnboarding for
   the whole flow, not just some steps), and that 760px is the site's
   deliberate onboarding reading-width ceiling ("roomier decision column").
   So the "narrow column" half of the complaint is already at the widest the
   design allows -- widening further would mean inventing a new cap, which
   the brief rules out. The real fix is the other half: "stranded near the
   top of a tall viewport." That's a vertical problem, and the codebase
   already has the exact idiom for it in .obSyncWrap just above (flex column
   + min-height:min(Xvh,Ypx) to pull short content toward the vertical
   center of a tall viewport) -- reused here at a taller ceiling since the
   intro step (dots + kicker + h1 + sub + card w/ setup list + value strip +
   trust line) carries more content than the sync screen. min-width:1081px
   matches the file's existing desktop-only convention for this exact kind
   of composition fix (see .mkHeroCopy centering right above .mkHero, same
   breakpoint, same "half-empty" framing). Scoped to .obIntro only, so
   mobile/tablet and every other onboarding step are untouched; when content
   already exceeds the min-height (e.g. a mid-sub-step with more copy),
   justify-content:center has no effect since there's no slack to distribute,
   so this can't compress or clip anything. */
@media(min-width:1081px){
  .obIntro{display:flex;flex-direction:column;justify-content:center;min-height:min(70vh,620px)}
}
/* --- Policy-consent modal (plaid.js consentAsk; flag require_policy_consent,
   default OFF). Reuses the .obConsent* contract above for the checkbox row so
   the tick looks identical whether it appears here or inline in onboarding.
   Only rendered at the single chokepoint before Plaid Link opens. --- */
.ppConsentBack{position:fixed;inset:0;z-index:80;display:flex;align-items:center;justify-content:center;
  padding:20px;background:rgba(10,14,26,.55);backdrop-filter:blur(3px)}
.ppConsentCard{width:100%;max-width:460px;max-height:calc(100vh - 40px);overflow:auto;
  background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--radius);
  box-shadow:var(--shadow);padding:22px 22px 18px}
.ppConsentTitle{margin:0 0 12px;font-size:19px;line-height:1.25}
.ppConsentList{margin:0 0 4px}
.ppConsentActs{display:flex;justify-content:flex-end;gap:10px;margin-top:18px}
.obConsent.err .obConsentCheck{outline:2px solid var(--danger-border);outline-offset:2px}
@media(max-width:480px){
  .ppConsentActs{flex-direction:column-reverse}
  .ppConsentActs .btn{width:100%}
}
/* ===== end 2026-07-25 deferred-UI wave ===== */

/* Annual-fee scorecard (Strategy → Activate). Backlog item 125: a fee card's
   break-even and its stated credits, so the fee stops being an unexplained
   number. Row borders rather than cards, because the Activate tab already has
   a card container and nesting a second one reads as a modal. */
.stratFeeGroup{display:flex;flex-direction:column;gap:14px}
.stratFeeRow{padding:12px 0;border-top:1px solid var(--line)}
.stratFeeRow:first-child{border-top:none;padding-top:0}
.stratFeeHead{display:flex;align-items:center;gap:8px;margin-bottom:6px}
.stratFeeBody{font-size:13.5px;line-height:1.45}
.stratFeeCredits{margin-top:6px}
/* --ink2, not --muted: the sheet defines --line and --ink2, and the existing
   `.muted` class resolves to var(--ink2). --muted is used in a few places but
   never defined, so it silently falls back to inherited colour. */
.stratFeeFree{margin:10px 0 0;font-size:13px;color:var(--ink2)}

/* === Mobile topbar: the hamburger must never be the thing that overflows ===
   Reported by Jeremy 2026-07-26 from his own phone (iPhone 16 Pro Max, 440px):
   no menu button visible at all. Measured cause, not guessed: every child of
   .topnavIn carried flex-shrink:0, so nothing could yield. Signed in AND admin,
   the row totalled 485px against a 440px viewport --
     logo 92 + spacer 8 + points pill 99 + theme 68 + account 127 + menu 44
   -- and the menu button, being last, landed at x=441. One pixel out of view is
   the same as absent, and the hamburger is the ONLY route to navigation at this
   width, so being pushed off strands the user.

   It only reproduced for Jeremy because the 127px "Admin dashboard" link does
   not render for anyone else. A normal signed-in account fits, which is why
   every previous mobile pass at this width passed.

   Two independent guards, because either alone would leave the failure one
   topbar addition away:
     1. the menu button is ordered last and cannot shrink, and everything that
        varies with account state CAN shrink and truncate
     2. the points pill is dropped below 560px, where the page it sits above
        already prints the same total in its headline */
@media(max-width:899px){
  .menuBtn{flex:0 0 auto;order:99}
  /* `.topnav .sidebarAccount` (two classes) already sets flex:0 0 auto, and it
     beats a single-class rule no matter which comes last in the file. The first
     version of this fix used `.sidebarAccount` and silently did nothing --
     measured: computed flex-shrink stayed 0 and a long account label still
     pushed the button to x=490 in a 390px viewport. Match the specificity. */
  .topnav .sidebarAccount,.topnav .topnavAccount,.topnav #ptsTopBar{min-width:0;flex:0 1 auto;overflow:hidden}
  .topnav .sidebarAccount a,.topnav .sidebarAccount button,
  .topnav .topnavAccount a,.topnav .topnavAccount button{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;display:block}
}
/* The rule directly above is what makes the signed-out auth control TRUNCATE
   rather than push the hamburger off-screen -- correct, but on a phone it
   printed "Sign in / Crea…" (measured on /ask/ at 390x844). Ellipsis is the
   right overflow behaviour and the wrong final state for the site's only
   auth entry point, so below 640px the topbar shows the first half of the
   label WHOLE (renderSidebarAccount wraps it in .signInShort/.signInLong)
   and the complete "Sign in / Create account" string stays in the nav-sheet
   row, which at this width is the navigation surface anyway. Scoped to
   `.topnav` so the >=900px sidebar button is unaffected; 768 and 1440 keep
   the long label because 640 is below both. */
@media(max-width:640px){
  .topnav .sidebarSignInBtn .signInLong{display:none}
  /* Measured at 390x844: .topnavIn's gap is tightened to 6px for the whole
     row by the <900px rule further up, leaving the auth control 6px from the
     hamburger. The packet asks for >=8px. Spend the 2px on this one control
     rather than loosening the row's gap, which would cost width at every
     child boundary and eat into the budget the hamburger guard depends on. */
  .topnav .sidebarAccount.sidebarSignIn{margin-right:2px}
}
@media(max-width:560px){
  /* Redundant here: the dashboard directly below reads "2,618,000 points across
     9 programs". At this width it is the difference between reaching the menu
     and not. */
  #ptsTopBar{display:none}
}


/* === PP_NOTFOUND ===
   In-app unknown-route view, rendered by assets/notfound.js
   (window.PP_renderNotFound). Ported from 404.html's <style> block --
   visually identical -- but re-pointed at this file's EXISTING sitewide
   semantic tokens (--bg-canvas, --bg-surface, --text-1/2/3, --line,
   --line-strong, --accent, --accent-text, --accent-on, --radius, --shadow,
   --font, --font-display -- all defined near the top of this file and
   already switched via [data-theme] on <html>) instead of
   404.html's own standalone :root + prefers-color-scheme block. That's
   deliberate: 404.html is a static, self-contained fallback page that
   can't assume styles.css loaded, so it carries its own copy of the
   tokens; this in-app view runs inside the SPA shell where those tokens
   already exist and already track the user's chosen light/dark theme
   (pp_theme), so reusing them (rather than re-declaring a second,
   prefers-color-scheme-driven set) is what keeps the two in sync with
   whichever theme is actually active.
   Every rule below is scoped under .ppNotFound so nothing here can
   collide with an existing class elsewhere in this file. */
.ppNotFound{
  width:100%;
  max-width:560px;
  margin:32px auto;
  background:var(--bg-surface);
  border:1px solid var(--line);
  border-radius:var(--radius);
  box-shadow:var(--shadow);
  padding:40px 32px;
  color:var(--text-1);
  font-family:var(--font);
}
.ppNotFound-eyebrow{
  margin:0 0 10px;
  font-size:13px;
  font-weight:700;
  letter-spacing:.08em;
  text-transform:uppercase;
  color:var(--accent-text);
}
.ppNotFound-h1{
  margin:0 0 14px;
  font-family:var(--font-display);
  font-weight:600;
  font-size:clamp(26px,5vw,34px);
  line-height:1.2;
  color:var(--text-1);
}
.ppNotFound-lede{
  margin:0 0 22px;
  font-size:16px;
  line-height:1.55;
  color:var(--text-2);
}
.ppNotFound-triedRow{
  margin:0 0 26px;
  padding:12px 14px;
  background:var(--bg-canvas);
  border:1px solid var(--line);
  border-radius:10px;
  font-size:14px;
  color:var(--text-3);
}
.ppNotFound-triedRow span{display:block;margin-bottom:4px}
.ppNotFound-triedRow code{
  display:block;
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:13.5px;
  color:var(--text-1);
  word-break:break-all;
  overflow-wrap:anywhere;
}
.ppNotFound-countdownRow{
  display:flex;
  align-items:center;
  gap:14px;
  margin:0 0 24px;
  padding:14px 16px;
  border:1px solid var(--line);
  border-radius:10px;
  background:var(--bg-canvas);
}
.ppNotFound-countRing{
  flex:0 0 auto;
  width:44px;
  height:44px;
  border-radius:50%;
  border:2px solid var(--line-strong);
  display:flex;
  align-items:center;
  justify-content:center;
  font-weight:700;
  font-size:16px;
  color:var(--accent-text);
}
.ppNotFound-countRing.ppNotFound-isStopped{
  color:var(--text-3);
  border-color:var(--line);
}
.ppNotFound #countdownText{
  margin:0;
  font-size:14.5px;
  color:var(--text-2);
}
.ppNotFound #countdownText strong{color:var(--text-1)}
.ppNotFound-actions{
  display:flex;
  gap:12px;
  flex-wrap:wrap;
}
.ppNotFound-btn{
  flex:1 1 160px;
  min-height:48px;
  display:inline-flex;
  align-items:center;
  justify-content:center;
  gap:8px;
  border-radius:999px;
  padding:13px 20px;
  font-size:15px;
  font-weight:650;
  font-family:var(--font);
  text-decoration:none;
  cursor:pointer;
  border:0;
}
.ppNotFound-btnPrimary{
  background:var(--accent);
  color:var(--accent-on);
}
.ppNotFound-btnGhost{
  background:var(--bg-surface);
  color:var(--text-1);
  border:1px solid var(--line-strong);
}
.ppNotFound-btn:focus-visible{
  outline:2px solid var(--accent-text);
  outline-offset:2px;
}
.ppNotFound-srOnly{
  position:absolute;
  width:1px;height:1px;
  padding:0;margin:-1px;
  overflow:hidden;
  clip:rect(0,0,0,0);
  white-space:nowrap;
  border:0;
}
/* Motion is opt-IN, matching 404.html: nothing above animates by default,
   and only when the OS allows motion. */
@media (prefers-reduced-motion: no-preference){
  .ppNotFound-btn{transition:transform .05s ease, opacity .15s ease}
  .ppNotFound-btn:active{transform:translateY(1px)}
  .ppNotFound-countRing{transition:color .2s ease, border-color .2s ease}
}

/* === iOS focus zoom ===
   Reported by Jeremy from his iPhone, 2026-07-26: "Whenever you click an element
   on the mobile page it also Zooms in a little, which means the page starts to
   scroll left and right. We can't have the focus / zooming in happen."

   Cause, measured on the deploy preview at 440x956: focusable fields computed
   below 16px -- composerInput at 15px, a textarea at 12.5px, wallet balance
   inputs at 14.5-15px. iOS Safari zooms the viewport on focus whenever a
   field's font-size is under 16px. That is the whole rule. The left-right
   scrolling he describes is the CONSEQUENCE: once zoomed, the layout is wider
   than the screen. The zoom causes the overflow, not the other way round.

   THE FIX THIS DELIBERATELY IS NOT: adding maximum-scale=1 or user-scalable=no
   to the viewport meta. That is the answer most search results give. It stops
   the zoom by disabling pinch-zoom entirely, which breaks the site for anyone
   who needs to magnify text. It trades an annoyance for an accessibility
   failure. If someone proposes it later, this comment is the reason not to.

   16px exactly, not larger: it is the threshold, and going above it would
   change the visual design rather than just disable the zoom. Applied only at
   mobile widths so desktop typography is untouched. */
@media(max-width:899px){
  input, select, textarea,
  .composerInput, .balInput, .searchbar, .uniSearchInput{
    font-size:16px;
  }
}

/* JOB3 addendum (2026-07-27): re-measured the cascade after the above shipped.
   This guard is a bare type/class list -- (0,0,1) for `input`, (0,1,0) for the
   named classes -- so ANY selector one class more specific still wins and
   still zooms. Seven were found still computing under 16px: .obSearchBar
   input, .inlineBalRow input, .inlineBalField.sm .balInput, .collectAmtInput,
   .hold input, .expActivityInput, .adminRolloutInput. Each is fixed with its
   own @media(max-width:899px){ <same selector>{font-size:16px} } placed right
   next to its source rule (not appended here), so the override sits beside
   the thing it overrides instead of in a second list that goes stale the
   same way this one did.

   Asked to make this generic -- so a brand new `.somethingNew input` next
   month is covered without a new edit -- and it isn't achievable in plain
   CSS without one of two costs: @layer (put every competing rule in this
   file into an earlier-declared layer so this guard, left unlayered, always
   wins regardless of specificity -- correct, but means auditing and moving
   every rule in a 6000+ line stylesheet into layers, which is out of scope
   for a font-size fix and a real risk of breaking unrelated cascades), or
   !important (forbidden by this file's own standing rule, for the same
   reason: it wins by force instead of by a checkable relationship, and the
   next override just adds another !important on top). Neither is a plain-
   CSS specificity fix, so this stays an explicit, if incomplete-by-
   construction, list -- flagged here in words rather than silently. */


/* ============================================================================
   44×44 TAP TARGETS — owner ruling #4, 2026-08-01: "fix now + gate in CI with
   a named exception list, not a blanket <a> exemption."

   MEASURED BEFORE FIXING, not inferred. e2e/tap-target-probe.mjs enumerated
   every visible interactive element under 44px across 4 breakpoints
   (390/440/768/1440) and 7 routes: 27 distinct signatures, 141 occurrences.
   Each rule below names the signature it closes and the measured size, so a
   future reader can tell a real fix from a defensive guess.

   THE GATE IS build/check-tap-targets.mjs, and it reads THIS BLOCK. Every
   selector in its manifest must carry a >=44px min-height (and min-width where
   the measured failure was horizontal). Delete a rule and the build fails —
   that is the point. The exception list lives in the same file with a written
   reason per entry; an unreasoned exception fails the build too.

   WHY min-height AND inline-flex on several of these: min-height alone does
   not grow a box whose content is shorter, and it does not vertically centre
   what is already inside. Setting the box to inline-flex + align-items:center
   makes the 44px real and keeps the label optically where it was, so this is a
   hit-area change rather than a visual redesign.
   ============================================================================ */

/* 37x44 and 43x44 — height was already right, WIDTH was not. Short labels
   ("Home", "Pricing") made the box narrower than the finger. */
.siteFooterLink{display:inline-flex;align-items:center;justify-content:center;min-width:44px}

/* 26x26 — the worst offender on the site, and it carries an aria-label
   ("Collapse chat history") so it is a real control, not decoration. */
.sidebarCollapseBtn{min-width:44px;min-height:44px}

/* 227x40 — "New chat". Width was never the problem. */
.sidebarSubNew{min-height:44px}

/* 34px tall at 1440 — every desktop top-nav item (Home, Wallet, Programs,
   Points Strategy, Data & Resources, Ask P&P AI), in both normal and .active
   states. --topnav-h is 58px, so 44 fits without changing the bar's height. */
.topnavLinks a{display:inline-flex;align-items:center;min-height:44px}

/* 35px tall — the Ask starter chips. */
.askHomeChip{min-height:44px}

/* 36px tall — "Chats", aria-label "Open chat history". */
.topnavChatsBtn{min-height:44px}

/* 43px — off by a single pixel, which is exactly the kind of miss a human
   review waves through and a gate does not. Height is JS-managed up to
   max-height:108px; a min-height floor does not fight that. */
.composerInput{min-height:44px}


/* Second measurement pass, all 16 matrix routes (the first covered 7 and
   missed these). Both are standalone controls, not inline prose. */

/* 173x26 — the "Data & Resources" breadcrumb on /status/, /updates/,
   /methodology/, /name-map/. A navigation control in its own right. */
.dataCrumb{min-height:44px}

/* 67x41 — the "Copy" button on /name-map/. Three pixels short. */
.nmCopyBtn{min-height:44px}

/* R86 /bank-connections — THIS BLOCK IS DELETED ON PURPOSE.
   ---------------------------------------------------------
   It styled a set of invented pm-prefixed manager classes (state, inst,
   instList, cards, cardList, alert, actions, sync), a bankConnect wrapper
   pair, and a bespoke danger button variant. Every one of them was invented
   here to make a hand-rolled
   manager look presentable, and every one of them duplicated a component the
   design system already had: .ppwCard, .ppwManageRow (+ the new read-only
   .ppwBankRow modifier), .ppwHeldCards/.ppwHeldCard, .ppwErrorBox,
   .ppwSectionHead, .ppwTruth and .ppwConfirmBar. Two vocabularies for one
   surface is how the two shells drifted in the first place.

   The route now renders inside .ppWallet .ppBankConnections and inherits the
   real ones from assets/wallet-cc.css. Do not restore anything below this
   comment; add to wallet-cc.css instead, scoped under .ppWallet, or the rule
   will not apply to the surface it was written for. */
