/* 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 */
  --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,.09); --line-strong:rgba(242,237,227,.18);
  --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 */
  --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;
  /* 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;

  /* 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}
body{margin:0;font:16px/1.55 var(--font);color:var(--ink);background:var(--bg2)}
.wrap{max-width:var(--maxw);margin:0 auto;padding:0 20px}
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}
.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)}

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

.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)}

/* Home hero relaunch (Brief #57) — demo animation -> dissolve -> single AI
   entry window. Reuses site tokens/classes (.hero, .eyebrow, .chip, .bubble,
   .chat) via descendant selectors under .ppHero, which win on specificity
   over the bare global rules without needing !important. */
.ppHero .hero{text-align:center;padding:14px 8px 20px;max-width:780px;margin:0 auto}
.ppHero .eyebrow{margin-bottom:14px}
.ppHero h1{font-size:clamp(26px,4vw,44px);line-height:1.1;margin:0 0 12px;letter-spacing:-.01em}
.ppHero h1 .amp{color:var(--brand)}
.ppHero .sub{font-size:17px;color:var(--ink2);max-width:640px;margin:0 auto}

.ppHero .stage{position:relative;background:linear-gradient(180deg,var(--bg-surface),var(--bg2));border:1px solid var(--line);border-radius:20px;padding:26px 22px;box-shadow:var(--shadow);overflow:hidden;min-height:300px}
.ppHero .scene{display:grid;grid-template-columns:1fr auto 1.15fr;gap:16px;align-items:center;transition:opacity .7s ease,transform .7s ease}
.ppHero .stage.dissolved .scene{opacity:0;transform:scale(.96) translateY(-8px);pointer-events:none;position:absolute;inset:26px 22px}
.ppHero .col{background:var(--bg-surface);border:1px solid var(--line);border-radius:16px;padding:16px}
.ppHero .collabel{font-size:12px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:var(--ink2);margin-bottom:12px;display:flex;align-items:center;gap:7px}
.ppHero .dotb{width:9px;height:9px;border-radius:50%;background:var(--good);animation:ppPulse 1.8s infinite}
@keyframes ppPulse{0%{box-shadow:0 0 0 0 rgba(15,118,110,.45)}70%{box-shadow:0 0 0 7px rgba(15,118,110,0)}100%{box-shadow:0 0 0 0 rgba(15,118,110,0)}}
.ppHero .chips{display:flex;flex-direction:column;gap:9px}
.ppHero .chip{display:flex;align-items:center;gap:10px;background:var(--bg2);border:1px solid var(--line);border-radius:11px;padding:9px 11px;opacity:0;transform:translateY(8px) scale(.98)}
.ppHero .chip.in{animation:ppChipin .5s cubic-bezier(.2,.7,.3,1) forwards}
@keyframes ppChipin{to{opacity:1;transform:none}}
.ppHero .chip .mono{width:26px;height:26px;border-radius:7px;color:#fff;font-weight:800;font-size:13px;display:flex;align-items:center;justify-content:center;flex:none}
.ppHero .chip b{font-size:14px;font-weight:700}
.ppHero .chip i{margin-left:auto;font-style:normal;font-weight:700;color:var(--brand-ink);font-variant-numeric:tabular-nums;font-size:13.5px}
.ppHero .datapill{margin-top:12px;text-align:center;font-size:12px;font-weight:700;color:var(--brand-ink);background:var(--accent-soft);border:1px solid var(--accent);border-radius:999px;padding:6px 10px;opacity:0}
.ppHero .datapill.in{animation:ppChipin .5s ease forwards}
.ppHero .flow{display:flex;flex-direction:column;gap:7px;align-items:center}
.ppHero .flow span{width:7px;height:7px;border-radius:50%;background:var(--accent);opacity:.22}
.ppHero .flow.on span{animation:ppFlowpulse 1.1s infinite}
.ppHero .flow.on span:nth-child(2){animation-delay:.16s}.ppHero .flow.on span:nth-child(3){animation-delay:.32s}
@keyframes ppFlowpulse{0%,100%{opacity:.22;transform:scale(1)}50%{opacity:1;transform:scale(1.35)}}
.ppHero .chat{display:flex;flex-direction:column;gap:8px;min-height:130px}
.ppHero .exFlag{align-self:flex-start;font-size:10.5px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:var(--ink2);background:var(--bg2);border:1px solid var(--line);border-radius:999px;padding:2px 8px;margin-bottom:2px}
.ppHero .bubble{padding:11px 13px;border-radius:14px;font-size:14px;line-height:1.45;max-width:96%;margin:0}
.ppHero .bubble.me{align-self:flex-end;background:var(--brand);color:#fff;border-bottom-right-radius:5px}
.ppHero .bubble.me:empty{display:none}
.ppHero .bubble.ai{align-self:flex-start;background:var(--bg2);border:1px solid var(--line);border-bottom-left-radius:5px}
.ppHero .bubble.ai.hide{display:none}
.ppHero .ans b{color:var(--brand-ink)} .ppHero .ans .g{color:var(--good);font-weight:800}
.ppHero .tip{margin-top:7px;padding-top:7px;border-top:1px dashed var(--line);font-size:13px;color:var(--ink2)} .ppHero .tip b{color:var(--brand-ink)}
.ppHero .typing{display:flex;gap:4px;padding:3px 0}
.ppHero .typing span{width:6px;height:6px;border-radius:50%;background:var(--text-4);animation:ppBlink 1.2s infinite}
.ppHero .typing span:nth-child(2){animation-delay:.2s}.ppHero .typing span:nth-child(3){animation-delay:.4s}
.ppHero .typing.hide{display:none}
@keyframes ppBlink{0%,100%{opacity:.3}50%{opacity:1}}

.ppHero .entry{opacity:0;transform:translateY(10px);pointer-events:none;transition:opacity .7s ease .25s,transform .7s ease .25s;max-width:700px;margin:0 auto;text-align:center}
.ppHero .stage.dissolved .entry{opacity:1;transform:none;pointer-events:auto}
.ppHero .ewelcome{font-size:16px;color:var(--ink);margin-bottom:18px;line-height:1.5}
/* Brief #59: the entry bar is the primary affordance on the page — modeled
   after the large, high-contrast, unmistakably-typeable bar pattern used by
   Claude.ai/ChatGPT. Bigger font/padding than the persistent .askrow bar
   elsewhere in the app (this one only ever appears once, dissolved-in, so it
   can afford to dominate) plus an explicit focus-within ring so the active
   state reads clearly. Presentation only — PP.homeAsk() is unchanged. */
.ppHero .ebar{display:flex;gap:10px;align-items:center;border:2.5px solid var(--brand);border-radius:999px;padding:10px 10px 10px 26px;background:var(--bg-surface);box-shadow:0 6px 26px rgba(20,32,43,.16);transition:border-color .15s,box-shadow .15s}
.ppHero .ebar:focus-within{border-color:var(--brand-ink);box-shadow:0 0 0 5px rgba(20,35,67,.15),0 6px 26px rgba(20,32,43,.16)}
.ppHero .ebar input{flex:1;border:0;background:none;font-size:19px;font-weight:600;outline:none;color:var(--ink);font-family:var(--font);min-width:0;padding:16px 0}
.ppHero .ebar input::placeholder{color:var(--ink2)}
.ppHero .ebar .btn{min-height:44px;font-size:15.5px;padding:14px 24px}
@media(max-width:480px){
  .ppHero .ebar{flex-wrap:wrap;border-radius:22px;padding:10px 14px}
  .ppHero .ebar input{flex-basis:100%;width:100%;font-size:17px}
  .ppHero .ebar .btn{flex:1;justify-content:center}
}
.ppHero .chips2{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-top:14px}
.ppHero .chip2[hidden]{display:none}
.ppHero .chip2{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)}
.ppHero .chip2:hover{border-color:var(--brand);color:var(--brand-ink)}
.ppHero .chip2.primaryChip{background:var(--brand);color:#fff;border-color:var(--brand);font-weight:750;width:100%;justify-content:center;padding:12px 16px;font-size:14.5px;box-shadow:var(--shadow)}
.ppHero .chip2browse{border:1px dashed var(--line-strong);background:none;border-radius:999px;padding:8px 14px;font-size:13.5px;font-weight:600;cursor:pointer;color:var(--ink2)}
.ppHero .emicro{margin-top:14px;font-size:12px;color:var(--ink2);max-width:520px;margin:14px auto 0}
.ppHero .replay{display:inline-block;margin-top:18px;font-size:12.5px;font-weight:600;color:var(--ink2);background:var(--bg-surface);border:1px solid var(--line);border-radius:999px;padding:6px 13px;cursor:pointer}

.ppHero .halves{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.ppHero .half{background:var(--bg-surface);border:1px solid var(--line);border-radius:16px;padding:18px}
.ppHero .htag{display:inline-block;font-size:11.5px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;padding:4px 10px;border-radius:999px;margin-bottom:10px}
.ppHero .htag.points{color:var(--brand-ink);background:var(--brand-soft)}
.ppHero .htag.prompts{color:var(--accent-text);background:var(--accent-soft)}
.ppHero .half h3{font-size:18px;font-weight:800;margin:0 0 6px}
.ppHero .half p{margin:0;color:var(--ink2);font-size:14.5px}
@media(max-width:640px){.ppHero .scene{grid-template-columns:1fr}.ppHero .flow{flex-direction:row}.ppHero .halves{grid-template-columns:1fr}}
/* Mobile round 2 (a) -- "ridiculous margins on the landing page" (Jeremy,
   390x844). Root cause: .ppHero .stage's own 26px/22px padding plus the
   nested .col's 16px padding stacked INSIDE the sitewide .wrap's 20px
   gutter, so the wallet/AI cards' actual content sat ~60px in from each
   screen edge on a 390px viewport (>30% of the width spent on padding
   before any content). Scoped to <=640px only -- desktop keeps the
   original spacious 26px/22px + 16px padding untouched. */
@media(max-width:640px){
  .ppHero .stage{padding:16px 12px}
  .ppHero .col{padding:12px}
  .ppHero .half{padding:14px}
}
@media(prefers-reduced-motion:reduce){.ppHero .chip,.ppHero .datapill{opacity:1!important;transform:none!important;animation:none!important}}

/* === WS-HOMEORD === */
/* Mobile fix: the demo animation / entry window (#ppStage) was rendering
   half-hidden below the fold because the eyebrow/h1/sub copy always led.
   The DOM now puts the stage first (see index.html), so mobile shows the
   animation immediately with the copy below it by default. On wider
   screens we restore the original copy-first reading order with flex
   `order` — no visual change on desktop. */
.ppHero{display:flex;flex-direction:column}
.ppHero .ppStageWrap{order:1}
.ppHero .ppHeroBlock{order:2}
.ppHero .ppHalvesWrap{order:3}
@media(min-width:641px){
  .ppHero .ppHeroBlock{order:1}
  .ppHero .ppStageWrap{order:2}
}
/* === end WS-HOMEORD === */

/* 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)}
.row{display:flex;align-items:center;gap:14px;padding:14px 4px;border-bottom:1px solid var(--line)}
.row:last-child{border-bottom:0}
.row .ico{width:40px;height:40px;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}
.row .val{font-weight:750;color:var(--ink);font-size:14px;text-align:right;white-space:nowrap}
.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}
.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}
.btn .btnIc{width:15px;height:15px;margin-right:6px;flex:0 0 auto}

/* ---- 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. */
.matrixTable td.matrixTop{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}

/* ---- Brief #51: print the Strategy matrix as-is (window.print(), no
   scheduler/second data source) — strip everything that isn't the matrix
   itself, and un-contain the horizontal scroller so every column renders on
   the page instead of clipping at the viewport-sized scrollbox. ----*/
@media print{
  *{-webkit-print-color-adjust:exact;print-color-adjust:exact}
  header.nav,nav.mobileMenu,.menuScrim,.sidebarBetaPill,.scrim,.back,
  .matrixPrintBtn,.matrixSortArrow,.matrixSwipeHint,.dock,.matrixNoteTrig{display:none!important}
  .view{display:none!important}
  #view-strategy{display:block!important}
  #view-strategy .panel{padding:0}
  .matrixScroll{overflow:visible;-webkit-mask-image:none;mask-image:none;border:0}
  .matrixTable{width:auto;min-width:0}
  .matrixTable thead{display:table-header-group}
  .matrixTable tr{break-inside:avoid}
  .matrixTable th.matrixCardCol{position:static;box-shadow:none}
  /* Color alone (yellow/green) can wash out on grayscale printers — back
     both signals with a shape/text cue that survives desaturation: a bold
     star prefix for best-in-category, and a real border (not the on-screen
     box-shadow, which some print engines drop) for wallet-held rows. */
  .matrixTable td.matrixTop{border:2px solid #7a5c00}
  .matrixTable td.matrixTop::before{content:"★ "}
  .matrixTable tr.matrixHeld td,.matrixTable tr.matrixHeld th{box-shadow:none;border-top:2px solid var(--good);border-bottom:2px solid var(--good)}
  .matrixTable tr.matrixHeld th.matrixCardCol{border-left:2px solid var(--good)}
  .matrixTable tr.matrixHeld td:last-child{border-right:2px solid var(--good)}
}

/* ---- 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}
.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}
.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}
.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) */
.stepper{display:flex;flex-direction:column;align-items:flex-start;gap:5px;margin:0 0 20px}
.stepPill{display:inline-flex;align-items:center;gap:6px;padding:8px 14px;border-radius:999px;border:1.5px solid var(--line);background:var(--bg-surface);color:var(--ink2);font-size:13px;font-weight:700;cursor:pointer;font-family:var(--font);white-space:nowrap;transition:background .12s,color .12s,border-color .12s}
.stepPill.current{background:var(--step);border-color:var(--step);color:#fff;box-shadow:var(--shadow)}
.stepPill.done{color:var(--step);border-color:var(--step);background:var(--bg-surface)}
.stepPill.upcoming{color:var(--ink2);border-color:var(--line);background:var(--bg2)}
.stepPill:hover{opacity:.92}
.stepCheck{font-weight:800}
.stepArrow{color:var(--ink2);font-size:16px;flex:none;line-height:1;margin-left:18px}
/* Row pairing the stepper with the duplicate top Next/Finish button (Item 1)
   so the primary action is visible without scrolling to the bottom. With a
   vertical stepper the button is pinned top-right instead of vertically
   centered, so it stays reachable without competing with the taller stack. */
.wizStepperRow{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}
.wizStepperRow .stepper{margin:0;flex:1;min-width:0}
.wizTopNavBtn{flex:none;white-space:nowrap}
@media(max-width:430px){
  .stepPill{padding:6px 10px;font-size:11.5px;gap:4px}
  .stepArrow{font-size:12px;margin-left:14px}
  .stepper{gap:4px}
}
/* Sticky stepper+hero: both live inside one sticky "stack" pinned just under
   the site header, so they scroll together as a single unit and can never
   overlap each other. The stepper condenses (smaller pills/padding) once the
   stack is actually pinned — tracked via the .stuck class toggled in
   updateWizSticky() (app.js) by comparing the stack's live position to the
   sticky offset. */
.wizStickyStack{position:sticky;top:63px;z-index:16;background:var(--bg-surface);padding-top:4px;margin-bottom:20px;transition:padding .15s}
.wizStickyStack.stuck .stepper{margin-bottom:4px;gap:2px}
/* min-height:auto overrides the pointer:coarse 44px floor below — the
   pinned/stuck stepper is a condensed progress indicator, not the primary
   tap target (the full-size stepper is one scroll away), and forcing 44px
   here reintroduces the sticky-stack height jump these condensed rules
   exist to prevent (Brief #49). */
.wizStickyStack.stuck .stepPill{padding:3px 8px;font-size:10.5px;gap:4px;min-height:auto}
.wizStickyStack.stuck .stepArrow{font-size:10px;line-height:1;margin-left:12px}
.wizHero{background:linear-gradient(135deg,var(--brand-soft),var(--bg-surface));border:1px solid var(--line);border-radius:var(--radius);padding:14px 18px;margin:0;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px 18px;box-shadow:0 6px 18px rgba(20,32,43,.1)}
.wizHeroCounts{font-size:13px;font-weight:700;color:var(--brand-ink);flex-basis:100%}
.wizHeroRow{display:flex;align-items:center;gap:16px;flex-wrap:wrap}
.wizHeroPts{font-size:13.5px;color:var(--ink2);font-weight:650}
.wizHeroVal{font-size:19px;font-weight:800;color:var(--good)}
.wizHeroLogos{display:flex;gap:5px;flex-wrap:wrap;flex-basis:100%;margin-top:2px}
.wizHeroLogos:empty{display:none;margin:0}
@media(max-width:430px){
  .wizHero{padding:12px 14px}
  .wizHeroVal{font-size:16.5px}
  /* Pts and value always stack on their own lines at this width, instead of
     wrapping only once the digit count grows — that conditional wrap is what
     made the hero's height (and the balance input pinned below it) jump
     mid-keystroke as a typed number got longer. Fixed to 2 lines = fixed
     height, so it never jumps under the user's finger while typing. */
  .wizHeroRow{flex-direction:column;align-items:flex-start;gap:4px}
  .wizStickyStack.stuck .stepper{gap:1px;margin-bottom:3px}
  .wizStickyStack.stuck .stepPill{padding:3px 7px;font-size:10px}
  .wizStickyStack.stuck .stepArrow{font-size:9px;margin-left:10px}
}
.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}
.inlineBalLabel{font-weight:700;font-size:13.5px;color:var(--ink)}
.inlineBalField{display:flex;align-items:center;gap:8px}
.balInput,.inlineBalRow input,.wizAddedRow input[type=text]{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,.wizAddedRow input[type=text]:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 4px rgba(192,138,30,.18)}
.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}

/* ---- Selected-programs panel (steps 2 & 3) ---- */
.wizSelectedPanel{border:1px solid var(--line);border-radius:var(--radius);background:var(--bg-surface);box-shadow:var(--shadow);padding:14px 16px;margin:14px 0 18px}
.wizSelectedPanel.empty{background:var(--bg2);box-shadow:none}
.wizSelectedTitle{margin:0 0 10px;font-size:13px;font-weight:750;color:var(--ink2);text-transform:uppercase;letter-spacing:.03em}
.wizPrefHelp{font-size:12.5px;color:var(--ink2);background:var(--bg2);border:1px solid var(--line);border-radius:10px;padding:8px 12px;margin:0 0 14px}
.wizAdded{display:flex;flex-direction:column;gap:9px;margin:14px 0}
.wizAddedRow{display:flex;align-items:center;gap:10px;border:1px solid var(--line);border-radius:14px;padding:11px 12px;background:var(--bg2);flex-wrap:wrap;box-shadow:0 1px 2px rgba(20,32,43,.04)}
.wizAddedRow+.wizAddedRow{margin-top:0}
.wizAddedRow .grow{flex:1;min-width:120px;font-weight:650;font-size:14.5px}
.prefchk{display:flex;align-items:center;gap:6px;font-size:13px;color:var(--ink2);cursor:pointer;white-space:nowrap}
.prefchk input{min-height:auto;width:auto}
.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)}
.wiznav{display:flex;gap:10px;justify-content:space-between;margin-top:22px;flex-wrap:wrap}
.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}
.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}
/* 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}
.row .plogo{margin-right:2px}
.wizHeroLogos .plogo img,.wizHeroLogos .plogo .mono{width:20px;height:20px}

/* 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}
}

/* 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), plus the
   mode-switch bar shared with the classic wizard fallback. Chip/search/
   balance-input styling itself is inherited from the existing .chip,
   .searchbar, .inlineBalRow etc rules above — only new, onboarding-specific
   layout lives here. */
.chatModeBar{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:14px}
.chatModeLabel{font-weight:800;font-size:14px;color:var(--brand-ink)}
.classicModeBar{margin-bottom:18px}
.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}
@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-56 wallet table === */
/* Brief #56: Your Wallet's three buckets (credit cards / airlines / hotels)
   render as real <table>s with clickable <th> headers instead of a stack of
   styled <div class="hubRow">s -- reusing the Strategy matrix's already-
   verified .matrixSortBtn pattern (base rule above, ~line 274) for the
   header buttons/arrows themselves, so this is the only sort mechanism in
   the app, not a second one. .hubTableScroll is the contained horizontal-
   scroll boundary -- if a long program name or a very narrow phone ever
   forces a bucket's table wider than its column budget, THIS wrapper
   scrolls, the page itself never does (same contract as .matrixScroll).
   contain:layout is required, not decorative: without it, Chrome still
   folds this box's *unclipped* content width into document.documentElement
   .scrollWidth even though overflow-x:auto visually clips it (verified via
   bisection while fixing Brief #53's Save-button-widened rows) -- the page
   was silently page-scrollable by the leaked amount despite looking fine
   and despite body.scrollWidth reporting clean. */
.hubTableScroll{overflow-x:auto;-webkit-overflow-scrolling:touch;contain:layout}
/* Mobile audit P0 #3 follow-up (2026-07-19): now that hubTableRow's real
   table-cell layout is restored (see the removed .hubRow note above), a
   dense bucket (long program names, a balance input, a $ value, a remove
   button) can still genuinely need more width than a narrow phone has —
   that's fine, .hubTableScroll's contained scroll is exactly the designed
   escape hatch (same contract as .matrixScroll). Give it the same reused
   edge-fade cue as .tabs/.matrixScroll so that's visually obvious here too. */
@media(max-width:600px){
  .hubTableScroll{-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 24px),transparent 100%);mask-image:linear-gradient(to right,#000 calc(100% - 24px),transparent 100%)}
}
.hubTable{border-collapse:collapse;width:100%;font-size:14.5px}
.hubTable th,.hubTable td{padding:10px 8px;text-align:left;border-bottom:1px solid var(--line);vertical-align:middle}
.hubTable tbody tr:last-child th,.hubTable tbody tr:last-child td{border-bottom:0}
.hubTable thead th{font-weight:750;color:var(--ink2);font-size:12px;text-transform:uppercase;letter-spacing:.02em;background:var(--bg2);white-space:nowrap}
.hubTable th.hubNameCol .matrixSortBtn,.hubTable th.hubBalCol .matrixSortBtn{text-align:left;justify-content:flex-start}
.hubNameWrap{display:flex;align-items:center;gap:12px;min-width:0}
.hubNameWrap .grow{flex:1;min-width:0;font-weight:650;overflow-wrap:break-word}
.hubBalCol,.hubValCol{white-space:nowrap}
.hubValCol{text-align:right}
.hubTable td.hubValCol .hubRowVal{text-align:right;display:block}
.hubRemoveCol{width:1%;text-align:center}
/* .hubTable's own balance input is denser than the wizard's full-size field
   (matches the pre-table .inlineBalField.sm override this replaces) but must
   still clear the project's 44px touch-target floor -- .inlineBalField.sm's
   38px min-height (styles.css ~line 549) predates this brief and is
   overridden here, scoped to just the wallet table so nothing else using
   that "sm" size elsewhere (e.g. the hub add-panel) is affected. */
.hubTable .inlineBalField.sm .balInput{min-height:44px}
.srOnly{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
/* 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){
  .hubTable{font-size:13.5px}
  .hubTable th,.hubTable td{padding:9px 6px}
  .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}
  .hubNameWrap{gap:8px}
}
/* === end WS-56 === */

/* === 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}
.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}

/* §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. */
.composerFeedbackBtn{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}
.composerFeedbackBtn svg{width:15px;height:15px;flex:none}
.composerFeedbackBtn:hover{border-color:var(--ask-gold);background:var(--ask-gold-soft);color:var(--ask-ink)}

/* §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}
/* 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{padding:16px 16px 32px}
}

/* 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. */
body.askEmpty .appMain{justify-content:center}
body.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. */
.appShell{display:flex;align-items:stretch;height:100vh;height:100dvh;overflow:hidden}
.appMain{flex:1;min-width:0;display:flex;flex-direction:column;height:100%;overflow:hidden}
/* header.nav (mobile-only) stays a direct child of
   .appMain, pinned above #viewsScroll, non-scrolling on every route --
   exactly the same treatment #/ask alone used to get (see the old
   body.viewIsAssistant rule this generalizes). #viewsScroll wraps every
   #view-* container as the one internal scroll region;
   #dock (last child of .appMain, see near the end of this file) sits
   pinned after it, in normal flow, never position:fixed.
   #view-assistant additionally fills 100% of whatever height this
   wrapper gives it (flex:1 further down) and manages its OWN internal
   scroll via #chatHistoryWrap, so it never itself triggers this
   wrapper's scrollbar; every other route's content scrolls here exactly
   as it used to scroll the whole window. */
.viewsScroll{flex:1;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;display:flex;flex-direction:column}

/* === 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/#panelEl/#collbar 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, the Pass-4 collapsed bar/
   panel, 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 === */

/* === PASS4-PANEL ===: in-page answer panel (Fable chat-first spec v2
   §1.2/§2.4, §5 pass 4). Lives inside #dock, directly above #composer --
   a collapsed bar (#collBar) and the panel itself (#panelEl) toggle via
   the [hidden] attribute, the same convention #followupChips/#askBalance
   already use above. Classes are ported 1:1 from the canonical mockup
   (mockups/chat-first-interaction.html: .collbar/.panel/.panel-head/
   .panel-body/.phbtn/.ctxchip) -- every color below is a Pass-1 semantic
   token, zero raw hex. See panelOpen/panelCollapse/panelReopen/panelClose
   in chat.js for the state machine and PP.ask() in router.js for the
   data-page-send branch that opens this.
   BUG FIX (Jeremy-reported P0, cut-off content on Methodology/Points
   Strategy/Your Wallet): this block's `.panel` selector was bare, so it
   also matched every OTHER element carrying class="panel" sitewide --
   #wizWrap, #stratWrap, #methodologyWrap, #resetWrap (see index.html)
   plus several JS-rendered list boxes in explore.js/strategy.js/
   wallet.js -- because it appears later in the cascade than the generic
   `.panel` card style (top of file), its max-height:56vh + overflow:
   hidden silently won on all of them, clipping the bottom of those
   pages' content with no way to scroll to it. Explore/Updates happened
   to route their top-level content through #exPanel/#updatesWrap, which
   never carried class="panel", so they were never hit (router.js's
   PP.renderExplore even strips a stray 'panel' class from #exPanel
   defensively -- a prior workaround for this exact collision, applied
   to one view instead of fixing the selector). Fix: scope every rule in
   this block that's meant for the answer panel to #panelEl specifically,
   so it can never leak onto unrelated .panel-classed elements again. */
.collbar{display:flex;align-items:center;gap:9px;width:100%;background:var(--bg-surface);
  border:1px solid var(--line-strong);border-bottom:none;border-radius:14px 14px 0 0;
  padding:9px 16px;font-size:12.5px;color:var(--text-2);cursor:pointer;font-family:var(--font);
  transition:background .15s}
.collbar[hidden]{display:none}
.collbar:hover{background:var(--bg-surface-2)}
.collbar .collbarStar{color:var(--accent);font-size:12px;flex:none}
.collbar .collbarTitle{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;text-align:left}
.collbar .collbarCount{color:var(--text-4);font-size:11.5px;white-space:nowrap;flex:none}
.collbar .collbarUp{color:var(--text-3);font-size:11px;flex:none}

#panelEl.panel{display:flex;flex-direction:column;width:100%;max-height:56vh;
  background:var(--bg-surface);border:1px solid var(--line-strong);border-bottom:none;
  border-radius:16px 16px 0 0;box-shadow:0 -10px 40px rgba(0,0,0,.18);overflow:hidden;
  animation:panelup .22s ease-out}
#panelEl.panel[hidden]{display:none}
@keyframes panelup{from{transform:translateY(14px);opacity:.4}to{transform:none;opacity:1}}
@media(prefers-reduced-motion:reduce){#panelEl.panel{animation:none}}
.panel-head{display:flex;align-items:center;gap:9px;padding:10px 12px 10px 16px;
  border-bottom:1px solid var(--line);flex-shrink:0}
.panel-head .panelStar{color:var(--accent);font-size:12px;flex:none}
.panel-head .pt{font-size:13px;color:var(--text-1);font-weight:600;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis;min-width:0}
.ctxchip{display:inline-flex;align-items:center;gap:5px;background:var(--accent-soft);
  border:1px solid rgba(199,154,59,.3);color:var(--accent-text);border-radius:999px;
  padding:2.5px 10px;font-size:11px;white-space:nowrap;flex-shrink:0}
.ctxchip[hidden]{display:none}
.panel-head .spacer{flex:1}
.phbtn{width:28px;height:28px;border-radius:8px;display:flex;align-items:center;justify-content:center;
  color:var(--text-3);flex-shrink:0;border:0;background:transparent;cursor:pointer;font-family:var(--font);
  transition:background .15s,color .15s;padding:0}
.phbtn:hover{background:var(--bg-surface-2);color:var(--text-1)}
.phbtn svg{width:14px;height:14px}
.panel-body{overflow-y:auto;min-height:0;padding:16px 18px;display:flex;flex-direction:column;gap:14px}
.panel-body .bubble{margin:0}
.panel-body .bubble.me{align-self:flex-end}
.panel-body .bubble.ai{background:var(--bg-surface-2)}
.panel-body .pillCard{background:var(--bg-surface-2)}
@media(pointer:coarse){
  .phbtn{width:36px;height:36px}
}

/* composer gets square top corners while docked under the collapsed bar
   or the open panel (Decision 2) -- toggled in panelSyncComposerMode(). */
.composer.panelmode{border-radius:0 0 16px 16px;border-top:1px solid var(--line)}

/* 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 both the thread and the panel. */
.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:28px}
.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)}
.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:block;font-size:12px;font-weight:700;color:var(--shell-gold);
  padding:6px 6px 2px;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:32px;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:36px;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)}
.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. */
@media(min-width:900px){
  header.nav{display:none}
  #menuScrim{display:none!important}
}
@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;margin-left:auto}
  header.nav{position:sticky;top:0;z-index:20}
}
@media(max-width:899px) and (prefers-reduced-motion:reduce){
  .sidebar{transition:none}
}
/* === end WS-SHELL === */

/* === 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}
#methodologyWrap p{margin:0 0 10px}
#methodologyWrap ul{font-size:14px}
/* === 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)}
.hubNameWrap .wsCardChip,.hubNameWrap .wsProgChip{margin-right:2px}

/* 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}

/* 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;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}
}
/* 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}
.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}
  /* 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}


/* ---- 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}}
