/* dopo swipe mode — mobile-first, game-like */

:root {
  --bg: #fafaf8;
  --card: #ffffff;
  --ink: #1f2430;
  --muted: #6b7280;
  --line: #e5e7eb;
  --accent: #2a6b64;
  --accent-bg: #f0f9f8;
  --accent-bright: #34d399;
  --accent-ink: #ffffff;
  --warn: #92400e;
  --warn-bg: #fffbeb;
  --in-green: #157f3d;
  --out-red: #b3261e;
  --park-gray: #4b5563;
  --dust: #b3a89a;
  --dust-shadow: rgba(31, 36, 48, .3);
  --shadow: 0 2px 6px rgba(31, 36, 48, .07), 0 12px 32px rgba(31, 36, 48, .12);
  --shadow-lift: 0 8px 18px rgba(31, 36, 48, .14), 0 28px 60px rgba(31, 36, 48, .22);
  --radius: 22px;
  --spring: cubic-bezier(.34, 1.56, .64, 1);
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #12151c;
    --card: #1c202a;
    --ink: #eceef2;
    --muted: #98a0ae;
    --line: #2c313d;
    --accent: #4cc2b4;
    --accent-bg: #163832;
    --accent-bright: #34d399;
    --accent-ink: #0c2b27;
    --warn: #f5b465;
    --warn-bg: #33270e;
    --in-green: #4ade80;
    --out-red: #f87171;
    --park-gray: #6b7280;
    --dust: #6d7686; /* dust catches light — a warm gray would vanish on dark */
    --dust-shadow: rgba(0, 0, 0, .55);
    --shadow: 0 2px 6px rgba(0, 0, 0, .35), 0 12px 32px rgba(0, 0, 0, .5);
    --shadow-lift: 0 8px 18px rgba(0, 0, 0, .45), 0 28px 60px rgba(0, 0, 0, .6);
  }
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
/* JS toggles visibility via the hidden property; author display rules must never beat it */
[hidden] { display: none !important; }

html, body { height: 100%; }

body {
  margin: 0;
  font: 15px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif;
  background: var(--bg);
  color: var(--ink);
  display: flex;
  flex-direction: column;
  height: 100dvh;
  overflow: hidden;
  padding: env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left);
  overscroll-behavior: none;
  user-select: none;
  -webkit-user-select: none;
  touch-action: manipulation; /* no double-tap zoom; pinch is cancelled in app.js (gesturestart) */
}

button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; touch-action: manipulation; }

/* ---------- header ---------- */

/* Above the cards (z 1-3) and opaque, so a card falling in from off-screen
   passes BEHIND the chrome instead of sweeping across the title. Cards only
   ever cross this band on their way down; at rest nothing overlaps, so the
   added background is invisible. */
header.top {
  display: flex;
  align-items: center;
  gap: 10px;
  /* the 4px that used to be .progress's top margin lives here instead: as
     margin it sat outside both backgrounds, leaving a full-width slit the
     falling card showed through for ~75ms. Same total spacing. */
  padding: 10px 14px 10px;
  position: relative;
  z-index: 4;
  background: var(--bg);
}

.brand {
  font-weight: 800;
  font-size: 18px;
  letter-spacing: .02em;
  color: var(--accent);
  margin-right: auto;
}

.streak {
  font-weight: 700;
  font-size: 15px;
  background: var(--warn-bg);
  color: var(--warn);
  border-radius: 999px;
  padding: 4px 12px;
  display: flex;
  align-items: center;
  gap: 3px;
}
.streak.pulse { animation: streak-pop .45s var(--spring); }
@keyframes streak-pop {
  0% { transform: scale(1); }
  40% { transform: scale(1.28) rotate(-4deg); }
  100% { transform: scale(1); }
}

.chip {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 5px 12px;
  font-size: 13px;
  font-weight: 600;
  box-shadow: 0 1px 3px rgba(31,36,48,.06);
}

.menu { position: relative; }
#menuBtn {
  font-size: 22px;
  line-height: 1;
  padding: 4px 10px;
  border-radius: 10px;
  color: var(--muted);
}
/* Popover API = top layer, which is above every stacking context. The old
   absolute + z-index:40 popup was trapped inside header.top's z-index:4 context,
   so .progress and .meter (also z-index:4, but later in DOM) painted over it. */
.menu-pop {
  position: fixed;
  inset: auto;
  margin: 0;
  /* Fallback while anchor positioning is unsupported (Firefox): sit under the
     header band, right-aligned to the button (14px from the viewport edge). */
  top: calc(env(safe-area-inset-top) + 46px);
  right: 14px;
  background: var(--card);
  color: var(--ink); /* UA [popover] forces CanvasText */
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: var(--shadow);
  padding: 6px;
  min-width: 170px;
  overflow: visible;
}
/* The UA hides [popover]:not(:popover-open) with display:none; an unconditional
   author display:flex would beat it and pin the menu open forever. */
.menu-pop:popover-open {
  display: flex;
  flex-direction: column;
}

@supports (anchor-name: --a) {
  #menuBtn { anchor-name: --menu-btn; }
  .menu-pop {
    position-anchor: --menu-btn;
    top: calc(anchor(bottom) + 6px);
    right: anchor(right);
  }
}
.menu-pop a, .menu-pop button {
  display: block;
  padding: 10px 14px;
  text-decoration: none;
  color: var(--ink);
  border-radius: 8px;
  font-size: 14px;
  text-align: left;
}
.menu-pop a:active, .menu-pop button:active { background: var(--accent-bg); }

.music-chip { color: var(--accent); }
.music-pop {
  position: fixed;
  inset: auto;
  margin: 0;
  /* Same fallback placement rules as .menu-pop: under the header band. */
  top: calc(env(safe-area-inset-top) + 46px);
  right: 14px;
  background: var(--card);
  color: var(--ink); /* UA [popover] forces CanvasText */
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: var(--shadow);
  padding: 12px;
  min-width: 210px;
  max-width: 280px;
}
.music-pop:popover-open {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.music-title {
  font-size: 14px;
  font-weight: 700;
  overflow-wrap: anywhere;
}
.music-author {
  font-size: 12px;
  color: var(--muted);
}
.music-source { color: var(--accent); text-decoration: none; }
.music-source::after { content: " ↗"; }
.music-controls {
  display: flex;
  gap: 6px;
  margin-top: 8px;
  flex-wrap: wrap;
}
.music-controls button {
  font-size: 12px;
  font-weight: 600;
  padding: 6px 9px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: none;
  color: var(--ink);
}
.music-controls button:active { background: var(--accent-bg); }

/* ---------- progress ---------- */

.progress {
  height: 8px;
  margin: 0 14px;
  background: var(--line);
  border-radius: 999px;
  overflow: hidden;
  position: relative;
  z-index: 4;
}
.progress-fill {
  height: 100%;
  width: 0%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--accent), var(--accent-bright));
  transition: width .5s var(--spring);
  transform-origin: left center;
}
.progress-fill.bump { animation: bar-bounce .45s var(--spring); }
@keyframes bar-bounce {
  40% { filter: brightness(1.5) saturate(1.4); }
}

.meter {
  font-size: 12px;
  color: var(--muted);
  text-align: center;
  padding: 5px 14px 0;
  font-variant-numeric: tabular-nums;
  position: relative;
  z-index: 4;
  background: var(--bg);
}

/* ---------- stage & stack ---------- */

.stage {
  flex: 1;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
  padding: 8px 0;
}

.stack {
  position: relative;
  width: min(88vw, 380px);
  height: min(100%, 480px);
}

.card {
  position: absolute;
  inset: 0;
  background: var(--card);
  border-radius: var(--radius);
  border: 1px solid var(--line);
  box-shadow: var(--shadow);
  display: flex;
  flex-direction: column;
  padding: 22px 20px 16px;
  touch-action: pan-y;
  will-change: transform;
  overflow: hidden;
  /* the card is a size container: the @container tiers below shrink the decorative parts
     (ring, emoji, group line, reasoning lines) on short viewports instead of letting the
     flex column crush the merchant / reasoning text into overlapping lines */
  container-type: size;
}
/* text rows never shrink — when a card is still too tall the LAST rows clip, nothing overlaps */
.card > .card-badge, .card > .merchant, .card > .amount, .card > .txn-date, .card > .reason,
.card > .held-line, .card > .evidence, .card > .ask-ai, .card > .hero { flex-shrink: 0; }

/* peek cards */
.card.c1 { transform: translateY(16px) scale(.94); opacity: .85; pointer-events: none; z-index: 2; }
.card.c2 { transform: translateY(30px) scale(.885); opacity: .5; pointer-events: none; z-index: 1; }
.card.c0 { z-index: 3; }
.card.c1, .card.c2 { transition: transform .4s var(--spring), opacity .3s ease; }

.card.unsure { border: 2px solid var(--warn); background: color-mix(in srgb, var(--warn-bg) 45%, var(--card)); }

.evidence {
  font-size: 13px;
  color: var(--muted);
  text-align: center;
  margin-top: 6px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.unsure-hint {
  margin-top: 6px;
  font-size: 13px;
  font-weight: 700;
  color: var(--warn);
  background: var(--warn-bg);
  border: 1px dashed var(--warn);
  border-radius: 999px;
  padding: 4px 12px;
}
.card.lifted {
  transition: transform .35s var(--spring), box-shadow .3s ease;
  transform: translateY(-18px) scale(1.03) !important;
  box-shadow: var(--shadow-lift);
}
/* The next card falls in from off-screen, exactly as a dealt card does —
   same keyframes, so there is one landing in the app rather than two curves
   to keep in sync. It replaces an earlier short "hop" from the peek position.
   The card that arrives at the BACK of the deck is still not animated: it is
   off-screen for most of any fall and then fully occluded by the two opaque
   cards in front of it, so animating it there is measurably invisible. Every
   card still falls from the top before you ever read it — it does so on the
   swipe that promotes it to the front. */
.card.landing { animation: deal-drop .52s cubic-bezier(.3, .9, .35, 1.18) both; }

/* color wash while dragging */
.wash {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  opacity: 0;
  pointer-events: none;
  z-index: 5;
}

/* stamps */
.stamp {
  position: absolute;
  top: 26px;
  font-size: 26px;
  font-weight: 900;
  letter-spacing: .06em;
  padding: 6px 14px;
  border: 4px solid;
  border-radius: 12px;
  opacity: 0;
  pointer-events: none;
  z-index: 6;
  background: var(--card);
}
.stamp-right { right: 18px; transform: rotate(9deg); color: var(--in-green); border-color: var(--in-green); }
.stamp-left { left: 18px; transform: rotate(-9deg); color: var(--park-gray); border-color: var(--park-gray); }

/* card content */
.card-badge {
  align-self: flex-start;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .08em;
  border-radius: 999px;
  padding: 3px 10px;
  margin-bottom: 6px;
}
.card-badge.unsure-badge { background: var(--warn-bg); color: var(--warn); }
.card-badge.rule-badge { background: var(--accent-bg); color: var(--accent); }
.card-badge.web-badge { background: #eaf2ff; color: #1d4ed8; }
.card-badge.lm-badge { background: #ecfdf3; color: #067647; }
.card-badge.disagree-badge { background: #fff1f2; color: #be123c; }
.held-line {
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
  text-align: center;
  margin-top: 4px;
}
.ask-ai {
  align-self: center;
  margin-top: 8px;
  min-height: 40px;
  padding: 8px 18px;
  border-radius: 999px;
  background: var(--accent-bg);
  color: var(--accent);
  font-size: 13px;
  font-weight: 700;
}
.ask-ai:active { filter: brightness(.95); }
.ask-ai[disabled] { opacity: .6; }
@media (prefers-color-scheme: dark) {
  .card-badge.web-badge { background: #1d2a45; color: #8ab4ff; }
  .card-badge.lm-badge { background: #12301f; color: #6ee7a0; }
}

.hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: 2px 0 6px;
}
.hero-ring {
  position: relative;
  width: 96px;
  height: 96px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.hero-ring svg { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); }
.hero-ring .ring-bg { stroke: var(--line); }
.hero-ring .ring-fg { stroke: var(--accent); stroke-linecap: round; }
.card.unsure .hero-ring .ring-fg { stroke: var(--warn); }
.hero-emoji { font-size: 44px; line-height: 1; filter: drop-shadow(0 3px 6px rgba(31,36,48,.15)); }

.cat-chip {
  margin-top: 8px;
  min-height: 44px;
  background: var(--accent-bg);
  color: var(--accent);
  font-weight: 700;
  font-size: 14px;
  border-radius: 999px;
  padding: 6px 16px;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.card.unsure .cat-chip { background: var(--warn-bg); color: var(--warn); }
.cat-chip:active { transform: scale(.96); }
.cat-group { font-size: 11px; color: var(--muted); margin-top: 3px; }

.merchant {
  font-size: 24px;
  font-weight: 800;
  text-align: center;
  margin-top: 8px;
  line-height: 1.2;
  overflow-wrap: anywhere;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.amount {
  font-size: 30px;
  font-weight: 800;
  text-align: center;
  font-variant-numeric: tabular-nums;
  margin-top: 2px;
}
.amount.in { color: var(--in-green); }
.amount.out { color: var(--out-red); }
.amount .cur { font-size: 16px; font-weight: 700; color: var(--muted); margin-left: 4px; }

.txn-date { text-align: center; font-size: 13px; color: var(--muted); margin-top: 2px; }

.reason {
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
  text-align: center;
  margin-top: 8px;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* ⓘ sits in the card's corner, the panel floats over the lower half: neither takes a row */
.details-toggle {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 4;
  min-width: 40px;
  min-height: 40px;
  color: var(--muted);
  font-size: 18px;
  line-height: 1;
  border-radius: 999px;
}
.details-toggle:active { background: var(--accent-bg); }
.details {
  display: none;
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: 12px;
  z-index: 5;
  font-size: 12px;
  color: var(--muted);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: var(--shadow);
  padding: 8px 10px;
  max-height: 55%;
  overflow-y: auto;
  overflow-wrap: anywhere;
  user-select: text;
  -webkit-user-select: text;
}
.card.details-open .details { display: block; }
.details b { color: var(--ink); }

/* short cards: shrink the hero first, then trim lines — merchant and amount stay full size longest.
   Container heights are the card's CONTENT box (outer height minus 38px padding + 2px border):
   the 480px phone stack is 440px here and gets the first tier; the 520px desktop stack does not. */
@container (max-height: 450px) {
  .hero { margin: 0 0 4px; }
  .hero-ring { width: 80px; height: 80px; }
  .hero-emoji { font-size: 36px; }
  .cat-group { display: none; }
  .merchant { font-size: 21px; }
  .reason { margin-top: 6px; }
  .evidence { margin-top: 4px; }
}
@container (max-height: 380px) {
  .card-badge { margin-bottom: 2px; }
  .hero { margin: 0 0 4px; }
  .hero-ring { width: 62px; height: 62px; }
  .hero-emoji { font-size: 28px; }
  .cat-chip { margin-top: 4px; min-height: 40px; font-size: 13px; }
  .merchant { font-size: 18px; margin-top: 4px; }
  .amount { font-size: 24px; }
  .reason { font-size: 14px; margin-top: 4px; -webkit-line-clamp: 2; }
  .evidence { -webkit-line-clamp: 1; }
  .unsure-hint { display: none; }
}

/* skeleton / empty */
.card.skeleton { align-items: center; justify-content: center; gap: 14px; border-style: dashed; }
.skel-emoji { font-size: 48px; animation: skel-breathe 1.4s ease-in-out infinite; }
@keyframes skel-breathe { 0%, 100% { opacity: .4; transform: scale(.96); } 50% { opacity: 1; transform: scale(1.05); } }
.skel-text { color: var(--muted); font-weight: 600; }
.skel-bar {
  width: 60%;
  height: 12px;
  border-radius: 8px;
  background: linear-gradient(90deg, var(--line) 25%, var(--bg) 50%, var(--line) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.2s linear infinite;
}
@keyframes shimmer { to { background-position: -200% 0; } }

.card.empty-card { align-items: center; justify-content: center; text-align: center; gap: 8px; }
.empty-emoji { font-size: 64px; }
.empty-title { font-size: 22px; font-weight: 800; }
.empty-sub { color: var(--muted); }
.empty-card button.retry {
  margin-top: 10px;
  background: var(--accent);
  color: var(--accent-ink);
  border-radius: 12px;
  padding: 10px 22px;
  font-weight: 700;
}

/* ---------- action bar (thumb zone) ---------- */

.actions {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 22px;
  padding: 8px 16px calc(14px + env(safe-area-inset-bottom));
}

.act {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  width: 64px;
  height: 64px;
  border-radius: 50%;
  background: var(--card);
  border: 1px solid var(--line);
  box-shadow: 0 3px 10px rgba(31,36,48,.1);
  font-size: 22px;
  transition: transform .15s var(--spring);
}
.act span { font-size: 10px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
.act:active { transform: scale(.88); }
.act:disabled { opacity: .4; }

.act-accept {
  width: 76px;
  height: 76px;
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
  font-size: 30px;
  font-weight: 800;
}
.act-accept span { color: var(--accent-ink); opacity: .85; }
.act-park { color: var(--park-gray); }

/* ---------- deal-in: heavy drop + dust ---------- */
.card.dealing { animation: deal-drop .52s cubic-bezier(.3, .9, .35, 1.18) both; }
.card.dealing.deal-1 { animation-delay: .07s; }
.card.dealing.deal-2 { animation-delay: .14s; }
/* Squash on contact at 62%: weight reads from the card deforming, not from
   the particles. The dust is garnish on top of this. */
@keyframes deal-drop {
  0% { transform: translateY(-120vh) rotate(5deg) scale(1.06); }
  62% { transform: translateY(0) rotate(0deg) scaleX(1.05) scaleY(.93); }
  74% { transform: translateY(-11px) scaleX(.985) scaleY(1.02); }
  86% { transform: translateY(0) scaleX(1.01) scaleY(.995); }
  100% { transform: translateY(0) scale(1); }
}
/* peek cards land on their resting transforms */
.card.dealing.deal-1 { animation-name: deal-drop-1; }
.card.dealing.deal-2 { animation-name: deal-drop-2; }
@keyframes deal-drop-1 {
  0% { transform: translateY(-120vh) rotate(-4deg) scale(1); }
  62% { transform: translateY(16px) scaleX(.96) scaleY(.90); }
  78% { transform: translateY(8px) scale(.94); }
  100% { transform: translateY(16px) scale(.94); }
}
@keyframes deal-drop-2 {
  0% { transform: translateY(-120vh) rotate(3deg) scale(.95); }
  62% { transform: translateY(30px) scaleX(.905) scaleY(.85); }
  80% { transform: translateY(24px) scale(.885); }
  100% { transform: translateY(30px) scale(.885); }
}
/* Impact dust — sprite sheet baked by scripts/gen-dust-sprite.ts (16 frames x
   2 rows). Rendered white with an alpha channel and used as a MASK, so --dust
   supplies the colour and one asset serves both themes.

   Everything here sits at z-index 0 inside .stage, i.e. UNDER the cards
   (z 1-3): the dust billows out from beneath the deck rather than washing
   across the card face. Only transform/opacity/mask-position animate, so the
   compositor does the work — see lib/dust.js for the tiering. */
.dust-cloud {
  position: absolute;
  z-index: 0;
  pointer-events: none;
  width: var(--sz);
  height: var(--sz);
  margin: calc(var(--sz) / -2);
  background: var(--dust);
  -webkit-mask-image: url("dust.png");
  mask-image: url("dust.png");
  -webkit-mask-size: var(--sheet-w) calc(var(--sz) * 2);
  mask-size: var(--sheet-w) calc(var(--sz) * 2);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  opacity: var(--op, 1);
  /* two animations, two curves: the sheet steps frame-to-frame while the
     transform eases — one element, no wrapper needed */
  animation: dust-sheet var(--dur) steps(16) both,
             dust-rise var(--dur) ease-out both;
}
@keyframes dust-sheet {
  from { -webkit-mask-position: 0 var(--row); mask-position: 0 var(--row); }
  to { -webkit-mask-position: calc(-1 * var(--sheet-w)) var(--row);
       mask-position: calc(-1 * var(--sheet-w)) var(--row); }
}
@keyframes dust-rise {
  from { transform: translate(0, 0) scale(var(--s0)); }
  to { transform: translate(var(--dx, 0), var(--dy)) scale(var(--s1)); }
}

/* particle sim target; sized to the deck's footprint, not the viewport */
.dust-canvas { position: absolute; z-index: 0; pointer-events: none; }

/* ---------- screen shake, for a genuinely big transaction ----------
   Applied to body's LAYOUT children, never to body itself: body carries the
   safe-area padding, and transforming it would make it the containing block
   for every position:fixed overlay — pushing toasts and sheets down by the
   notch inset on iOS. The fixed overlays are excluded deliberately; several
   carry their own transforms (.note is translateX(-50%)) which a shake would
   clobber, and chrome holding still while the deck jolts is what sells it.
   Mostly vertical, decaying: the deck was dropped, not nudged sideways. */
body.quaking > .top,
body.quaking > .progress,
body.quaking > .meter,
body.quaking > .stage,
body.quaking > .webbar,
body.quaking > .actions {
  animation: quake var(--quake-ms, 300ms) cubic-bezier(.36, .07, .19, .97) both;
}
@keyframes quake {
  0% { transform: translate3d(0, 0, 0); }
  14% { transform: translate3d(0, var(--quake-amp, 4px), 0); }
  30% { transform: translate3d(calc(var(--quake-amp, 4px) * .28), calc(var(--quake-amp, 4px) * -.62), 0); }
  46% { transform: translate3d(calc(var(--quake-amp, 4px) * -.22), calc(var(--quake-amp, 4px) * .44), 0); }
  62% { transform: translate3d(calc(var(--quake-amp, 4px) * .14), calc(var(--quake-amp, 4px) * -.28), 0); }
  78% { transform: translate3d(0, calc(var(--quake-amp, 4px) * .16), 0); }
  100% { transform: translate3d(0, 0, 0); }
}
@media (prefers-reduced-motion: reduce) {
  body.quaking > * { animation: none; }
}

/* contact shadow — the ground reacting is half of why the drop reads as heavy */
.dust-ground {
  position: absolute;
  z-index: 0;
  pointer-events: none;
  border-radius: 50%;
  background: radial-gradient(closest-side, var(--dust-shadow), transparent 72%);
  opacity: 0;
  animation: dust-ground .5s ease-out both;
}
@keyframes dust-ground {
  0% { opacity: 0; transform: translateX(-50%) scaleX(.55); }
  18% { opacity: 1; transform: translateX(-50%) scaleX(1.15); }
  100% { opacity: 0; transform: translateX(-50%) scaleX(1.35); }
}

/* ---------- toasts ---------- */

.note {
  position: fixed;
  left: 50%;
  bottom: calc(180px + env(safe-area-inset-bottom));
  transform: translateX(-50%);
  background: var(--ink);
  color: var(--bg);
  padding: 8px 16px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 600;
  z-index: 60;
  max-width: 90vw;
  text-align: center;
  animation: note-in .3s var(--spring);
}
@keyframes note-in { from { opacity: 0; transform: translate(-50%, 10px); } }

/* Top-anchored, under the header band: the thumb zone (action buttons) stays
   clear, so a fresh "Pick" tap never lands on the toast. Theme-native colours
   (the old inverted look read as "ignores dark mode"). */
.undo-toast {
  position: fixed;
  left: 50%;
  top: calc(56px + env(safe-area-inset-top));
  transform: translateX(-50%);
  width: min(92vw, 380px);
  background: var(--card);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 8px 8px 8px 16px;
  z-index: 68; /* above the celebration overlay: undo must dismiss it */
  box-shadow: var(--shadow-lift);
  animation: toast-down .3s var(--spring);
}
@keyframes toast-down { from { opacity: 0; transform: translate(-50%, -10px); } }
.undo-row { display: flex; align-items: center; gap: 10px; }
.undo-text {
  flex: 1;
  font-size: 13px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.undo-btn {
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 800;
  border-radius: 12px;
  padding: 12px 18px;
  font-size: 14px;
  flex-shrink: 0;
}
.undo-btn kbd { font: inherit; opacity: .7; }
.rule-chip {
  margin-top: 8px;
  width: 100%;
  text-align: left;
  background: var(--accent-bg);
  color: var(--accent);
  border-radius: 12px;
  padding: 10px 12px;
  font-size: 13px;
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rule-chip.saved { background: color-mix(in srgb, var(--accent) 28%, var(--card)); }

/* ---------- accounts filter ---------- */
.acct-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 8px;
  min-height: 44px;
  border-bottom: 1px solid var(--line);
  cursor: pointer;
}
.acct-row:last-child { border-bottom: 0; }
.acct-row input { width: 20px; height: 20px; accent-color: var(--accent); flex-shrink: 0; }
.acct-name { font-weight: 700; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acct-mask { color: var(--muted); font-weight: 500; }
.acct-sub { color: var(--muted); font-size: 12px; max-width: 90px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acct-count { background: var(--accent-bg); color: var(--accent); font-size: 12px; font-weight: 800; border-radius: 999px; padding: 2px 9px; }

/* ---------- bottom sheets ---------- */

.sheet-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(15, 18, 25, .45);
  z-index: 70;
  opacity: 1;
  transition: opacity .25s ease;
}
.sheet-backdrop.closing { opacity: 0; }

.sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  max-height: 78dvh;
  background: var(--card);
  border-radius: 22px 22px 0 0;
  z-index: 75;
  display: flex;
  flex-direction: column;
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
  box-shadow: 0 -10px 40px rgba(15,18,25,.25);
  transform: translateY(100%);
  transition: transform .34s var(--spring);
}
.sheet.open { transform: translateY(0); }

.grabber { width: 40px; height: 4px; border-radius: 4px; background: var(--line); margin: 8px auto 2px; flex-shrink: 0; }
.sheet-head { display: flex; align-items: center; padding: 6px 16px 8px; flex-shrink: 0; }
.sheet-title { font-weight: 800; font-size: 16px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sheet-close { font-size: 16px; color: var(--muted); min-width: 44px; min-height: 44px; border-radius: 10px; }
.sheet-body { overflow-y: auto; padding: 0 12px 16px; overscroll-behavior: contain; }

.sheet-section { font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); padding: 12px 8px 4px; }

/* Settings groups: native <details>, one per topic, so the sheet reads as a short
   menu instead of one long scroll. The open state is left to the user within a
   session; app.js only forces a group open when it has something to show there. */
.settings-group { border-bottom: 1px solid var(--line); padding-bottom: 4px; }
.settings-group:last-of-type { border-bottom: none; }
.settings-summary {
  list-style: none;
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 48px;
  padding: 8px;
  font-size: 15px;
  font-weight: 800;
  cursor: pointer;
  border-radius: 10px;
}
.settings-summary::-webkit-details-marker { display: none; }
.settings-summary::after {
  content: "›";
  margin-left: auto;
  font-size: 22px;
  font-weight: 400;
  color: var(--muted);
  transition: transform .15s ease;
}
.settings-group[open] > .settings-summary::after { transform: rotate(90deg); }
.settings-summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.settings-group > .sheet-section:first-of-type { padding-top: 4px; }
/* skip-tag search-ahead picker: picked chips, a search box, matches (or the deck's own tags) */
.tag-picker { display: flex; flex-direction: column; gap: 8px; padding: 6px 8px 8px; }
.tag-list { display: flex; flex-wrap: wrap; gap: 8px; }
.tag-list:empty { display: none; }
.cutoff-chip.tag-chip { flex: 0 1 auto; min-height: 40px; padding: 8px 12px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; } /* out-specifies .cutoff-chip's flex: 1 — chips size to their name */
.tag-chip .tag-n, .tag-chip .tag-x { font-weight: 400; color: var(--muted); margin-left: 6px; }
.tag-chip.on .tag-x { color: var(--accent); }
.tag-search {
  width: 100%;
  min-height: 44px;
  padding: 10px 14px;
  font: inherit;
  font-size: 16px; /* iOS zooms the page into any focused field under 16px */
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 12px;
  -webkit-appearance: none;
  appearance: none;
}
.tag-search:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
.tag-search:disabled { opacity: .5; }
.tag-empty { font-size: 12px; color: var(--muted); padding: 0 2px; }
.ob-toggle { padding: 6px 4px; min-height: 40px; }

.recent-row { display: flex; flex-wrap: wrap; gap: 8px; padding: 4px 4px 6px; }
.recent-row .cat-btn { flex: 0 0 auto; border-radius: 999px; padding: 8px 14px; }

.cat-btn {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  text-align: left;
  padding: 12px 12px;
  border-radius: 12px;
  font-size: 15px;
  font-weight: 600;
  background: var(--bg);
  border: 1px solid transparent;
  margin-top: 6px;
}
.cat-btn .cat-emoji { font-size: 20px; width: 26px; text-align: center; flex-shrink: 0; }
.cat-btn:active { background: var(--accent-bg); }
.cat-btn.guess { border-color: var(--accent); background: var(--accent-bg); color: var(--accent); }
.cat-btn.guess::after { content: "model guess"; margin-left: auto; font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: .06em; opacity: .7; }

.later-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 8px;
  border-bottom: 1px solid var(--line);
}
.later-item:last-child { border-bottom: none; }
.later-info { flex: 1; min-width: 0; }
.later-merchant { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.later-sub { font-size: 12px; color: var(--muted); }
.unpark-btn { background: var(--accent-bg); color: var(--accent); font-weight: 700; border-radius: 10px; padding: 8px 14px; flex-shrink: 0; }
.later-empty { text-align: center; color: var(--muted); padding: 30px 10px; }

/* ---------- celebration ---------- */

.celebrate {
  position: fixed;
  inset: 0;
  z-index: 65;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(15, 18, 25, .25);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}
.celebrate-card {
  background: var(--card);
  border-radius: 24px;
  padding: 28px 32px;
  text-align: center;
  box-shadow: var(--shadow-lift);
  animation: celebrate-in .5s var(--spring);
  max-width: 84vw;
}
@keyframes celebrate-in {
  from { transform: scale(.6) translateY(30px); opacity: 0; }
  to { transform: scale(1) translateY(0); opacity: 1; }
}
.celebrate-emoji { font-size: 64px; animation: celebrate-spin .7s var(--spring); }
@keyframes celebrate-spin {
  from { transform: scale(0) rotate(-30deg); }
  to { transform: scale(1) rotate(0); }
}
.celebrate-title { font-size: 24px; font-weight: 800; margin-top: 8px; }
.celebrate-sub { color: var(--muted); margin-top: 4px; }
.celebrate-btn {
  margin-top: 18px;
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 800;
  border-radius: 14px;
  padding: 12px 28px;
  font-size: 16px;
}

/* ---------- settings sheet ---------- */

.field { padding: 6px 8px 4px; }
.field-label { display: block; font-size: 13px; font-weight: 700; margin-bottom: 6px; }
.field-input {
  width: 100%;
  font: inherit;
  font-size: 16px; /* iOS zooms the page into any focused field under 16px */
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 12px 14px;
  min-height: 44px;
  user-select: text;
  -webkit-user-select: text;
}
.field-input::placeholder { color: var(--muted); }
.field-input:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
.field-input.invalid { border-color: var(--out-red); }
.field-input.invalid:focus { outline-color: var(--out-red); }
.field-hint { font-size: 12px; font-weight: 600; color: var(--accent); margin-top: 4px; }
.field-error { font-size: 12px; font-weight: 700; color: var(--out-red); margin-top: 4px; padding: 0 8px; }
.field .field-error { padding: 0; }

.settings-info { font-size: 13px; color: var(--muted); padding: 6px 8px 0; }
.settings-info b { color: var(--ink); }

.cutoff-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 8px;
}
.cutoff-chip {
  flex: 1 1 auto;
  min-height: 44px;
  padding: 10px 14px;
  font-weight: 700;
  font-size: 14px;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 12px;
  white-space: nowrap;
}
.cutoff-chip.on {
  border-color: var(--accent);
  background: var(--accent-bg);
  color: var(--accent);
}

.rule-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 8px;
  border-bottom: 1px solid var(--line);
}
.rule-row:last-child { border-bottom: none; }
.rule-info { flex: 1; min-width: 0; }
.rule-pattern { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rule-cat { font-size: 12px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rule-del {
  flex-shrink: 0;
  min-height: 44px;
  padding: 8px 14px;
  font-weight: 700;
  color: var(--out-red);
  border-radius: 10px;
}

.toggle-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 8px;
  min-height: 44px;
  cursor: pointer;
}
.toggle-row input { width: 20px; height: 20px; accent-color: var(--accent); flex-shrink: 0; margin-top: 1px; }
.toggle-text { font-weight: 700; }
.toggle-sub { display: block; font-size: 12px; font-weight: 500; color: var(--muted); margin-top: 2px; }

.save-btn {
  display: block;
  width: calc(100% - 16px);
  margin: 14px 8px 8px;
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 800;
  font-size: 16px;
  border-radius: 14px;
  padding: 14px;
}
.save-btn:disabled { opacity: .6; }

/* ---------- onboarding wizard ---------- */

/* Phones: sized to the VISUAL viewport (--vv-h/--vv-top, kept current by app.js),
   not 100dvh — iOS does not shrink the layout viewport for the keyboard, so a
   dvh-tall dialog kept its footer (Continue) under the keys. */
dialog.onboard {
  margin: 0;
  border: 0;
  padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
  background: var(--card);
  color: var(--ink);
  top: var(--vv-top, 0px);
  width: 100dvw;
  height: var(--vv-h, 100dvh);
  max-width: 100dvw;
  max-height: var(--vv-h, 100dvh);
  border-radius: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
dialog.onboard::backdrop { background: var(--bg); }
dialog.onboard:not([open]) { display: none; }

@media (min-width: 600px) {
  dialog.onboard {
    width: 100%;
    max-width: 420px;
    top: 0;
    height: auto;
    max-height: min(88dvh, 640px);
    margin: auto;
    padding: 0;
    border-radius: var(--radius);
    box-shadow: var(--shadow-lift);
  }
}

.onboard-progress {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 16px 20px 6px;
  flex-shrink: 0;
}
.onboard-progress #obStepLabel {
  font-size: 12px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
}
.onboard-dots { display: flex; gap: 6px; }
.onboard-dots .dot { width: 7px; height: 7px; border-radius: 999px; background: var(--line); }
.onboard-dots .dot.on { background: var(--accent); }

.onboard-body {
  flex: 1;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 8px 20px 20px;
}

.ob-title { font-size: 21px; font-weight: 800; margin: 4px 0 10px; outline: none; }
.onboard-step p { color: var(--muted); margin: 0 0 10px; line-height: 1.5; }
.ob-warn { color: var(--warn); font-weight: 600; font-size: 13px; }

.ob-steps {
  color: var(--muted);
  font-size: 14px;
  margin: 0 0 14px;
  padding-left: 22px;
}
.ob-steps li { margin-bottom: 6px; }
.ob-steps a { color: var(--accent); font-weight: 700; text-decoration: none; }
.ob-steps a:hover { text-decoration: underline; }

#obAiGroup { margin-bottom: 6px; }

/* radio cards: input is visually hidden but stays focusable, so the label
   is the whole hit target and :has()/:focus-within drive the highlight */
.ob-choice {
  display: block;
  position: relative;
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 12px 14px;
  margin-bottom: 10px;
  cursor: pointer;
}
.ob-choice:has(input:checked) { border-color: var(--accent); background: var(--accent-bg); }
.ob-choice:focus-within { outline: 2px solid var(--accent); outline-offset: 1px; }
.ob-choice input[type="radio"] {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.ob-choice-title { font-weight: 800; }
.ob-choice-lead { font-size: 13px; color: var(--muted); margin-top: 2px; }
.ob-choice-bullets { margin: 6px 0 0; padding-left: 18px; font-size: 12px; color: var(--muted); }
.ob-choice-bullets li { margin-bottom: 2px; }

.field-input-row { display: flex; gap: 8px; align-items: center; }
.field-input-row .field-input { flex: 1; }
.field-show {
  flex-shrink: 0;
  min-height: 44px;
  padding: 0 14px;
  border-radius: 12px;
  border: 1px solid var(--line);
  font-weight: 700;
  font-size: 13px;
  color: var(--ink);
  background: var(--bg);
}
.field-show[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); background: var(--accent-bg); }

.ob-gestures { list-style: none; margin: 0 0 16px; padding: 0; }
.ob-gestures li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--line);
  font-size: 14px;
  color: var(--ink);
}
.ob-gestures li:last-child { border-bottom: none; }
.ob-gesture-emoji { font-size: 18px; width: 24px; text-align: center; flex-shrink: 0; }
.ob-gestures kbd {
  font: inherit;
  font-size: 11px;
  font-weight: 800;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 2px 6px;
}

.ob-subtitle { font-size: 15px; font-weight: 800; margin: 14px 0 4px; }
.ob-sub { font-size: 13px; color: var(--muted); margin-top: 4px; }
.onboard-step .toggle-row { padding: 6px 0; } /* tighter than Settings: the legend shares the screen */
.onboard-step .ob-gestures { margin-bottom: 8px; }

.onboard-footer {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  padding: 10px 20px calc(14px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--line);
  flex-shrink: 0;
}
.ob-back {
  color: var(--muted);
  font-weight: 700;
  padding: 10px 4px;
  min-height: 44px;
}
#obNote {
  flex: 1 1 100%;
  order: -1;
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
  margin: 0 0 4px;
}
#obNote:empty { display: none; }
.ob-secondary {
  color: var(--muted);
  font-weight: 700;
  background: none;
  padding: 10px 14px;
  min-height: 44px;
  margin-left: auto;
}
.ob-primary {
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 800;
  font-size: 16px;
  border-radius: 14px;
  padding: 14px 24px;
  min-height: 44px;
  flex: 1 1 auto;
  min-width: 140px;
  margin-left: auto;
}
.ob-primary:disabled { opacity: .6; }
.ob-back:not([hidden]) ~ .ob-primary,
.ob-back:not([hidden]) ~ .ob-secondary ~ .ob-primary { margin-left: 0; }

/* transition when switching panels; killed under reduced motion */
@keyframes ob-enter {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}
.ob-enter { animation: ob-enter .28s var(--spring); }
@media (prefers-reduced-motion: reduce) {
  .ob-enter { animation: none; }
}

/* ---------- "New version" update toast ---------- */

.update-toast {
  position: fixed;
  top: calc(10px + env(safe-area-inset-top));
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 14px;
  background: var(--ink);
  color: var(--bg);
  border-radius: 999px;
  padding: 8px 8px 8px 18px;
  font-size: 13px;
  font-weight: 600;
  z-index: 66; /* under sheet backdrop (70): a late toast never covers an open sheet */
  box-shadow: var(--shadow-lift);
  animation: note-in .3s var(--spring);
}
.update-btn {
  background: rgba(255, 255, 255, .16);
  color: inherit;
  font-weight: 800;
  border-radius: 999px;
  padding: 10px 16px;
  font-size: 13px;
  flex-shrink: 0;
}
.update-btn:disabled { opacity: .6; }
@media (prefers-color-scheme: dark) {
  .update-toast { background: #f1f2f5; color: #1f2430; }
  .update-btn { background: rgba(31, 36, 48, .1); }
}

/* ---------- confetti ---------- */

#confetti {
  position: fixed;
  inset: 0;
  z-index: 80;
  pointer-events: none;
  display: none;
}
#confetti.on { display: block; }

/* ---------- flash pulse (reduced-motion celebration) ---------- */

.flash {
  position: fixed;
  inset: 0;
  z-index: 62;
  pointer-events: none;
  background: radial-gradient(circle at 50% 45%, rgba(52,211,153,.3), transparent 60%);
  animation: flash-fade .9s ease forwards;
}
@keyframes flash-fade { from { opacity: 1; } to { opacity: 0; } }

/* ---------- reduced motion ---------- */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .12s !important;
  }
  .flash { animation-duration: .9s !important; }
  .skel-emoji, .skel-bar { animation: none !important; }
  .card.c1, .card.c2 { transition: none; }
}

/* larger screens: keep it centered and phone-shaped (height-gated so landscape phones don't clip) */
@media (min-width: 720px) and (min-height: 700px) {
  .stack { width: 400px; height: 520px; }
}

/* short viewports (landscape phones): compact card so nothing clips */
@media (max-height: 520px) {
  .card { padding: 10px 16px 6px; }
  .hero { margin: 0; }
  .hero-ring { width: 48px; height: 48px; }
  .hero-emoji { font-size: 24px; }
  .cat-chip { margin-top: 2px; font-size: 12px; padding: 4px 12px; }
  .cat-group { display: none; }
  .merchant { font-size: 16px; -webkit-line-clamp: 1; margin-top: 2px; }
  .amount { font-size: 18px; }
  .reason { display: none; }
  .evidence { display: none; }
  .unsure-hint { display: none; }
  .details-toggle { display: none; }
  .card-badge { display: none; }
}

/* ---------- static-build additions ---------- */

/* token-field footnotes (links to the LM/OR token pages) */
.field-sub { font-size: 12px; color: var(--muted); margin-top: 4px; }
.field-sub a { color: var(--accent); font-weight: 700; text-decoration: none; }
.field-sub a:hover { text-decoration: underline; }

/* "Forget tokens on this device" */
.forget-btn {
  display: block;
  width: 100%;
  margin-top: 10px;
  background: none;
  border: 1px solid var(--line);
  color: var(--out-red);
  font-weight: 700;
  border-radius: 14px;
  padding: 12px;
  font-size: 14px;
}

/* armed = one more tap actually clears them */
.forget-btn.armed {
  border-color: var(--out-red);
  background: color-mix(in srgb, var(--out-red) 12%, transparent);
}

/* floating web-check / OpenRouter-hint bar above the action buttons */
.webbar {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(112px + env(safe-area-inset-bottom));
  z-index: 30;
  max-width: min(92vw, 440px);
}
.webbar button {
  background: var(--card);
  color: var(--accent);
  border: 1px solid var(--line);
  box-shadow: var(--shadow);
  font-weight: 700;
  font-size: 13px;
  border-radius: 999px;
  padding: 10px 18px;
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.webbar[data-mode="webcheck"] button { color: #1d4ed8; }
@media (prefers-color-scheme: dark) {
  .webbar[data-mode="webcheck"] button { color: #8ab4ff; }
}

/* ---------- offline / sync status (S6) ---------- */

/* header chip: persistent "Offline · N queued" (aria-live polite in markup) */
.conn-chip {
  color: var(--warn);
  background: var(--warn-bg);
  border-color: color-mix(in srgb, var(--warn) 30%, transparent);
  white-space: nowrap;
}

/* snapshot-mode banner under the meter: "Showing data from <age>" */
.stale-banner {
  margin: 6px auto 0;
  width: fit-content;
  max-width: 92vw;
  font-size: 12px;
  font-weight: 600;
  color: var(--warn);
  background: var(--warn-bg);
  border: 1px solid color-mix(in srgb, var(--warn) 25%, transparent);
  border-radius: 999px;
  padding: 4px 12px;
  text-align: center;
}

/* poison-item surface: "N changes couldn't sync — tap to re-sort / discard" */
.stuck-banner {
  display: block;
  margin: 6px auto 0;
  width: fit-content;
  max-width: 92vw;
  font-size: 12px;
  font-weight: 700;
  color: var(--out-red);
  background: color-mix(in srgb, var(--out-red) 10%, var(--card));
  border: 1px solid color-mix(in srgb, var(--out-red) 35%, transparent);
  border-radius: 999px;
  padding: 6px 14px;
  text-align: center;
}
.stuck-banner:active { transform: scale(.97); }

/* free-tier quota surface: "Free AI quota used up — tap to add your own key" */
.upgrade-banner {
  display: block;
  margin: 6px auto 0;
  width: fit-content;
  max-width: 92vw;
  font-size: 12px;
  font-weight: 700;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, var(--card));
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  border-radius: 999px;
  padding: 6px 14px;
  text-align: center;
}
.upgrade-banner:active { transform: scale(.97); }

/* "Add to Home Screen" hint — browser tabs only, once per device */
.install-banner {
  display: flex;
  align-items: center;
  margin: 6px auto 0;
  width: fit-content;
  max-width: 92vw;
  font-size: 12px;
  font-weight: 700;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, var(--card));
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  border-radius: 999px;
  padding: 0 2px 0 6px;
}
.install-banner button { padding: 8px 8px; min-height: 36px; }
.install-banner button:active { transform: scale(.97); }
#installDismiss { padding: 8px 12px; opacity: .7; }
.install-pop {
  position: fixed;
  inset: 0;
  margin: auto;
  width: min(92vw, 360px);
  height: fit-content;
  background: var(--card);
  color: var(--ink); /* UA [popover] forces CanvasText */
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lift);
  padding: 18px 20px 16px;
}
.install-pop::backdrop { background: rgba(0, 0, 0, .35); }
.install-title { font-size: 17px; font-weight: 800; margin-bottom: 10px; }
.install-pop .ob-steps { margin-bottom: 8px; }
.install-pop b { color: var(--ink); }
.install-ok {
  display: block;
  width: 100%;
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 800;
  border-radius: 14px;
  padding: 12px;
  min-height: 44px;
}

/* ==========================================================================
   fast category pickers (pk-*) — engine: public/lib/pickerui.js
   Ported from the picker lab. Rules of this section:
     - every transition/animation is <= 150ms (the whole point of the feature);
     - tile colour is per-category hue in a custom property, set from JS with
       style.setProperty("--h", …) because CSP forbids style="" in markup;
     - OKLCH lightness 0.53 light / 0.50 dark at chroma 0.13 keeps white text
       above 4.5:1 on every hue — no hsl() fallback (every target supports
       oklch, and the fallback was a contrast regression);
     - nothing here may scroll: the engine's fits() enforces it and the glue
       falls back to the list picker when it returns false.
   ========================================================================== */

/* ---------- sheet host: faster slide, taller box, no scrolling ---------- */

.sheet.pick-fast { transition-duration: .14s; max-height: 92dvh; }
body.pick-fast-open .sheet-backdrop,
.sheet-backdrop.pick-fast { transition-duration: .14s; }

.sheet.pick-tiles .sheet-body,
.sheet.pick-cols .sheet-body,
.sheet.pick-dock .sheet-body,
.sheet.pick-wheel .sheet-body {
  overflow: hidden;
  overscroll-behavior: none;
  padding: 0 10px 12px;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}
.sheet.pick-tiles .sheet-body,
.sheet.pick-cols .sheet-body { height: min(70dvh, 620px); }
.sheet.pick-wheel .sheet-body { height: min(96vw, 62dvh); }
.sheet.pick-dock .sheet-body { height: auto; }

/* ---------- engine roots ---------- */

.pk-root { position: relative; }
.pk { height: 100%; min-height: 0; }

.pk-tiles,
.pk-right,
.pk-dock-sub {
  display: grid;
  grid-template-columns: repeat(var(--cols, 3), 1fr);
  gap: 6px;
}
.pk-tiles,
.pk-right { grid-auto-rows: 1fr; }

/* ---------- the tile: one control, every variant reuses it ---------- */

.pk-tile {
  --h: 0;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-width: 0;
  min-height: 0;
  padding: 4px 6px;
  border: 0;
  border-radius: 14px;
  overflow: hidden;
  background: oklch(0.53 0.13 var(--h));
  color: #fff;
  text-shadow: 0 1px 1px rgba(0, 0, 0, .25);
  transition: transform 70ms ease-out, filter 70ms ease-out, opacity 120ms ease-out;
}
@media (prefers-color-scheme: dark) {
  .pk-tile { background: oklch(0.50 0.13 var(--h)); }
}

.pk-emoji { font-size: clamp(20px, 6vmin, 34px); line-height: 1; }
/* no emoji on the category → two-letter monogram, so the tile still has a glyph */
.pk-emoji.pk-mono { font-size: clamp(15px, 4.6vmin, 26px); font-weight: 800; letter-spacing: .01em; opacity: .92; }
.pk-name {
  font-size: clamp(11px, 3vmin, 15px);
  font-weight: 600;
  line-height: 1.15;
  text-align: center;
  max-width: 100%;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: break-word;
  hyphens: auto;
}
/* the chevron lives inside the name so it disappears with it on dock circles,
   and so it cannot collide with the ::after "recently used" dot */
.pk-group .pk-name::after { content: "\A0›"; opacity: .85; }

.pk-hot {
  position: absolute;
  top: 4px;
  right: 5px;
  padding: 2px 4px;
  border-radius: 5px;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  background: rgba(0, 0, 0, .28);
  color: #fff;
  text-shadow: none;
  opacity: .9;
}
/* touch-only devices have no hotkeys to hint at (hides the wheel's <text> too) */
@media (hover: none) and (pointer: coarse) {
  .pk-hot { display: none; }
}

.pk-tile.pk-guess::before { content: "★"; position: absolute; top: 3px; left: 5px; font-size: 11px; }
.pk-tile.pk-recent::after {
  content: "";
  position: absolute;
  bottom: 5px;
  left: 6px;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: rgba(255, 255, 255, .85);
}

.pk-tile.pk-back {
  background: transparent;
  color: var(--ink);
  text-shadow: none;
  border: 1.5px dashed var(--line);
}
.pk-tile.pk-back .pk-hot { background: var(--line); color: var(--ink); }

.pk-tile.pk-sel { outline: 3px solid var(--ink); outline-offset: -3px; }
.pk-tile.pk-dim { opacity: .45; }

/* ---------- press / commit feedback ---------- */

.pk-press { transform: scale(.94); filter: brightness(1.12); transition-duration: 40ms; }
.pk-tile.pk-hit { filter: brightness(1.28); transform: scale(1.03); z-index: 2; }
.pk-tile.pk-fade { opacity: .35; }

/* level swap: the new level pops in once, 120ms */
@keyframes pk-pop { from { opacity: 0; transform: translateY(6px) scale(.985); } to { opacity: 1; transform: none; } }
.pk-swap { animation: pk-pop .12s ease-out; }

/* ---------- Columns ---------- */

.pk-cols {
  display: grid;
  grid-template-columns: minmax(112px, 36%) 1fr;
  gap: 8px;
}
.pk-left { display: flex; flex-direction: column; gap: 5px; min-height: 0; }
.pk-left .pk-tile {
  flex: 1;
  flex-direction: row;
  justify-content: flex-start;
  gap: 8px;
  padding: 4px 8px;
  border-radius: 11px;
}
.pk-left .pk-emoji { font-size: clamp(18px, 4.6vmin, 26px); }
.pk-left .pk-emoji.pk-mono { font-size: 14px; }
.pk-left .pk-name { text-align: left; }
.pk-right { min-height: 0; }
.pk-hint {
  grid-column: 1 / -1;
  display: grid;
  place-items: center;
  padding: 20px;
  color: var(--muted);
  font-size: 14px;
  text-align: center;
}

/* ---------- Dock ---------- */

.pk-dock { display: flex; flex-direction: column; justify-content: flex-end; gap: 8px; }
.pk-dock-sub {
  padding: 8px;
  border-radius: 16px;
  background: var(--bg);
  box-shadow: 0 -2px 10px rgba(0, 0, 0, .08);
  animation: pk-rise .12s ease-out;
}
@keyframes pk-rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.pk-dock-sub .pk-tile {
  min-height: 46px;
  flex-direction: row;
  gap: 6px;
  padding: 4px 8px;
  border-radius: 11px;
}
.pk-dock-sub .pk-emoji { font-size: 20px; }
.pk-dock-sub .pk-emoji.pk-mono { font-size: 14px; }
.pk-dock-sub .pk-name { text-align: left; font-size: 12px; }
/* long names in the dock sub-row: one line with an ellipsis, clear of the hotkey badge */
.pk-dock-sub .pk-name { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; -webkit-line-clamp: unset; }
.pk-dock-sub .pk-tile { padding-right: 20px; }

.pk-dock-row { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; }
.pk-dock-row .pk-tile {
  width: var(--ds, 56px);
  height: var(--ds, 56px);
  border-radius: 50%;
  padding: 0;
}
.pk-dock-row .pk-emoji { font-size: calc(var(--ds, 56px) * .5); }
.pk-dock-row .pk-emoji.pk-mono { font-size: calc(var(--ds, 56px) * .34); }
.pk-dock-row .pk-name { display: none; }
.pk-dock-row .pk-hot { top: -3px; right: -3px; }
.pk-dock-row .pk-tile.pk-sel { outline: 3px solid var(--ink); outline-offset: 2px; }

/* ---------- Wheel ---------- */

/* At rest the inner ring is the whole disc (R0..98 in viewBox units). Opening
   a group puts `pk-open` on the <svg>: the ring shrinks to 56/98 of its size
   over 120ms and the fan is drawn in the space it leaves. The paths are never
   rebuilt — pickerui.js redrawWheel() toggles the class and swaps the fan only,
   so the pointer capture on the <svg> survives the level change. The scale
   factor here is picker.js wheelGeometry().innerScale; keep them equal. */

/* explicit 1fr row: an auto row would size to the SVG's 1:1 intrinsic height
   (= its width) and overflow a box that is wider than it is tall */
.pk-wheel { display: grid; grid-template: minmax(0, 1fr) / minmax(0, 1fr); place-items: center; height: 100%; width: 100%; min-height: 0; }
.pk-wheel svg {
  display: block;
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  min-width: 0;
  min-height: 0;
  overflow: visible;
  touch-action: none;
  /* default only — the builder's font-size presentation attributes win over
     an inherited value, so per-wedge sizing still applies */
  font: 700 9px/1 system-ui, -apple-system, sans-serif;
}
/* transform-origin 0 0 is user space (0,0) = the wheel's centre for the
   viewBox "-102 -102 204 204", so the ring shrinks in place around the hub */
g.pk-inner { transform-origin: 0 0; transition: transform 120ms ease-out; }
svg.pk-open g.pk-inner { transform: scale(.571); }
@media (prefers-reduced-motion: reduce) {
  g.pk-inner { transition: none; }
}
/* 4.2 units scale to about 4px — unreadable; the hotkeys themselves still work */
svg.pk-open g.pk-inner text.pk-hot { display: none; }
.pk-wedge {
  --h: 0;
  fill: oklch(0.53 0.13 var(--h));
  stroke: var(--card);
  stroke-width: 1.2;
  transition: filter 70ms ease-out, opacity 120ms ease-out;
}
@media (prefers-color-scheme: dark) {
  .pk-wedge { fill: oklch(0.50 0.13 var(--h)); }
}
.pk-wedge.pk-sel { filter: brightness(1.3); stroke: var(--ink); stroke-width: 1.8; }
.pk-wedge.pk-guess { stroke: #fff; stroke-width: 2; }
/* white dot = used recently, same marker as the tiles (a dashed wedge edge read
   as a rendering bug, which is exactly what it looked like) */
.pk-dot { fill: rgba(255, 255, 255, .9); pointer-events: none; }
.pk-wedge.pk-dim { opacity: .35; }
.pk-wedge.pk-press { filter: brightness(1.3); }
.pk-wedge.pk-hit { filter: brightness(1.45); }
.pk-wedge.pk-fade { opacity: .3; }
/* siblings step back so the finger's wedge is unmistakable — a brightness bump
   alone is invisible between children two hue degrees apart */
.pk-wedge.pk-dull { opacity: .65; /* keep the colour coding legible while dragging */ }
/* AFTER pk-dim/pk-dull: a hovered top-level leaf carries pk-dim from the open
   fan, and a dimmed "highlight" is no highlight at all */
.pk-wedge.pk-hov {
  opacity: 1;
  filter: brightness(1.5) saturate(1.15);
  stroke: #fff;
  stroke-width: 2.4;
}
/* stroke compensation: the shrunken ring's strokes scale with it, so the
   widths above are divided by .571 to hold their on-screen weight. Same order
   as the base rules — later wins, hov last. */
svg.pk-open g.pk-inner .pk-wedge { stroke-width: 2.1; }
svg.pk-open g.pk-inner .pk-wedge.pk-guess { stroke-width: 3.5; }
svg.pk-open g.pk-inner .pk-wedge.pk-sel { stroke-width: 3.15; }
svg.pk-open g.pk-inner .pk-wedge.pk-hov { stroke-width: 4.2; }
g.pk-outer.pk-swap { animation: pk-fade-in .12s ease-out; }
@keyframes pk-fade-in { from { opacity: 0; } to { opacity: 1; } }

.pk-glyph {
  fill: #fff;
  text-anchor: middle;
  dominant-baseline: central;
  pointer-events: none;
}
.pk-glyph.pk-mono { font-weight: 800; }
/* mirrors the tiles' ::before star — the guess has to be findable on the wheel too */
.pk-star { fill: #fff; text-anchor: middle; dominant-baseline: central; pointer-events: none; opacity: .9; }
.pk-wheel text.pk-hot {
  position: static;
  padding: 0;
  background: none;
  border-radius: 0;
  fill: rgba(255, 255, 255, .85);
  font-size: 4.2px;
  text-anchor: middle;
  dominant-baseline: central;
  pointer-events: none;
}
.pk-center { --h: 0; fill: var(--card); stroke: var(--line); transition: fill 70ms ease-out; }
/* the hub wears the hovered wedge's own hue: the lens can be clipped off the
   top of a short panel, the hub never is */
.pk-center.pk-lit { fill: oklch(0.9 0.09 var(--h)); stroke: oklch(0.62 0.14 var(--h)); }
@media (prefers-color-scheme: dark) {
  .pk-center.pk-lit { fill: oklch(0.32 0.08 var(--h)); stroke: oklch(0.55 0.13 var(--h)); }
}
.pk-c1,
.pk-c2 { text-anchor: middle; dominant-baseline: central; pointer-events: none; }
.pk-c1 { fill: var(--ink); font-size: 6px; font-weight: 650; }
.pk-c2 { fill: var(--muted); font-size: 4.5px; font-weight: 500; }

/* ---------- lens: the chip above the finger ---------- */

/* The finger sits ON the wedge it is picking. This chip repeats it, in the
   wedge's own colour, a thumb-pad's height further up where nothing covers it.

   Where the Popover API exists the chip is a popover="hint" and paints in the
   top layer: above the sheet body's overflow:hidden and every stacking context,
   positioned fixed from --lx/--ly in VIEWPORT px. A hint popover light-dismisses
   on any outside pointerup or Escape, so pickerui.js re-arms it on every move.
   This base rule is the fallback (no API): absolute inside the picker root,
   root-relative px, z-index 4. */
.pk-lens {
  --h: 0;
  --lx: 0px;
  --ly: 0px;
  position: absolute;
  left: 0;
  top: 0;
  z-index: 4;
  display: flex;
  align-items: center;
  gap: 8px;
  max-width: 92vw;
  padding: 9px 16px;
  border-radius: 999px;
  background: oklch(0.53 0.13 var(--h));
  box-shadow: 0 6px 18px rgba(0, 0, 0, .28);
  color: #fff;
  font-size: 18px;
  font-weight: 800;
  white-space: nowrap;
  pointer-events: none;
  transform: translate(calc(var(--lx) - 50%), calc(var(--ly) - 50%));
}
@media (prefers-color-scheme: dark) {
  .pk-lens { background: oklch(0.5 0.13 var(--h)); }
}
/* Kept as SEPARATE rules, never in one selector list with the base rule: Safari
   16 drops a whole list when it meets a pseudo-class it does not know.
   `inset: 0 auto auto 0` (top:0 left:0), NOT `inset: auto` — with auto the fixed
   box resolves to its static position and the translate lands off by the
   root's screen offset. The UA [popover] sheet sets margin, border, overflow,
   color: CanvasText and background: Canvas; all re-declared here. */
.pk-lens:popover-open {
  display: flex;
  position: fixed;
  inset: 0 auto auto 0;
  margin: 0;
  border: 0;
  overflow: visible;
  color: #fff;
  background: oklch(0.53 0.13 var(--h));
}
@media (prefers-color-scheme: dark) {
  .pk-lens:popover-open { background: oklch(0.5 0.13 var(--h)); }
}
/* The base display:flex would otherwise beat the UA's display:none and pin a
   closed popover chip visible in flow. */
.pk-lens[popover]:not(:popover-open) { display: none; }
.pk-lens-glyph { font-size: 24px; line-height: 1; }
.pk-lens-glyph.pk-mono { font-size: 17px; letter-spacing: .01em; }
.pk-lens-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* ---------- screen-reader surfaces ---------- */

/* .pk-sr mirrors the (aria-hidden) wheel as real buttons; .pk-live announces
   level changes. Both stay focusable/clickable — clipped, never display:none. */
.pk-sr,
.pk-live {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---------- onboarding "try it" panel (must fit 375x667) ---------- */

.onboard-body:has(.ob-picker-panel) { display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
.onboard-step:has(.ob-picker-panel) { display: flex; flex-direction: column; min-height: 0; flex: 1; }
/* the try panel competes with the wizard's own chrome for a 667px-tall screen.
   Trim the step's furniture (title, row padding) — never the 44px controls. */
.onboard-body:has(.ob-picker-panel) { padding-bottom: 10px; }
.onboard-step:has(.ob-picker-panel) .ob-title { font-size: 18px; margin: 2px 0 4px; }
.onboard-step:has(.ob-picker-panel) .cutoff-row { padding: 2px 0 4px; gap: 6px; }
.onboard-step:has(.ob-picker-panel) .ob-lead { margin: 0 0 4px; font-size: 14px; }
.onboard-step:has(.ob-picker-panel) .ob-picker-blurb { margin: 0; padding: 0 0 4px; font-size: 12px; }
/* position: relative so the engine's fallback lens (no Popover API) is placed
   against the panel; the engine owns the root's children and wipes them on
   every level change */
.ob-picker-panel { display: flex; flex-direction: column; gap: 6px; min-height: 0; flex: 1; position: relative; }
.ob-picker-root {
  flex: 1;
  min-height: 0;
  position: relative;
  overflow: hidden;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}
/* sized by the panel, never by the viewport — the wizard is a full-screen
   dialog and a dvh-sized wheel would push the footer off an iPhone SE. The
   panel is short and wide, so HEIGHT is the binding constraint: let width
   follow from the 1:1 ratio instead of fighting `height: 100%` for the box. */
.ob-picker-panel .pk-wheel svg { width: auto; height: 100%; aspect-ratio: 1; }
.ob-picker-sample {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-weight: 700;
  font-size: 15px;
}
.ob-picker-sample .ob-picker-amt { margin-left: auto; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--muted); }
.ob-picker-result { min-height: 18px; font-size: 13px; font-weight: 700; color: var(--accent); }
.ob-picker-result:empty { visibility: hidden; }

/* ---------- short viewports (landscape phones) ---------- */

@media (max-height: 520px) {
  .sheet.pick-tiles .sheet-body,
  .sheet.pick-cols .sheet-body { height: min(74dvh, 300px); }
  .sheet.pick-wheel .sheet-body { height: min(74dvh, 62vw); }
  .pk-emoji { font-size: clamp(16px, 5vmin, 26px); }
  .pk-dock-sub .pk-tile { min-height: 40px; }
}

/* "Try it" buttons in the wizard picker step and in Settings. A 13px text link
   was the smallest thing on either screen; this is a real button. */
.try-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 48px;
  padding: 0 16px;
  border-radius: 12px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 16px;
  font-weight: 700;
  margin: 8px 0 4px;
  transition: transform 90ms ease-out, filter 90ms ease-out;
}
.try-btn:active { transform: scale(.97); filter: brightness(1.12); }
@media (prefers-reduced-motion: reduce) {
  .try-btn { transition: none; }
}
