/* ============================================================================
   BIZ+ — Design System
   "An instrument, not a toy: calm, dignified, fast — and fluent in the user's
   own register." All colour lives in css/tokens.css -- see BUILD-SPEC-V2.md.
   Gold ALWAYS means the user won. Dark theme is first-class; light is a mirror.
   ========================================================================== */

/* ---------- 1. Reset & base --------------------------------------------- */
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;text-size-adjust:100%}
body{margin:0;font-family:var(--font-ui);color:var(--ink);background:var(--bg);
  font-size:15px;line-height:1.45;-webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;overflow-x:hidden;
  font-feature-settings:"tnum" 0;}
h1,h2,h3,h4,p,figure{margin:0}
button{font:inherit;color:inherit;cursor:pointer;border:0;background:none;-webkit-tap-highlight-color:transparent}
a{color:var(--brand-ink);text-decoration:none}
input,select,textarea{font:inherit;color:inherit}
img,svg{max-width:100%;display:block}
ul{margin:0;padding:0;list-style:none}
:focus-visible{outline:2.5px solid var(--brand-ink);outline-offset:2px;border-radius:6px}
::selection{background:color-mix(in srgb,var(--brand) 45%,transparent)}
[hidden]{display:none!important}

/* number tabular alignment for all figures */
.num,.money,.stat-v,.tile-v,td.n,th.n,.mono{font-variant-numeric:tabular-nums}

/* ---------- 1b. The pre-boot screen -------------------------------------
   Everything below this rule needs js/app.js, and js/app.js is the last of 34
   scripts — about 1.19 MB of them. On a slow line that is thirty to sixty
   seconds during which index.html's placeholder is the entire app, and it used
   to be one motionless word. A frozen screen and a dead app look the same.
   The bar is the honest part: it says work is happening, it does not pretend to
   know how much is left, and it carries no words — nothing here can be
   translated, because which of the five languages this person reads is a fact
   only js/core.js knows, and js/core.js has not run yet. That is literal: the
   word "Counting…" was deleted from index.html on 2026-08-26, so this rule
   styles a bar and nothing else, and carries no type declarations because it
   has no type to set.
   It animates `transform` and nothing else on purpose. A transform animation
   runs on the compositor, so it keeps moving while the main thread is busy
   parsing the very scripts it is waiting for; anything driven by layout or
   paint would freeze exactly when it is needed. */
.preboot{display:flex;align-items:center;justify-content:center;height:100vh}
.preboot-track{width:132px;height:3px;border-radius:999px;background:var(--line);overflow:hidden}
.preboot-bar{width:44px;height:100%;border-radius:999px;background:var(--brand);
  animation:preboot-slide 1.15s cubic-bezier(.4,0,.2,1) infinite alternate}
@keyframes preboot-slide{from{transform:translateX(0)}to{transform:translateX(88px)}}
/* Still a signal, just not a moving one. */
@media (prefers-reduced-motion:reduce){.preboot-bar{width:100%;animation:none;opacity:.45}}

/* ---------- 2. Fonts ---------------------------------------------------- */
/* Design tokens now live in css/tokens.css -- the single source of colour.
   They were removed from here on 2026-07-25 because two :root blocks were
   fighting and load order silently decided which colours the app actually got. */

/* ---------- 2b. Safe areas ---------------------------------------------
   index.html sets `viewport-fit=cover`, i.e. the app opts INTO drawing under
   the system bars, and until now spent NOTHING on the top inset: the greeting,
   the bell and the menu had 14px of padding and nothing else between them and
   a Dynamic Island. The bottom inset was honoured, but as five separate
   env() calls that no runtime could reach.

   These two names live HERE, not in tokens.css, for the same reason motion.css
   keeps the duration scale: every one of the six consumers is in this file
   (.sheet, .toast-wrap, .view-pad, .appbar, .tabbar, .fab), so this is the file
   that owns shell geometry and there is exactly one declaration to find. Colour
   is not involved, so the 2026-07-25 two-fighting-:root-blocks failure cannot
   repeat here.

   env() with an explicit 0px fallback so a browser that does not know the
   function still produces a valid calc() rather than dropping the padding
   declaration outright. On Android today the WebView reports 0 for both while
   `android:windowOptOutEdgeToEdgeEnforcement` holds (values-v35/styles.xml), so
   this is a no-op there and becomes the fix the moment that opt-out stops
   working at targetSdk 36. Because both values sit behind a custom property, an
   OnApplyWindowInsetsListener can later publish the real pixel insets by
   setting --safe-top/--safe-bottom on :root (an inline style, which outranks
   this rule) and all six sites update at once, with no CSS change. */
:root{
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
}

/* app root paints the ambient gradient.
   This was `background:var(--bg-grad);background-attachment:fixed` on #app
   itself — one of the few reliable ways to make Android WebView REPAINT a
   1250x720 radial gradient on every scroll frame instead of scrolling it, paid
   on the gesture the user makes most. Dropping the attachment was not an
   option: on the LANDING portal #app IS the scrolled box and is several screens
   tall, so the gradient would re-anchor to the document and the page would go
   several shades darker. A viewport-sized fixed LAYER gives identical pixels —
   same paint area, same anchoring — as a composited layer the scroller never
   touches. Negative z-index keeps it under every child, pointer-events:none out
   of every tap. */
#app{min-height:100vh;min-height:100dvh}
#app::before{content:"";position:fixed;inset:0;z-index:-1;background:var(--bg-grad);pointer-events:none}

/* reduced motion — the single authoritative block lives in css/motion.css and
   already covers every declaration this file had, plus animation-iteration
   -count. Two blanket !important blocks in two files is how a reduced-motion
   bug hides: you fix one and the other keeps overriding. Change it THERE. */

/* ---------- 4. Typography helpers -------------------------------------- */
.display{font-family:var(--font-display);font-optical-sizing:auto;letter-spacing:-.01em;line-height:1.05}
.h1{font-family:var(--font-display);font-weight:600;font-size:clamp(26px,6vw,40px);letter-spacing:-.02em;line-height:1.08}
.h2{font-family:var(--font-display);font-weight:600;font-size:22px;letter-spacing:-.015em;line-height:1.15}
.h3{font-weight:700;font-size:16px;letter-spacing:-.01em}
.eyebrow{font-size:11px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3)}
.lead{font-size:16px;color:var(--ink-2);line-height:1.5}
.muted{color:var(--ink-2)}.faint{color:var(--ink-3)}
.mono{font-family:var(--font-mono);font-size:.86em;letter-spacing:-.01em}
.tiny{font-size:12px}.small{font-size:13px}
.tc{text-align:center}.tr{text-align:right}
.wrap-any{overflow-wrap:anywhere}

/* ---------- 5. Layout primitives -------------------------------------- */
.row{display:flex;align-items:center;gap:12px}
.row.top{align-items:flex-start}
.row.wrap{flex-wrap:wrap}
.between{display:flex;align-items:center;justify-content:space-between;gap:12px}
.col{display:flex;flex-direction:column}
.grow{flex:1;min-width:0}
.gap-4{gap:4px}.gap-6{gap:6px}.gap-8{gap:8px}.gap-12{gap:12px}.gap-16{gap:16px}.gap-24{gap:24px}
.stack>*+*{margin-top:12px}
.stack-16>*+*{margin-top:16px}
.stack-24>*+*{margin-top:24px}
.grid{display:grid;gap:12px}
.grid-2{grid-template-columns:1fr 1fr}
.grid-3{grid-template-columns:repeat(3,1fr)}
/* Every .grid-3 in the app is a row of money tiles (HOME, COACH, BUDGET, AJO),
   and on a 320px phone three of them leave each .kpi 54px of inner width. A
   naira figure does not fit in 54px and .kpi clips what does not fit, so
   ₦695,500 painted as "₦695" — measured on HOME, COACH, BUDGET and AJO. Two
   across is the widest a phone can honestly carry: it gives the same tile 104px
   and every compact figure fits at its designed size. The odd third tile takes
   the whole second row rather than sitting alone beside a hole — and the
   :last-child guard means a six-tile grid still lays out as 2x3 untouched. */
@media (max-width:400px){
  .grid-3{grid-template-columns:1fr 1fr}
  .grid-3 > :nth-child(3):last-child{grid-column:span 2}
}
.grid-auto{grid-template-columns:repeat(auto-fill,minmax(150px,1fr))}
.pad{padding:var(--gut)}.pad-lg{padding:20px}
.mt-4{margin-top:4px}.mt-8{margin-top:8px}.mt-12{margin-top:12px}.mt-16{margin-top:16px}.mt-24{margin-top:24px}
.hairline{height:1px;background:var(--line);border:0;margin:0}

/* ---------- 6. Cards & tiles ------------------------------------------ */
.card{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-2);
  box-shadow:var(--shadow-1);padding:16px;position:relative}
.card.flat{box-shadow:none}
.card.raised{box-shadow:var(--shadow-2)}
.card.pad-lg{padding:20px}
/* `background` used to ride in this list. Nothing ever moved it: .card.tap has
   exactly two states below, one transform and one border-colour, and no module
   sets a background on a .card.tap. So it was a paint transition on every
   tappable card that could not fire — 15 of them on LEARN, which is 15 of that
   screen's 20 paint transitions. The one thing it DID catch was the theme flip
   changing --surface underneath it, which made a .card.tap fade while every
   plain .card next to it snapped. Removing it is one fewer paint property and
   one more consistent theme flip. */
.card.tap{transition:transform var(--t-tap) var(--ease-out),border-color var(--t-fast) var(--ease-out)}
.card.tap:active{transform:scale(.985)}
.card.tap:hover{border-color:var(--line-2)}
.card-title{font-weight:700;font-size:14px;display:flex;align-items:center;gap:8px}
.card-sub{font-size:12.5px;color:var(--ink-3);margin-top:2px}

.tile{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-2);padding:14px;box-shadow:var(--shadow-1)}
.tile-k{font-size:11.5px;font-weight:600;color:var(--ink-3);letter-spacing:.02em;text-transform:uppercase}
.tile-v{font-family:var(--font-display);font-weight:600;font-size:23px;letter-spacing:-.02em;margin-top:5px}
.tile-d{font-size:12px;color:var(--ink-2);margin-top:3px}

/* ---------- 7. Buttons ------------------------------------------------- */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;
  min-height:var(--tap);padding:0 18px;border-radius:var(--r-pill);font-weight:600;font-size:14.5px;
  background:var(--surface-2);color:var(--ink);border:1px solid var(--line-2);
  transition:transform var(--t-tap) var(--ease-out),
    background var(--t-fast) var(--ease-out),border-color var(--t-fast) var(--ease-out);white-space:nowrap}
.btn:active{transform:scale(.97)}
.btn:hover{background:var(--surface-hover)}
/* `filter` sat on the base .btn, so EVERY button in the app carried a paint
   transition for a brightness lift that exactly one variant has, in a state a
   thumb cannot produce — 11 buttons on GUARD alone. It lives with its one
   consumer now. Restated in full rather than appended: a second `transition`
   shorthand replaces the first outright, so the other three properties have to
   come with it. The list is otherwise byte-identical to what .btn had. */
.btn.primary{background:var(--grad-brand);color:var(--on-brand);border-color:transparent;box-shadow:var(--grad-brand-shadow);
  transition:transform var(--t-tap) var(--ease-out),filter var(--t-fast) var(--ease-out),
    background var(--t-fast) var(--ease-out),border-color var(--t-fast) var(--ease-out)}
.btn.primary:hover{filter:brightness(1.06)}
.btn.gold{background:var(--grad-gold);color:var(--on-gold);border-color:transparent;box-shadow:var(--grad-brand-shadow)}
.btn.ghost{background:transparent;border-color:var(--line-2)}
.btn.subtle{background:var(--brand-soft);color:var(--brand-ink);border-color:transparent}
.btn.danger{background:var(--danger-soft);color:var(--danger-ink);border-color:transparent}
.btn.block{width:100%}
/* 34px is the drawn height GUARD's dense charge list needs; --tap is the height
   a thumb needs, and .btn.sm was overriding it with nothing to put it back. The
   pair of buttons on a charge row are opposite verdicts 10px apart, and one of
   them — "I cancelled it" — books money into the gold "won back" headline, so a
   near-miss on a 34px target does not just misfire, it inflates the figure the
   product is sold on. Same invisible expander as .switch below: the button is
   not moved or resized, the row does not reflow, the target becomes 44px. */
.btn.sm{position:relative;min-height:34px;padding:0 13px;font-size:13px}
.btn.sm::before{content:"";position:absolute;left:0;right:0;top:50%;height:var(--tap);transform:translateY(-50%)}
.btn.lg{min-height:52px;font-size:16px;padding:0 24px}
.btn:disabled{opacity:.5;pointer-events:none}
.btn .ic{width:18px;height:18px}
.btn-row{display:flex;gap:10px}
/* Two buttons that mean opposite things, one of which moves money. 10px of gap
   is close enough that a thumb aimed at the edge of one lands on the other;
   with this the near-miss lands on nothing instead, which is recoverable. */
.btn-row.decide{gap:18px}
.icon-btn{width:var(--tap);height:var(--tap);border-radius:var(--r-pill);display:inline-flex;align-items:center;
  justify-content:center;background:var(--surface-2);border:1px solid var(--line);
  transition:background var(--t-fast) var(--ease-out),transform var(--t-tap) var(--ease-out)}
.icon-btn:active{transform:scale(.92)}
.icon-btn:hover{background:var(--surface-hover)}
.notif-dot{position:absolute;top:4px;right:4px;min-width:15px;height:15px;padding:0 3px;border-radius:999px;background:var(--m-coral-fill);color:transparent;font-size:0;display:flex;align-items:center;justify-content:center;border:2px solid var(--surface)}
/* Available to a screen reader, absent from the page. A coloured dot is a fine
   signpost for anyone who can see it and nothing at all for anyone who cannot,
   so the sentence it stands for is carried here instead. */
.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}

/* ---------- 8. Chips / segmented / switch ----------------------------- */
.chip{position:relative;display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 12px;border-radius:var(--r-pill);
  background:var(--surface-2);border:1px solid var(--line);font-size:12.5px;font-weight:600;color:var(--ink-2);
  transition:background var(--t-fast) var(--ease-out),color var(--t-fast) var(--ease-out),
    border-color var(--t-fast) var(--ease-out);white-space:nowrap}
/* The month picker on INSIGHT and GUARD is chips, and 32px is 12px under the
   floor this file sets for itself. ::after and not ::before, because .chip.on
   ::before is already the selection tick — and absolute, so it is not a flex
   item of the chip and takes no layout space. .chip-scroll below carries the
   matching 6px of vertical padding so the expander stays inside the scroller
   and is not clipped out of hit-testing by its own overflow.
   button/a only: .chip is also worn by plain <span> badges (AJO's member count,
   the landing plan tags), and an invisible 44px overlay on something nobody can
   press is a tap thief, not a tap target. */
button.chip::after,a.chip::after{content:"";position:absolute;left:0;right:0;top:50%;height:var(--tap);transform:translateY(-50%)}
.chip.on{background:var(--brand-soft);color:var(--brand-ink);border-color:transparent}
/* Selection was signalled by fill colour and nothing else — which the language
   picker, the category corrector and the share-privacy switch all depend on.
   A tick is a second, non-colour signal (WCAG 1.4.1); aria-pressed at the call
   sites is the third, for anyone who cannot see either. */
.chip.on::before{content:"\2713";font-weight:800;font-size:11px;line-height:1;flex:none}
.chip.gold.on{background:var(--gold-soft);color:var(--gold-ink)}
.chip .dot{width:8px;height:8px;border-radius:50%}
.chip-scroll{display:flex;gap:8px;overflow-x:auto;padding:6px 0;scrollbar-width:none;-ms-overflow-style:none}
.chip-scroll::-webkit-scrollbar{display:none}

.seg{display:inline-flex;background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r-pill);padding:3px;gap:2px}
/* `transition:.15s` here was `transition: all`, so every incidental property
   change — including layout ones — animated. Named properties only. */
.seg button{padding:7px 14px;border-radius:var(--r-pill);font-size:13px;font-weight:600;color:var(--ink-2);
  transition:background var(--t-fast) var(--ease-out),color var(--t-fast) var(--ease-out)}
.seg button.on{background:var(--grad-brand);color:var(--on-brand)}

.switch{position:relative;width:46px;height:28px;border-radius:999px;background:var(--surface-3);border:1px solid var(--line-2);
  transition:background var(--t-fast) var(--ease-out);flex:none}
/* 46x28 is the drawn size; --tap is the size a thumb needs. An invisible
   expander gives it the 44px the design system asks for without moving the
   switch or reflowing the row it sits in. */
.switch::before{content:"";position:absolute;left:0;right:0;top:50%;height:var(--tap);transform:translateY(-50%)}
.switch::after{content:"";position:absolute;top:2px;left:2px;width:22px;height:22px;border-radius:50%;background:var(--ink-2);
  transition:transform var(--t-fast) var(--ease-out),background var(--t-fast) var(--ease-out)}
.switch.on{background:var(--brand)}
.switch.on::after{transform:translateX(18px);background:var(--on-fill)}

/* ---------- 9. Rows / list items -------------------------------------- */
.item{display:flex;align-items:center;gap:12px;padding:12px 4px;border-bottom:1px solid var(--line);min-height:56px}
.item:last-child{border-bottom:0}
.item . av{flex:none}
.item .main{flex:1;min-width:0}
.item .t1{font-weight:600;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* A row whose title carries status flags (failed / fee / money-back /
   check-this) lays its title out as a flex row instead, so the narration
   truncates and the flags never do — see modules/view.js txnRow(). Nothing
   here forces that on a plain .t1; the flags row opts in. */
.item .t1>.badge,.item .t1>.tintchip{flex:none}
.item .t2{font-size:12.5px;color:var(--ink-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.item .amt{font-family:var(--font-display);font-weight:600;font-size:15px;letter-spacing:-.01em;flex:none;text-align:right}
.tap-row{transition:background var(--t-tap) var(--ease-out);border-radius:10px;cursor:pointer}
.tap-row:active{background:var(--surface-2)}

.av{width:40px;height:40px;border-radius:12px;display:flex;align-items:center;justify-content:center;
  font-weight:700;font-size:14px;background:var(--surface-3);color:var(--ink);overflow:hidden}
.av.sm{width:32px;height:32px;border-radius:9px;font-size:12px}
.av.lg{width:52px;height:52px;border-radius:15px;font-size:18px}
.av.round{border-radius:50%}

/* ---------- 10. Money display + market mode --------------------------- */
.money{font-family:var(--font-display);font-weight:600;letter-spacing:-.02em;font-variant-numeric:tabular-nums}
.money .k{opacity:.62;font-size:.62em;font-weight:600}     /* kobo */
.money.pos{color:var(--credit-ink)}
.money.gold{color:var(--gold-ink)}
.money.neg{color:var(--ink)}
.money.danger{color:var(--danger-ink)}
.ccy{font-size:.6em;font-weight:700;opacity:.7;margin-right:.12em;vertical-align:.06em}
/* market mode: veil every figure.
   pointer-events:none used to sit on .mask itself — right for a plain figure,
   wrong the moment the masked node IS the control. GOALS puts `mask` and
   role="button" on one element, so market mode silently killed "where this
   number comes from" on every goal (keyboard worked, touch did not: the inverse
   of the rest of the app). Veil everything; stop pointers only on what was
   never interactive.
   The veil does NOT ease. 24 .mask nodes were counted on the VIEW home, and
   easing a 9px blur on 24 at once is paint+GPU work on a Mali or Adreno at the
   moment a trader is covering figures in public, where speed beats grace. It
   was asymmetric anyway — this rule matches only while market mode is ON, so
   the veil eased in over 240ms and vanished instantly. Both directions are
   instant now; at rest, the same blur(9px) saturate(.4) at .9. */
.market-mode .mask{filter:blur(9px) saturate(.4);opacity:.9;user-select:none}
.market-mode .mask:not([role]):not([tabindex]):not(button):not(a){pointer-events:none}
.market-mode .mask-hide{visibility:hidden}
/* the screen-reader / market-mode substitute for a veiled figure — see
   kit.js ui.maskedTag(), which is what finally emits it */
.masked-tag{display:none}
.market-mode .masked-tag{display:inline;font-size:12px;color:var(--ink-3);font-weight:600}

/* evidence "explain every number" affordance */
.explain{border-bottom:1.5px dotted color-mix(in srgb,var(--brand-ink) 55%,transparent);cursor:help}

/* ---------- 11. Badges / pills / status ------------------------------- */
.badge{display:inline-flex;align-items:center;gap:5px;height:22px;padding:0 9px;border-radius:999px;font-size:11px;font-weight:700;letter-spacing:.01em}
.badge.teal{background:var(--brand-soft);color:var(--brand-ink)}
.badge.gold{background:var(--gold-soft);color:var(--gold-ink)}
.badge.credit{background:var(--credit-soft);color:var(--credit-ink)}
.badge.danger{background:var(--danger-soft);color:var(--danger-ink)}
.badge.warn{background:var(--warn-soft);color:var(--warn-ink)}
.badge.violet{background:var(--violet-soft);color:var(--violet-ink)}
.badge.muted{background:var(--surface-3);color:var(--ink-2)}
/* Geometry and colour only — at rest, a 7px credit-green disc. The BREATHING
   comes from .a-pulse/kg-pulse in css/motion.css, which lists .dot-live beside
   it so the period is written once. It used to expand a box-shadow ring here: a
   repaint every frame, for ever, once per unseen notification (up to 9) and for
   a whole AI coach answer. kg-pulse uses opacity, which composites. */
.dot-live{width:7px;height:7px;border-radius:50%;background:var(--credit)}

/* ---------- 12. Progress / meters ------------------------------------- */
.bar{height:8px;border-radius:999px;background:var(--surface-3);overflow:hidden}
/* No transition on the fill. Every meter (ui.bar, the BUDGET meters) is built
   from an HTML string with its width already inline, so the node is born at its
   final width and the declaration that used to sit here could never fire once.
   A mount animation is deliberately NOT substituted: it would replay on every
   data refresh — the exact fault this release removes — and .no-arrive cannot
   reach an inline style. If a meter is ever made to move, use transform:scaleX
   from a left origin, never width. */
.bar>i{display:block;height:100%;border-radius:999px;background:var(--brand)}
.bar>i.gold{background:var(--grad-gold)}
.bar>i.credit{background:var(--credit)}
.bar>i.danger{background:var(--danger)}
.ring{--p:0;--sz:96px;--th:9px;width:var(--sz);height:var(--sz);border-radius:50%;flex:none;
  background:conic-gradient(var(--rc,var(--brand)) calc(var(--p)*1%),var(--surface-3) 0);
  display:flex;align-items:center;justify-content:center;position:relative}
.ring::before{content:"";position:absolute;inset:var(--th);border-radius:50%;background:var(--surface)}
.ring .ring-c{position:relative;text-align:center}

/* ---------- 13. Sheets / modals / toasts ------------------------------ */
/* Surfaces declare their timing TWICE on purpose. A transition is read from the
   state being ENTERED, so the base rule is the exit and the .show rule is the
   entry. Entry is --t-slow on --ease-out; exit is one step quicker on --ease-in,
   because a surface leaving is not news. Every exit is also shorter than the
   setTimeout that removes the node in kit.js (sheet 340ms, modal 260, toast 320,
   popover 180), so nothing is torn out of the DOM mid-fade. The scrim used to
   run .22s both ways and uncovered the screen 100ms before the sheet had gone. */
.scrim{position:fixed;inset:0;background:var(--scrim);z-index:60;opacity:0;
  transition:opacity var(--t-base) var(--ease-in);backdrop-filter:blur(2px)}
.scrim.show{opacity:1;transition:opacity var(--t-slow) var(--ease-out)}
.sheet{position:fixed;left:0;right:0;bottom:0;z-index:61;background:var(--surface);border-radius:22px 22px 0 0;
  box-shadow:var(--shadow-3);padding:8px 16px calc(16px + var(--safe-bottom));transform:translateY(102%);
  transition:transform var(--t-base) var(--ease-in);max-height:92vh;overflow-y:auto;max-width:520px;margin:0 auto}
.sheet.show{transform:translateY(0);transition:transform var(--t-slow) var(--ease-out)}
.sheet-grip{width:38px;height:4px;border-radius:999px;background:var(--line-2);margin:6px auto 12px}
.sheet-title{font-family:var(--font-display);font-weight:600;font-size:19px;letter-spacing:-.01em}
.modal{position:fixed;inset:0;z-index:61;display:flex;align-items:center;justify-content:center;padding:20px;pointer-events:none}
.modal .box{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-3);box-shadow:var(--shadow-3);
  width:100%;max-width:440px;padding:22px;transform:scale(.94);opacity:0;pointer-events:auto;max-height:90vh;overflow:auto;
  transition:transform var(--t-fast) var(--ease-in),opacity var(--t-fast) var(--ease-in)}
.modal.show .box{transform:scale(1);opacity:1;
  transition:transform var(--t-base) var(--ease-out),opacity var(--t-base) var(--ease-out)}
.toast-wrap{position:fixed;left:0;right:0;bottom:calc(78px + var(--safe-bottom));z-index:80;display:flex;flex-direction:column;align-items:center;gap:8px;pointer-events:none;padding:0 16px}
.toast{background:var(--surface-3);color:var(--ink);border:1px solid var(--line-2);border-radius:14px;
  padding:11px 16px;font-size:13.5px;font-weight:600;box-shadow:var(--shadow-2);max-width:440px;
  display:flex;align-items:center;gap:10px;transform:translateY(14px);opacity:0;pointer-events:auto;
  transition:transform var(--t-fast) var(--ease-in),opacity var(--t-fast) var(--ease-in)}
.toast.show{transform:translateY(0);opacity:1;
  transition:transform var(--t-base) var(--ease-out),opacity var(--t-base) var(--ease-out)}
.toast.gold{border-color:color-mix(in srgb,var(--gold) 55%,transparent);background:color-mix(in srgb,var(--gold-soft) 60%,var(--surface))}
.toast.danger{border-color:color-mix(in srgb,var(--danger) 45%,transparent)}
/* the one control a toast may carry: an undo, beside the line confirming the
   thing it undoes. Inherits the toast's ink so it reads on gold and danger too. */
.toast-action{background:none;border:0;padding:0 0 0 4px;margin-left:auto;font:inherit;color:inherit;
  font-weight:800;text-decoration:underline;white-space:nowrap;cursor:pointer}

/* popover (evidence for a number) */
.pop{position:fixed;z-index:82;background:var(--surface);border:1px solid var(--line-2);border-radius:14px;
  box-shadow:var(--shadow-3);padding:12px 14px;max-width:300px;font-size:12.5px;transform:scale(.96);opacity:0;
  transition:transform var(--t-tap) var(--ease-in),opacity var(--t-tap) var(--ease-in)}
.pop.show{transform:scale(1);opacity:1;
  transition:transform var(--t-fast) var(--ease-out),opacity var(--t-fast) var(--ease-out)}

/* ---------- 14. Inputs ------------------------------------------------- */
.field{display:block;width:100%}
.field label,.lbl{display:block;font-size:12.5px;font-weight:600;color:var(--ink-2);margin-bottom:6px}
.input,.select,.textarea{width:100%;min-height:var(--tap);padding:11px 13px;border-radius:12px;
  background:var(--surface-2);border:1px solid var(--line-2);color:var(--ink);font-size:15px;
  transition:border-color var(--t-fast) var(--ease-out),background var(--t-fast) var(--ease-out)}
.input:focus,.select:focus,.textarea:focus{outline:none;border-color:var(--brand-ink);background:var(--surface)}
.textarea{min-height:96px;resize:vertical;line-height:1.5}
.input.mono{font-family:var(--font-mono);font-size:13px}
.input-amt{font-family:var(--font-display);font-size:22px;font-weight:600}

/* ---------- 15. Skeleton --------------------------------------------- */
/* `.sk` + @keyframes shimmer stood here with zero call sites, sweeping
   0 -> -200% while the LIVE skeleton (.skeleton, motion.css) sweeps the other
   way — two shimmer systems in opposite directions, one of them invisible.
   There is one skeleton now, the one with 73 callers. `spin` stays: motion.css
   .a-spin and onboarding's inline spinner both use it, and keyframe names are
   global regardless of file order. */
@keyframes spin{to{transform:rotate(360deg)}}

/* ---------- 16. Mobile phone frame & app shell ------------------------ */
.mobile-stage{min-height:100vh;min-height:100dvh;display:flex;align-items:center;justify-content:center;padding:0}
@media (min-width:720px){
  .mobile-stage{padding:28px 16px}
}
.phone{position:relative;width:100%;max-width:430px;margin:0 auto;background:var(--bg);
  min-height:100vh;min-height:100dvh;display:flex;flex-direction:column;overflow:hidden}
@media (min-width:720px){
  .phone{min-height:auto;height:min(900px,94vh);border-radius:40px;border:1px solid var(--line-2);
    box-shadow:var(--shadow-3),0 0 0 10px rgba(0,0,0,.28);background:var(--bg-grad)}
  .phone .screen{border-radius:40px}
}
/* BELOW the desktop frame, the phone IS the viewport — and this is the block
   that says so. Everything above declares `min-height`, which is a FLOOR and
   not a cap, and .mobile-stage centres rather than stretches, so the column had
   no upper bound at all: it grew to its content (measured 1,882px against an
   812px viewport at 375 wide, every tab over 1,300) and the DOCUMENT became the
   scroller. Only the >=720px branch ever set a real height, which is exactly
   why the desktop "phone frame" preview looked right and a real phone did not.

   The chrome is the thing this restores. .appbar is `position:sticky` but its
   scroll container never scrolled, so it had nothing to stick to; .tabbar and
   .fab are `position:absolute` to .screen, so they parked at the bottom of a
   1,882px box a thumb could not reach. Cap the frame and scrolling moves inside
   .scroll, which is what all three were written against.

   `min-height:0` on .screen and .scroll is belt-and-braces against flexbox
   automatic minimum size: a flex item refuses to shrink below its content
   without it, which would hand the overflow straight back one level down.
   100vh first and 100dvh second, matching the pattern above — a WebView too old
   for dvh still gets a cap, just one that ignores the URL bar. */
@media (max-width:719.98px){
  .mobile-stage{height:100vh;height:100dvh;align-items:stretch}
  .phone{height:100vh;height:100dvh;min-height:0}
  .phone .screen{min-height:0}
  .scroll{min-height:0}
}
/* ===== A REAL DEVICE IS NOT A PREVIEW =====================================
   Everything above draws the app inside a 430px-wide simulated handset with a
   bezel, a radius and a drop shadow. That is a presentation device for the
   marketing site, and it shipped inside the native binary — where it is simply
   false. On an iPhone 17 Pro Max (440pt) the product never reached the edges;
   on iPad the >=720px branch fired and the whole app became a small
   phone-shaped card floating in the middle of the display. Apple rejected
   build 16 under guideline 4 for precisely this: "not optimized to support all
   screen sizes or resolutions", with a note that an app downloadable onto iPad
   must work for iPad users.
   So inside the app the frame is not overridden piecemeal — it is switched off.
   native.js stamps `native` on <html> before the first paint. */
html.native .mobile-stage{padding:0;min-height:100vh;min-height:100dvh;height:100vh;height:100dvh;align-items:stretch}
html.native .phone{
  max-width:none;width:100%;
  height:100vh;height:100dvh;min-height:0;
  border:0;border-radius:0;box-shadow:none;background:var(--bg);
}
html.native .phone .screen{border-radius:0;min-height:0}
html.native .scroll{min-height:0}

/* Filling the screen is the floor, not the finish. A single column of money
   rows stretched across a 1024pt iPad is technically "responsive" and still
   looks unconsidered, so the CHROME goes full-bleed (background, tab bar, safe
   areas) while the READING COLUMN keeps a comfortable measure and centres.
   Applies on any wide viewport, native or web, so an Android tablet and a
   desktop browser get the same treatment as iPad — and phones, which never
   reach 560px of usable width, are untouched by every rule here. */
@media (min-width:560px){
  html.native .scroll > *,
  html.native .view-pad{max-width:620px;margin-left:auto;margin-right:auto}
  /* The bars keep their full-bleed background and their sticky/fixed behaviour;
     only their CONTENT is pulled into the same column — by padding the bar, not
     by sizing its children.

     Sizing the children is what the first version of this rule did, and it was
     a real defect: `html.native .appbar > *` is (0,2,1) and beats `.icon-btn`
     (0,1,0), so `width:100%` landed on every 44px round button. Measured at
     820x1180 with .native on <html>: each icon button became 252.7px and
     `.grow` — flex-basis:0, so it contributes no scaled shrink — collapsed to
     0px, taking the screen title with it. Every heading in the app vanished on
     iPad, which is the one device App Review keeps testing on. Pad the bar and
     the flex row inside it is left exactly as the phone lays it out. */
  html.native .appbar{
    padding-left:max(16px,calc((100% - 620px) / 2));
    padding-right:max(16px,calc((100% - 620px) / 2));
  }
  html.native .tabbar{
    padding-left:max(6px,calc((100% - 620px) / 2));
    padding-right:max(6px,calc((100% - 620px) / 2));
  }
  /* The FAB is a child of .screen, not of .scroll, so no column rule above ever
     reached it: on an 11-inch iPad it floated ~200pt to the right of the column
     it acts on, anchored to the device edge while the content it adds to sat in
     the middle. The column's right edge is at 50% + 310px, so 50% - 292px parks
     it 18px inside that edge — the same inset it has on a phone, where the
     max() keeps the old value because the column fills the screen. */
  html.native .fab{right:max(18px,calc(50% - 292px))}
}
/* Landscape on a short screen (a phone turned sideways, a Split View pane):
   the tab bar plus the padding written for a 812pt canvas eats the content, so
   give the scroller back its room rather than letting rows disappear. */
@media (max-height:480px){
  html.native .view-pad{padding-bottom:calc(72px + var(--safe-bottom))}
}

.phone .screen{flex:1;display:flex;flex-direction:column;overflow:hidden;position:relative}
.scroll{flex:1;overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch;overscroll-behavior-y:contain}
.scroll::-webkit-scrollbar{width:0}
.view-pad{padding:12px 16px calc(96px + var(--safe-bottom))}

/* status/topbar inside phone.
   The top inset is spent HERE and nowhere else: .appbar is the first thing in
   the scroller on every tab, it is sticky, and it carries the three controls
   (greeting, bell, menu) that a notch or a Dynamic Island would otherwise sit
   on top of. 14px is what the design asks for BELOW the system bar, so the
   inset is added to it rather than replacing it — on a device with no inset
   --safe-top is 0px and the padding is byte-for-byte what it was. */
.appbar{display:flex;align-items:center;gap:10px;padding:calc(14px + var(--safe-top)) 16px 8px;position:sticky;top:0;z-index:20;
  background:linear-gradient(180deg,var(--bg) 62%,transparent)}
.appbar .title{font-family:var(--font-display);font-weight:600;font-size:19px;letter-spacing:-.01em}
.appbar .sub{font-size:11.5px;color:var(--ink-3)}

/* bottom tab bar (thumb reach).
   The 16px backdrop blur below is the largest remaining per-frame cost in the
   app: the bar sits permanently over the list, so the compositor re-samples
   behind it on every scroll frame — the gesture the user makes most, on the
   phones least able to afford it. The known fix (an opaque bar below phone
   width) changes how the bar LOOKS at rest, which is a design decision and not
   a timing one, so it is not taken here. See the reduced-transparency block
   after .land-nav for the one case that costs nothing to honour. */
.tabbar{position:absolute;left:0;right:0;bottom:0;z-index:30;display:flex;justify-content:space-around;
  padding:8px 6px calc(8px + var(--safe-bottom));
  background:color-mix(in srgb,var(--surface) 88%,transparent);backdrop-filter:blur(16px) saturate(1.2);
  border-top:1px solid var(--line)}
.tabbar button{flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;padding:4px 2px;color:var(--ink-3);
  font-size:10px;font-weight:600;transition:color var(--t-fast) var(--ease-out);position:relative}
.tabbar button .ic{width:23px;height:23px;transition:transform var(--t-fast) var(--ease-out)}
.tabbar button.on{color:var(--brand-ink)}
.tabbar button.on .ic{transform:translateY(-1px)}
.tabbar button.on::before{content:"";position:absolute;top:-8px;width:22px;height:3px;border-radius:999px;background:var(--brand)}
/* Gold means money the user WON or recovered — nowhere else. Painting the
   active Coach tab gold spent the one colour the language reserves on a
   navigation state, which is exactly how a reserved colour stops meaning
   anything. Coach keeps its prominence from the 26px centre icon. */
.tabbar button.coach.on{color:var(--brand-ink)}
.tabbar button.coach.on::before{background:var(--brand)}

/* FAB */
.fab{position:absolute;right:18px;bottom:calc(84px + var(--safe-bottom));z-index:31;width:56px;height:56px;
  border-radius:20px;background:var(--grad-brand);color:var(--on-brand);display:flex;align-items:center;
  justify-content:center;box-shadow:var(--grad-gold-shadow);transition:transform var(--t-tap) var(--ease-out)}
.fab:active{transform:scale(.9)}

/* ---------- 17. Charts (pure CSS/SVG) -------------------------------- */
.spark{display:block;width:100%;height:40px}
.spark path.line{fill:none;stroke:var(--brand-ink);stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round}
.spark path.area{fill:var(--brand-soft);stroke:none}
/* `.bars`/`.bars .b` are gone with the height transition a previous pass had
   already removed from them. It could never fire — columns get their height
   inline at build time and are never re-measured — and the classes had no
   renderer at all: INSIGHT's trend explorer builds bare divs with inline
   heights. Grow a column with transform:scaleY from a bottom origin if one is
   ever wanted; never with height, which is a layout animation. */
.legend{display:flex;flex-wrap:wrap;gap:10px}
.legend .l{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--ink-2)}
.legend .l i{width:10px;height:10px;border-radius:3px}

/* stacked category bar */
.stackbar{display:flex;height:14px;border-radius:999px;overflow:hidden;background:var(--surface-3)}
.stackbar>i{height:100%}

/* ---------- 18. Money Story shareable card --------------------------- */
.story{border-radius:22px;overflow:hidden;position:relative;color:var(--story-ink);
  background:var(--story-grad);
  border:1px solid var(--story-line);box-shadow:var(--shadow-3)}
.story .story-pad{padding:22px}
.story .brandline{display:flex;align-items:center;gap:8px;font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--story-ink-2)}
.story .big{font-family:var(--font-display);font-weight:600;font-size:34px;letter-spacing:-.02em;line-height:1.02}
.story .goldbig{color:var(--story-gold)}
.story-watermark{position:absolute;right:-10px;bottom:-16px;opacity:.10;width:150px}

/* ---------- 19. Admin / Biz+ Console layout ------------------------- */
.admin{display:grid;grid-template-columns:248px 1fr;min-height:100vh;min-height:100dvh}
.admin .side{background:var(--surface);border-right:1px solid var(--line);display:flex;flex-direction:column;position:sticky;top:0;height:100vh;height:100dvh;overflow-y:auto}
.admin .side .brand{padding:18px 18px 12px;display:flex;align-items:center;gap:10px}
.admin .side nav{padding:6px 10px;flex:1}
.admin .side nav .navi{display:flex;align-items:center;gap:11px;padding:10px 12px;border-radius:11px;color:var(--ink-2);
  font-size:13.5px;font-weight:600;margin-bottom:2px;cursor:pointer;
  transition:background var(--t-fast) var(--ease-out),color var(--t-fast) var(--ease-out)}
.admin .side nav .navi:hover{background:var(--surface-2);color:var(--ink)}
.admin .side nav .navi.on{background:var(--brand-soft);color:var(--brand-ink)}
.admin .side nav .navi .ic{width:19px;height:19px;flex:none}
.admin .side .nav-group{font-size:10.5px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-3);padding:14px 14px 6px}
.admin .main{min-width:0;display:flex;flex-direction:column}
.admin .topbar{position:sticky;top:0;z-index:20;display:flex;align-items:center;gap:14px;padding:14px 24px;
  background:color-mix(in srgb,var(--bg) 86%,transparent);backdrop-filter:blur(12px);border-bottom:1px solid var(--line)}
.admin .content{padding:22px 24px 60px;max-width:1280px;width:100%;margin:0 auto}
.admin-mobile-note{display:none}
@media (max-width:860px){
  .admin{grid-template-columns:1fr}
  .admin .side{position:fixed;z-index:70;height:100dvh;width:262px;transform:translateX(-102%);
    transition:transform var(--t-base) var(--ease-in)}
  .admin .side.open{transform:translateX(0);box-shadow:var(--shadow-3);
    transition:transform var(--t-slow) var(--ease-out)}
  .admin .content{padding:16px}
}

/* data table */
.table{width:100%;border-collapse:collapse;font-size:13px}
.table th{text-align:left;font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--ink-3);
  padding:9px 12px;border-bottom:1px solid var(--line-2);position:sticky;top:0;background:var(--surface)}
.table td{padding:11px 12px;border-bottom:1px solid var(--line);vertical-align:middle}
.table tr:last-child td{border-bottom:0}
.table tr.tap{cursor:pointer;transition:background var(--t-tap) var(--ease-out)}
.table tr.tap:hover{background:var(--surface-2)}
.table td.n,.table th.n{text-align:right;font-variant-numeric:tabular-nums}
.table-wrap{overflow-x:auto;border:1px solid var(--line);border-radius:var(--r-2);background:var(--surface)}

/* ---------- 20. Landing ---------------------------------------------- */
.land{max-width:1120px;margin:0 auto;padding:0 20px}
.land-nav{position:sticky;top:0;z-index:40;display:flex;align-items:center;justify-content:space-between;
  padding:14px 20px;background:color-mix(in srgb,var(--bg) 82%,transparent);backdrop-filter:blur(12px);border-bottom:1px solid var(--line)}
.hero{padding:56px 0 32px;text-align:center}
.hero .kicker{display:inline-flex;align-items:center;gap:8px;padding:6px 14px;border-radius:999px;background:var(--brand-soft);color:var(--brand-ink);font-size:12px;font-weight:700;margin-bottom:20px}
.hero h1{font-size:clamp(32px,7vw,60px)}
.trilingual{font-family:var(--font-display);font-weight:600}
.feat-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:16px}
.portal-card{text-align:left;transition:transform var(--t-fast) var(--ease-out),border-color var(--t-fast) var(--ease-out)}
.portal-card:hover{transform:translateY(-3px);border-color:var(--brand)}

/* ---------- 20b. Reduced transparency --------------------------------- */
/* Four surfaces frost what is behind them: .tabbar (blur 16, over the list the
   user scrolls most), .land-nav and .admin .topbar (12), .scrim (2). A
   backdrop-filter over moving content is re-sampled every frame and there is no
   cheaper way to draw it, so the only honest saving is not to draw it for
   someone who has said they do not want it. Declared AFTER all four rules on
   purpose: same specificity, later declaration wins. Nobody's appearance
   changes here who did not ask for the change. */
@media (prefers-reduced-transparency: reduce){
  .tabbar{background:var(--surface);backdrop-filter:none}
  .land-nav,.admin .topbar{background:var(--bg);backdrop-filter:none}
  .scrim{backdrop-filter:none}
}

/* ---------- 20c. Containment ------------------------------------------
   `contain:` had zero hits in this codebase, which means nothing in the app has
   ever told the browser it may skip work. Now that the height cap above hands
   scrolling back to .scroll, the repeating list rows ARE the scroll cost, so
   this is where containment is worth having and where it is provably safe.

   The two levels are deliberately different, and the difference is clipping.

   ROWS get paint containment: the browser may skip painting a row's contents
   entirely once the row is outside .scroll. Safe because a row's children never
   leave it — probed across seven tabs, 66 boxes and ~1,355 descendants with
   every arrival animation finished: zero descendants extended past their row.
   An element's own outline, border and transform are NOT clipped by its own
   paint containment, so :focus-visible and :active stay exactly as drawn.

   CARDS get layout containment ONLY, no paint. A .card is where .a-stagger
   lives, and kg-rise starts its children 10px BELOW their final position: the
   same probe caught rows reaching up to 5.2px past their card's edge MID-RISE.
   Paint containment there would crop a row as it slid in — the arrival
   choreography is the thing this release exists to protect, so the card keeps
   painting freely and only its layout is isolated. Same reason .tabbar is
   absent: `.tabbar button.on::before` sits 8px ABOVE its button.

   `content-visibility:auto` is deliberately NOT here. It needs a real
   contain-intrinsic-size per card or the scroll height jumps, and these cards
   run from one KPI to a twelve-row account list; worse, a skipped subtree
   changes what a render is observably doing, and the arrival/refresh
   distinction is measured by exactly that. It wants its own change with its own
   measurement, not a rider on this one. */
.card{contain:layout}
.item,.tap-row{contain:layout paint}

/* ---------- 21. Animations -------------------------------------------
   There is ONE animation vocabulary and it lives in css/motion.css. What stood
   here was a second, parallel one, every entry of it dead: riseIn/.rise/
   .rise-1..6 (byte-identical to kg-rise + .a-stagger), countGlow/.win-glow,
   tallyDraw (never even referenced by a selector), floatY/.float, and
   .spark-burst — kit.js ui.winBurst builds those particles inline via the Web
   Animations API. Half a vocabulary that does nothing is how the next person
   concludes the system does not work and writes a third one. A new primitive
   goes in motion.css, where the reduced-motion block and .no-arrive can both
   reach it — neither of which was true of anything above.
   popIn stays: landing.js re-triggers .pop-in on the parser demo output. Being
   the one arrival-shaped class outside motion.css, it is now named in that
   file's .no-arrive suppressor list beside the rest of the vocabulary — nothing
   renders differently (landing is never softRefresh'd, so it cannot replay
   today) but the contract holds by construction instead of by a fact about
   landing.js. If it ever gains a caller inside the app, migrate that caller to
   .a-pop — popIn's near-twin, with the overshoot — and delete this pair. */
@keyframes popIn{from{opacity:0;transform:scale(.9)}to{opacity:1;transform:none}}
.pop-in{animation:popIn var(--t-slow) var(--ease-out) both}

/* ---------- 22. Utility ---------------------------------------------- */
.pill-note{background:var(--surface-2);border:1px solid var(--line);border-radius:12px;padding:10px 12px;font-size:12.5px;color:var(--ink-2)}
.callout{border-left:3px solid var(--brand);background:var(--brand-soft);border-radius:0 12px 12px 0;padding:12px 14px;font-size:13px}
.callout.gold{border-color:var(--gold);background:var(--gold-soft)}
.callout.danger{border-color:var(--danger);background:var(--danger-soft)}
.divider-t{display:flex;align-items:center;gap:10px;color:var(--ink-3);font-size:11px;font-weight:700;letter-spacing:.1em;text-transform:uppercase}
.divider-t::before,.divider-t::after{content:"";flex:1;height:1px;background:var(--line)}
.kbd{font-family:var(--font-mono);font-size:11px;background:var(--surface-3);border:1px solid var(--line-2);border-radius:6px;padding:1px 6px}
.link{color:var(--brand-ink);font-weight:600;cursor:pointer}
.link:hover{text-decoration:underline}
.flag{width:20px;height:14px;border-radius:3px;display:inline-block;vertical-align:-2px;overflow:hidden;flex:none}
.empty{text-align:center;padding:36px 16px;color:var(--ink-3)}
.empty .ic{width:44px;height:44px;margin:0 auto 12px;opacity:.5}
.scroll-x{overflow-x:auto;scrollbar-width:none}
.scroll-x::-webkit-scrollbar{display:none}
.nowrap{white-space:nowrap}
.clamp-2{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
