/* SpendPattern — design system
   Large type + high contrast for readability (older users),
   fluid motion kept subtle and GPU-friendly (transform/opacity only). */

:root {
  --bg: #fafafa;
  --card: #ffffff;
  --ink: #18181b;
  --muted: #64646e;
  --line: #e8e8ec;
  --brand: #7c3aed;
  --brand-2: #d946ef;
  --brand-ink: #ffffff;
  --pos: #10b981;
  --neg: #ef4444;
  --warn-bg: #fef3e2;
  --shadow: 0 8px 30px rgba(24, 24, 27, .07);
  --radius: 18px;
}
:root[data-theme="dark"] {
  --bg: #000000;
  --card: #16161c;
  --ink: #f4f4f5;
  --muted: #9d9da8;
  --line: #26262e;
  --brand: #a78bfa;
  --brand-2: #e879f9;
  --warn-bg: #2a2118;
  --shadow: 0 8px 30px rgba(0, 0, 0, .45);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #000000;
    --card: #16161c;
    --ink: #f4f4f5;
    --muted: #9d9da8;
    --line: #26262e;
    --brand: #a78bfa;
    --brand-2: #e879f9;
    --warn-bg: #2a2118;
    --shadow: 0 8px 30px rgba(0, 0, 0, .45);
  }
}

/* Theme switch animation — View Transitions circular wipe (no text stutter) */
::view-transition-old(root), ::view-transition-new(root) { animation: none; mix-blend-mode: normal; }
::view-transition-new(root) { z-index: 2147483646; }
::view-transition-old(root) { z-index: 1; }

* { box-sizing: border-box; margin: 0; }
/* Safety net: the browser's own default link blue (#0000EE-ish) is
   harsh, especially on a dark background — any <a> that doesn't get a
   more specific colour rule elsewhere lands here instead of on that
   default. Sky blue, matching one of the hero gradient's own stops
   rather than an unrelated new colour. */
a { color: #38bdf8; }
[hidden] { display: none !important; }
html { -webkit-text-size-adjust: 100%; }
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", system-ui, sans-serif;
  background: var(--bg);
  color: var(--ink);
  font-size: 17px;
  line-height: 1.55;
  min-height: 100dvh;
  display: flex; flex-direction: column;
  padding-bottom: env(safe-area-inset-bottom);
  overflow-x: hidden;
}
.card, .screen, .cards > * { min-width: 0; }
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

/* ── Top bar ── */
.topbar {
  display: flex; justify-content: space-between; align-items: center;
  flex-wrap: wrap; gap: 10px 16px;
  padding: 14px clamp(16px, 4vw, 40px);
  position: sticky; top: 0; z-index: 5;
  background: color-mix(in srgb, var(--bg) 80%, transparent);
  backdrop-filter: blur(12px);
}
/* backdrop-filter can't be captured correctly by the View Transitions API's
   snapshot — during the circular-wipe theme animation it can briefly render
   flat/unblurred, showing whatever's behind it for a frame. Swapping to a
   fully opaque background (still the correct new theme colour, since
   applyTheme() flips --bg synchronously before this class is even needed)
   for the animation's duration sidesteps the compositing gap entirely. */
.topbar.no-blur { backdrop-filter: none; background: var(--bg); }
.topbar-tools {
  display: flex; flex-wrap: wrap; gap: 8px;
  padding: 0 clamp(16px, 4vw, 40px) 10px;
}
.brand { display: flex; align-items: center; gap: 10px; font-weight: 800; font-size: 20px; color: var(--brand); white-space: nowrap; }
.topbar-right { position: relative; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.privacy-tip-wrap { display: inline-flex; }
.btn-privacy {
  width: 40px; height: 40px; border-radius: 12px; border: 1.5px solid var(--line);
  background: color-mix(in srgb, var(--pos) 10%, transparent); color: var(--pos);
  cursor: pointer; display: grid; place-items: center;
  transition: transform .3s ease, border-color .2s ease;
}
.btn-privacy:hover, .btn-privacy:focus-visible { transform: scale(1.08); border-color: var(--pos); }
.btn-privacy:active { transform: scale(.9); }
.privacy-tip {
  position: absolute; top: calc(100% + 10px); right: 0; z-index: 20;
  width: max-content; max-width: min(240px, calc(100vw - 32px));
  background: var(--card); color: var(--ink); border: 1.5px solid var(--line);
  border-radius: 12px; padding: 10px 14px; font-size: 13.5px; font-weight: 600;
  box-shadow: var(--shadow);
  opacity: 0; transform: translateY(-6px); pointer-events: none;
  transition: opacity .2s ease, transform .2s ease;
}
.btn-privacy:hover + .privacy-tip,
.btn-privacy:focus-visible + .privacy-tip,
.privacy-tip.show {
  opacity: 1; transform: translateY(0);
}

/* Supported-banks info moved out of the dropzone (was cluttering it) into
   a small popover — same interaction pattern as the history drawer. */
/* position: relative so each popover anchors under its OWN button. Without
   it they resolve against .topbar-right and every dropdown opens stacked in
   the same spot at the far right, regardless of which button was clicked. */
.banks-dropdown { display: inline-flex; position: relative; }
.banks-toggle {
  font: inherit; font-size: 13.5px; font-weight: 600; color: var(--muted);
  background: var(--card); border: 1.5px solid var(--line); border-radius: 999px;
  padding: 7px 14px; cursor: pointer; white-space: nowrap;
  display: inline-flex; align-items: center;
  /* not border-color: it reads from var(--line), and a transitioned custom
     property freezes at its first computed value — it would keep the dark
     theme's border after switching to light. */
  transition: color .15s ease, background .15s ease, box-shadow .15s ease;
}
.banks-toggle:hover { color: var(--ink); background: color-mix(in srgb, var(--ink) 5%, var(--card)); }
.banks-toggle[aria-expanded="true"] {
  color: var(--brand);
  background: color-mix(in srgb, var(--brand) 10%, var(--card));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 14%, transparent);
}
.banks-toggle .caret { display: inline-block; margin-left: 7px; font-size: 15px; transition: transform .22s var(--spring, ease); }
.banks-toggle[aria-expanded="true"] .caret { transform: rotate(180deg); }
.banks-popover {
  position: absolute; top: calc(100% + 10px); right: 0; z-index: 20;
  width: 248px; max-width: calc(100vw - 32px); background: var(--card);
  border: 1px solid var(--line);
  border-radius: 16px; box-shadow: 0 12px 34px rgba(0,0,0,.14), var(--shadow); padding: 12px;
  animation: pop-in .18s ease both;
}
@keyframes pop-in { from { opacity: 0; transform: translateY(-8px) scale(.97); } to { opacity: 1; transform: none; } }
/* Anchor side is decided at open time in app.js (see wireDropdown): once the
   topbar wraps, these buttons move to the left edge and a right-anchored
   popover hangs off-screen, but a left-anchored one overflows the right for
   the second button. Only measuring can tell which side actually fits. */
.banks-popover.align-left { right: auto; left: 0; }
.banks-popover-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); margin-bottom: 6px; }
.banks-popover ul { list-style: none; margin: 0; padding: 0; }
.banks-popover li { font-size: 14px; padding: 5px 0; color: var(--ink); text-align: left; }
.banks-popover li::before { content: "✓ "; color: var(--pos); font-weight: 700; }

/* ── Screens ── */
.screen {
  max-width: 1080px; width: 100%; margin: 0 auto; padding: 8px clamp(16px, 4vw, 40px) 24px;
  flex: 1; display: flex; flex-direction: column; justify-content: space-between; gap: 18px;
}

.hero { text-align: center; padding: clamp(8px, 2vw, 16px) 0 18px; animation: rise .6s ease both; }
.hero h1 { font-size: clamp(32px, 6vw, 56px); font-weight: 800; letter-spacing: -.03em; line-height: 1.12; }
.hero em {
  font-style: normal;
  background: linear-gradient(90deg, #2dd4bf, #38bdf8, #a855f7, #ef4444, #fb923c, #2dd4bf);
  background-size: 300% 100%;
  -webkit-background-clip: text; background-clip: text; color: transparent;
  animation: hero-hue 8s ease-in-out infinite;
}
@keyframes hero-hue {
  0%, 100% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
}
.hero .sub { max-width: 560px; margin: 10px auto 0; color: var(--muted); font-size: 15.5px; }

/* ── Dropzone ──
   3D tilt: .dropzone stays perfectly static (transform: none) until a real
   mousemove fires (see app.js) — no idle animation, no CSS :hover transform
   competing with it. The wrapping .dz-perspective supplies `perspective`
   since a preserve-3d element needs an ancestor with perspective set to
   actually render foreshortening rather than a flat instant "flip". */
.dz-perspective { perspective: 1000px; margin: 8px auto 0; max-width: 680px; width: 100%; animation: rise .6s .12s ease both; }
.dropzone {
  position: relative; width: 100%;
  border: 2.5px dashed color-mix(in srgb, var(--brand) 45%, transparent);
  border-radius: var(--radius);
  background: var(--card);
  padding: clamp(28px, 5vw, 48px);
  text-align: center; cursor: pointer; color: var(--brand);
  /* NOT overflow:hidden here — WebKit/Safari has a long-standing bug
     where overflow:hidden combined with transform-style:preserve-3d on
     (or above) a 3D-transformed element renders incorrectly: children
     flicker, pop, or clip wrong mid-transition, even though Chrome/
     Firefox handle the identical combo fine. Rather than patch around a
     browser bug, the box just never establishes that clip — see below
     for how each thing that used to rely on it is handled instead. */
  box-shadow: var(--shadow);
  transform-style: preserve-3d; transform: rotateX(0) rotateY(0) scale(1);
  /* NOT transform/box-shadow here — app.js's tilt loop already eases both
     itself (a per-frame lerp toward the cursor target), and a CSS
     transition racing against 60 direct style writes a second per frame
     was exactly what caused the stutter/"dancing" reported on a slow
     hover: each new write restarted an in-flight transition before it
     could resolve. border-color is the only property still genuinely
     driven by CSS-only state changes (:focus-visible, .dragover). */
  transition: border-color .2s ease;
}
.dropzone:focus-visible { border-color: var(--brand); outline: none; }
.dropzone.dragover {
  border-style: solid; border-color: var(--brand-2);
  background: color-mix(in srgb, var(--brand) 7%, var(--card));
  outline: none;
  animation: dropzone-pulse 1.4s ease-in-out infinite;
}
@keyframes dropzone-pulse {
  0%, 100% { box-shadow: 0 0 0 4px color-mix(in srgb, var(--brand-2) 25%, transparent), 0 20px 50px color-mix(in srgb, var(--brand) 35%, transparent); }
  50%      { box-shadow: 0 0 0 9px color-mix(in srgb, var(--brand-2) 14%, transparent), 0 20px 54px color-mix(in srgb, var(--brand) 45%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .dropzone.dragover { animation: none; }
  .dz-file, .dz-pad, .dz-impact { animation: none !important; opacity: 1; transform: translate(-50%, 7px); }
}

/* Glow that tracks the same point the tilt is leaning toward — reinforces
   the 3D illusion instead of the card looking like it's warping for no
   reason. Only visible while .tracking is active (set/cleared by app.js
   alongside the mousemove handler). */
/* Its blur bleeds a few px past the dashed border now that .dropzone has
   no overflow clip (see that rule's comment) — reads as an ambient glow
   escaping the box, not a bug; way less risky than fighting Safari for
   pixel-perfect containment. */
.dz-glow { position: absolute; inset: -2px; border-radius: var(--radius); z-index: 0; opacity: 0; transition: opacity .25s ease; filter: blur(24px); pointer-events: none; }
.dropzone.tracking .dz-glow { opacity: .5; }
.dz-inner { position: relative; z-index: 1; transform: translateZ(30px); }

/* ── Inner file-fall loop — independent of the tilt above, runs on its
   own timeline the entire time regardless of hover state. One PDF (Adobe
   red) and one DOCX (Word blue) icon alternate every landing, toggled in
   app.js on the fall animation's "animationiteration" event rather than
   duplicating these keyframes per file type. */
.dz-stage { position: relative; height: 64px; margin: 0 auto 4px; width: 64px; transform: translateZ(45px); }
.dz-pad {
  position: absolute; left: 50%; bottom: -4px; width: 46px; height: 10px;
  transform: translateX(-50%) scaleX(.5); border-radius: 50%;
  background: radial-gradient(ellipse at center, color-mix(in srgb, var(--brand) 55%, transparent), transparent 70%);
  opacity: 0;
  animation: dz-pad-pulse 2.8s cubic-bezier(.4,0,.2,1) infinite;
}
@keyframes dz-pad-pulse {
  0%, 34% { opacity: 0; transform: translateX(-50%) scaleX(.5); }
  40%     { opacity: .9; transform: translateX(-50%) scaleX(1.15); }
  55%, 80% { opacity: .35; transform: translateX(-50%) scaleX(.85); }
  100%    { opacity: 0; transform: translateX(-50%) scaleX(.5); }
}
/* Rises only ~one icon-height above the stage (was -70px) — small enough
   to stay inside the box's own padding at every breakpoint without
   needing overflow:hidden to hide the poke-through, which is exactly the
   clip .dropzone no longer has (Safari bug, see its comment). */
.dz-file {
  position: absolute; left: 50%; top: 0; width: 34px; height: 42px;
  transform: translate(-50%, -34px) rotate(-6deg) scale(.7);
  opacity: 0;
  animation: dz-file-fall 2.8s cubic-bezier(.4,0,.2,1) infinite;
}
.dz-file svg { width: 100%; height: 100%; display: block; }
@keyframes dz-file-fall {
  0%   { transform: translate(-50%, -34px) rotate(-10deg) scale(.7); opacity: 0; }
  8%   { opacity: 1; }
  38%  { transform: translate(-50%, 10px) rotate(2deg) scale(1.04); opacity: 1; }
  46%  { transform: translate(-50%, 2px) rotate(-1deg) scale(.97); opacity: 1; }
  54%  { transform: translate(-50%, 7px) rotate(0deg) scale(1); opacity: 1; }
  80%  { transform: translate(-50%, 7px) rotate(0deg) scale(1); opacity: 1; }
  92%  { opacity: 0; }
  100% { transform: translate(-50%, -34px) rotate(-10deg) scale(.7); opacity: 0; }
}
.dz-impact {
  position: absolute; left: 50%; bottom: 2px; width: 10px; height: 10px; border-radius: 50%;
  border: 1.5px solid var(--brand-2); transform: translate(-50%, 50%) scale(0); opacity: 0;
  animation: dz-impact-ring 2.8s cubic-bezier(.4,0,.2,1) infinite;
}
@keyframes dz-impact-ring {
  0%, 36% { opacity: 0; transform: translate(-50%, 50%) scale(0); }
  38%     { opacity: .8; transform: translate(-50%, 50%) scale(.3); }
  52%     { opacity: 0; transform: translate(-50%, 50%) scale(2.6); }
  100%    { opacity: 0; transform: translate(-50%, 50%) scale(2.6); }
}

.dz-title { font-size: 18px; font-weight: 700; color: var(--ink); margin-top: 6px; transform: translateZ(20px); }
.dz-hint { color: var(--muted); margin-top: 4px; font-size: 14.5px; transform: translateZ(15px); }
.dz-tips { margin-top: 14px; display: flex; flex-direction: column; align-items: center; gap: 6px; }
.dz-tip {
  font-size: 12.5px; color: var(--muted);
  background: color-mix(in srgb, var(--ink) 6%, transparent);
  border-radius: 999px; padding: 5px 12px;
}

.btn-outline {
  display: block; margin: 10px auto 0; font: inherit; font-weight: 700;
  color: var(--brand); background: transparent; cursor: pointer;
  border: 1.5px solid color-mix(in srgb, var(--brand) 40%, transparent);
  border-radius: 999px; padding: 10px 20px;
  transition: background .15s ease, transform .15s ease, border-color .15s ease;
}
.btn-outline:hover { background: color-mix(in srgb, var(--brand) 10%, transparent); border-color: var(--brand); transform: translateY(-1px); }
.btn-outline:active { transform: translateY(0); }

.sample-showcase { text-align: center; margin: 10px auto 0; }
.sample-showcase-label { font-size: 12px; color: var(--muted); margin-bottom: 6px; }
.sample-showcase-links { display: flex; justify-content: center; gap: 18px; flex-wrap: wrap; }
.sample-showcase-links a {
  font-size: 13.5px; font-weight: 600; color: var(--brand);
  text-decoration: none; border-bottom: 1px dashed color-mix(in srgb, var(--brand) 50%, transparent);
  padding-bottom: 1px;
}
.sample-showcase-links a:hover { color: var(--brand-2); border-color: var(--brand-2); }

.sample-banner {
  max-width: 560px; margin: 0 auto 16px; text-align: center;
  font-weight: 600; font-size: 14px; color: var(--brand);
  background: color-mix(in srgb, var(--brand) 10%, transparent);
  border-radius: 12px; padding: 10px 16px;
}
.confidence-banner {
  max-width: 640px; margin: 0 auto 16px; text-align: center;
  font-weight: 600; font-size: 14px; color: var(--neg);
  background: color-mix(in srgb, var(--neg) 10%, transparent);
  border-radius: 12px; padding: 12px 16px;
  display: flex; flex-direction: column; gap: 8px; align-items: center;
}
.confidence-banner b { display: block; }

/* ── Export panel. The format picking and the anonymize option moved into
   the download modal — leaving one button here keeps the results page a
   dashboard rather than a settings form. */
.export-panel { text-align: center; display: flex; flex-direction: column; align-items: center; gap: 10px; }
.export-formats-hint { font-size: 12.5px; color: var(--muted); margin: -2px 0 0; }
.export-panel .error { margin: 0; }

/* ── Modal shell (download picker + about) ── */
.modal-backdrop {
  position: fixed; inset: 0; z-index: 60; background: rgba(10, 10, 14, .5);
  backdrop-filter: blur(3px);
  opacity: 0; transition: opacity .22s ease;
}
.modal-backdrop.show { opacity: 1; }
.app-modal {
  position: fixed; z-index: 61; top: 50%; left: 50%;
  transform: translate(-50%, -50%) scale(.96); opacity: 0;
  width: min(560px, calc(100vw - 28px)); max-height: min(84vh, 780px);
  display: flex; flex-direction: column;
  background: var(--card); color: var(--ink);
  border: 1px solid var(--line); border-radius: 22px;
  box-shadow: 0 24px 70px rgba(0, 0, 0, .3);
  transition: opacity .22s ease, transform .22s ease;
}
.app-modal.open { opacity: 1; transform: translate(-50%, -50%) scale(1); }
.app-modal-head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 18px 20px 12px; border-bottom: 1px solid var(--line); flex: none;
}
.app-modal-head h2 { margin: 0; font-size: 19px; }
.modal-close {
  font: inherit; font-size: 15px; line-height: 1; color: var(--muted);
  background: none; border: 0; cursor: pointer; padding: 8px; border-radius: 10px;
  transition: color .15s ease, background .15s ease;
}
.modal-close:hover { color: var(--ink); background: color-mix(in srgb, var(--ink) 8%, transparent); }
.app-modal-body { padding: 16px 20px; overflow-y: auto; flex: 1 1 auto; }
.app-modal-foot {
  padding: 14px 20px 18px; border-top: 1px solid var(--line); flex: none;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
}
.modal-lede { margin: 0 0 14px; font-size: 14px; color: var(--muted); line-height: 1.55; }
.modal-foot-note { margin: 0; font-size: 12px; color: var(--muted); text-align: center; }
.modal-foot-note a { color: #38bdf8; font-weight: 700; }

/* ── Format picker ── */
.fmt-list { list-style: none; margin: 0 0 16px; padding: 0; display: grid; gap: 9px; }
.fmt-item {
  position: relative; display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 12px;
  padding: 12px 14px; border: 1.5px solid var(--line); border-radius: 14px;
  cursor: pointer; background: var(--bg);
  transition: background .15s ease, box-shadow .15s ease;
}
.fmt-item:hover { background: color-mix(in srgb, var(--ink) 4%, var(--bg)); }
.fmt-check { position: absolute; opacity: 0; pointer-events: none; }
.fmt-ico { font-size: 22px; line-height: 1; }
.fmt-text b { display: block; font-size: 14.5px; }
.fmt-text small { display: block; color: var(--muted); font-size: 12.5px; line-height: 1.45; margin-top: 2px; }
.fmt-tick {
  width: 22px; height: 22px; border-radius: 7px; flex: none;
  border: 1.5px solid var(--line); display: grid; place-items: center;
  transition: background .15s ease, border-color .15s ease;
}
.fmt-tick::after {
  content: ""; width: 10px; height: 6px; margin-top: -2px;
  border-left: 2.5px solid #fff; border-bottom: 2.5px solid #fff;
  transform: rotate(-45deg) scale(.5); opacity: 0;
  transition: opacity .15s ease, transform .15s ease;
}
.fmt-check:checked ~ .fmt-tick { background: var(--pos); border-color: var(--pos); }
.fmt-check:checked ~ .fmt-tick::after { opacity: 1; transform: rotate(-45deg) scale(1); }
.fmt-check:checked ~ .fmt-text b { color: var(--pos); }
/* :has lets the whole row react, but the tick above already carries the
   state on its own — so browsers without :has still show a correct UI. */
.fmt-item:has(.fmt-check:checked) {
  border-color: var(--pos);
  background: color-mix(in srgb, var(--pos) 8%, var(--bg));
}
.fmt-check:focus-visible ~ .fmt-tick { box-shadow: 0 0 0 3px color-mix(in srgb, var(--pos) 35%, transparent); }

/* ── Anonymize switch ── */
.switch-row {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 13px 14px; border: 1.5px solid var(--line); border-radius: 14px;
  cursor: pointer; background: var(--bg);
  transition: background .15s ease;
}
.switch-row:hover { background: color-mix(in srgb, var(--ink) 4%, var(--bg)); }
.switch-text b { display: block; font-size: 14px; }
.switch-text small { display: block; color: var(--muted); font-size: 12.5px; line-height: 1.45; margin-top: 3px; }
.switch { position: relative; flex: none; }
.switch input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.switch-track {
  display: block; width: 46px; height: 27px; border-radius: 999px;
  background: color-mix(in srgb, var(--ink) 18%, transparent);
  transition: background .2s ease;
}
.switch-track::after {
  content: ""; position: absolute; top: 3px; left: 3px;
  width: 21px; height: 21px; border-radius: 50%; background: #fff;
  box-shadow: 0 1px 4px rgba(0,0,0,.3);
  transition: transform .2s ease;
}
.switch input:checked + .switch-track { background: var(--pos); }
.switch input:checked + .switch-track::after { transform: translateX(19px); }
.switch input:focus-visible + .switch-track { box-shadow: 0 0 0 3px color-mix(in srgb, var(--pos) 35%, transparent); }

/* ── About modal body ── */
.about-section { margin-bottom: 22px; }
.about-section:last-child { margin-bottom: 0; }
.about-section h3 { margin: 0 0 8px; font-size: 15px; color: var(--brand); }
.about-section p { margin: 0 0 9px; font-size: 14.5px; line-height: 1.6; color: var(--muted); }
.about-section ul, .about-section ol { margin: 0; padding-left: 20px; }
.about-section li { font-size: 14.5px; line-height: 1.6; color: var(--muted); margin-bottom: 7px; }
.about-section b { color: var(--ink); }
.about-section code {
  font-size: 13px; padding: 1px 5px; border-radius: 5px;
  background: color-mix(in srgb, var(--ink) 8%, transparent);
}

/* ── Password ── */
.password-row { max-width: 560px; margin: 18px auto 0; background: var(--card); border-radius: var(--radius); padding: 20px; box-shadow: var(--shadow); animation: rise .35s ease both; }
.password-row label { font-weight: 600; }
.pw-line { display: flex; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
.pw-line input {
  flex: 1; min-width: 180px; font-size: 17px; padding: 12px 14px;
  border: 1.5px solid var(--line); border-radius: 12px; background: var(--bg); color: var(--ink);
}
.pw-line input:focus { border-color: var(--brand); outline: none; }

.btn-primary {
  font: inherit; font-weight: 700; color: var(--brand-ink);
  background: var(--brand); border: 0; border-radius: 12px;
  padding: 12px 22px; cursor: pointer;
  transition: transform .15s ease, filter .15s ease;
}
.btn-primary:hover { transform: translateY(-1px); filter: brightness(1.08); }
.btn-primary:active { transform: translateY(0); }
.btn-ghost {
  font: inherit; font-weight: 600; color: var(--brand);
  background: none; border: 0; cursor: pointer; padding: 8px 0;
}

.btn-export {
  font: inherit; font-weight: 700; color: var(--brand-ink);
  background: linear-gradient(135deg, var(--brand), var(--brand-2));
  border: 0; border-radius: 999px; cursor: pointer;
  padding: 11px 22px; display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  box-shadow: 0 4px 18px color-mix(in srgb, var(--brand) 45%, transparent);
  transition: transform .15s ease, box-shadow .15s ease;
  animation: export-pulse 2.6s ease-in-out infinite;
}
.btn-export:hover {
  transform: translateY(-1px);
  box-shadow: 0 6px 22px color-mix(in srgb, var(--brand) 55%, transparent);
  animation-play-state: paused;
}
.btn-export:active { transform: translateY(0); }
.btn-export:disabled { opacity: .7; cursor: default; animation-play-state: paused; }
@keyframes export-pulse {
  0%, 100% { box-shadow: 0 4px 18px color-mix(in srgb, var(--brand) 45%, transparent); }
  50%      { box-shadow: 0 4px 26px color-mix(in srgb, var(--brand) 70%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .btn-export { animation: none; }
}

/* Green variant for the actual buy action. color is forced because
   .btn-export's own `color: var(--brand-ink)` is equally specific and
   resolves to white in light theme but would otherwise be theme-dependent —
   on this gradient it must stay white in both. */
.btn-export-green {
  color: #fff !important;
  background: linear-gradient(135deg, #10b981, #22c55e 55%, #4ade80);
  box-shadow: 0 4px 18px color-mix(in srgb, #10b981 45%, transparent);
  animation: export-pulse-green 2.6s ease-in-out infinite;
}
.btn-export-green:hover {
  box-shadow: 0 6px 22px color-mix(in srgb, #10b981 58%, transparent);
  animation-play-state: paused;
}
@keyframes export-pulse-green {
  0%, 100% { box-shadow: 0 4px 18px color-mix(in srgb, #10b981 45%, transparent); }
  50%      { box-shadow: 0 4px 26px color-mix(in srgb, #10b981 70%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .btn-export-green { animation: none; }
}

.error {
  max-width: 560px; margin: 16px auto; text-align: center;
  color: var(--neg); font-weight: 600;
  background: color-mix(in srgb, var(--neg) 10%, transparent);
  border-radius: 12px; padding: 12px 16px;
  animation: shake .4s ease;
}

/* ── Trust strip ── */
.trust { display: flex; justify-content: center; gap: clamp(14px, 3vw, 36px); flex-wrap: wrap; margin-top: 18px; animation: rise .6s .24s ease both; }
.trust-item { display: flex; gap: 8px; align-items: center; }
.trust-item span { font-size: 20px; width: 26px; text-align: center; flex: none; }
.trust-item b { font-size: 14px; }
.trust-item small { display: block; color: var(--muted); font-size: 12px; }
/* Mobile: keep it one compact row (not a stacked column — that used more
   vertical space than the page had) and drop the descriptive subtext,
   which mostly repeated what's already in the banks popover/dropzone. */
@media (max-width: 620px) {
  .trust { gap: 14px; }
  .trust-item { flex-direction: column; gap: 2px; width: auto; text-align: center; }
  .trust-item small { display: none; }
  .topbar { padding: 10px clamp(14px, 4vw, 40px); }
  .hero { padding: 10px 0 14px; }
  .hero h1 { font-size: 34px; }
  .hero .sub { font-size: 16px; margin-top: 8px; }
  .dropzone { padding: 14px 18px; }
  .dz-title { font-size: 16px; }
}

/* ── Loading ── */
.loading { display: grid; justify-content: center; place-items: center; gap: 22px; min-height: 60dvh; text-align: center; }
/* No box/border around the icon — it floats directly on the page
   background, matching the rest of the app's unboxed, minimal structure. */
.scanner { position: relative; width: 180px; height: 180px; display: grid; place-items: center; }
/* Glassy "dim glow, dim glow" breathing ring behind the icon — opacity +
   transform only, so it's compositor-only (GPU), same reasoning as the
   hero gradient fix: cheap to animate forever without janking the page. */
.scanner::before {
  content: ""; position: absolute; inset: -30px; border-radius: 50%;
  background: radial-gradient(circle, color-mix(in srgb, var(--brand) 45%, transparent), transparent 70%);
  filter: blur(22px);
  animation: glow-breathe 2.4s ease-in-out infinite;
}
@keyframes glow-breathe {
  0%, 100% { opacity: .3; transform: scale(.9); }
  50%      { opacity: .85; transform: scale(1.1); }
}
.scan-doc {
  font-size: 92px; display: block; position: relative;
  animation: doc-pulse 1.4s ease-in-out infinite;
}
.scan-line {
  position: absolute; left: 50%; top: 0; width: 230px; height: 4px; border-radius: 4px;
  transform: translate(-50%, 14px);
  background: linear-gradient(90deg, transparent, #22d3ee, var(--brand), #22d3ee, transparent);
  box-shadow: 0 0 18px 2px color-mix(in srgb, var(--brand) 65%, transparent),
              0 0 30px 6px color-mix(in srgb, #22d3ee 45%, transparent);
  animation: scan 1.4s ease-in-out infinite;
}
/* transform instead of animating `top` — top triggers layout+paint every
   frame, transform is compositor-only. */
@keyframes scan {
  0%, 100% { transform: translate(-50%, 14px); }
  50%      { transform: translate(-50%, 210px); }
}
#loading-msg { font-size: 20px; font-weight: 600; color: var(--muted); }

/* ── Results ── */
.results-head {
  display: flex; justify-content: space-between; align-items: center;
  flex-wrap: wrap; gap: 10px 14px; padding: 6px 0 14px;
}
/* Fixed blue rather than var(--brand): this badge names which bank the
   statement came from, which is a statement of fact, not a brand accent —
   keeping it off the purple stops it competing with the export button. */
.bank-badge {
  font-weight: 700; font-size: 14px; letter-spacing: .04em; white-space: nowrap;
  background: color-mix(in srgb, #2980F2 14%, transparent);
  color: #2980F2; padding: 6px 14px; border-radius: 999px;
}
:root[data-theme="dark"] .bank-badge { color: #6FB0FF; background: color-mix(in srgb, #2980F2 22%, transparent); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .bank-badge { color: #6FB0FF; background: color-mix(in srgb, #2980F2 22%, transparent); }
}
.btn-ghost, .btn-export { white-space: nowrap; }
/* Used to force #btn-again onto its own full-width row here for a 3-item
   overflow problem (back link + badge + export button all fighting for
   one line) — the export button moved out into its own card/download
   modal since, leaving just the back link and badge, which comfortably
   share one row at every width. The old override was stretching the
   button's own UA default text-align:center across the full row, which
   is why it read as centred rather than left-aligned. */

.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 14px; }
.card {
  background: var(--card); border-radius: var(--radius); box-shadow: var(--shadow);
  padding: clamp(16px, 3vw, 24px); margin-top: 14px;
  animation: rise .5s ease both;
}
.cards .card { margin-top: 0; }
.card h2 { font-size: 19px; margin-bottom: 10px; }
.stat small { color: var(--muted); font-weight: 600; font-size: 14px; }
.stat b { display: block; font-size: clamp(22px, 3.2vw, 30px); font-weight: 800; margin-top: 4px; font-variant-numeric: tabular-nums; }
.pos { color: var(--pos); } .neg { color: var(--neg); }
.stat.filterable {
  position: relative;
  cursor: pointer; border: 1.5px solid transparent; text-align: left;
  font: inherit; color: inherit; width: 100%;
  transition: border-color .15s ease, transform .1s ease;
}
.stat.filterable:hover { border-color: color-mix(in srgb, var(--brand) 30%, transparent); }
.stat.filterable:active { transform: scale(.98); }
.stat.filterable.is-active { border-color: var(--brand); background: color-mix(in srgb, var(--brand) 8%, var(--card)); }
/* Persistent (not hover-only — hover doesn't exist on touch) affordance
   that these two cards are buttons, unlike the Net/Transactions cards
   beside them which look identical otherwise. A small funnel icon plus a
   micro tap-hint, not a checkbox/switch look, since this is a one-shot
   filter action, not a toggle-state setting. */
.filter-hint-ico {
  position: absolute; top: 12px; right: 12px; color: var(--muted); opacity: .55;
}
.filter-hint-text {
  display: block; margin-top: 7px; font-size: 11px !important; font-weight: 600;
  color: var(--brand) !important; opacity: .75;
}
.stat.filterable.is-active .filter-hint-text { opacity: 1; font-size: 0 !important; }
.stat.filterable.is-active .filter-hint-text::before { content: "Showing only this — tap to clear"; font-size: 11px; }
.stat.filterable.is-active .filter-hint-ico { color: var(--brand); opacity: 1; }

/* Mobile: single column, everything scrolls top-to-bottom. Wide viewports:
   auto-fit naturally goes to 2 columns when there's room for both — and
   just as naturally collapses back to 1 full-width column if only one of
   the two children is visible (e.g. only one of anomaly/subscription has
   data), no JS needed to detect that case. */
/* margin-top lives on the section itself (spacing from whatever came
   before it), not its children — a grid container doesn't let child
   margins collapse outward, so zeroing the children without giving the
   grid its own margin left it touching the previous section entirely. */
.grid2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 14px; margin-top: 14px; }
.grid2 > .card { margin-top: 0; }
.chart-wrap { position: relative; height: min(320px, 46vh); }
.chart-wrap canvas { max-width: 100% !important; }

/* Extra bottom room so the doughnut's hover tooltip near the bottom of the
   ring never gets clipped by the card edge. */
#cats-card { padding-bottom: clamp(36px, 6vw, 56px); }
.donut-row { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.donut-wrap { flex: 1 1 260px; min-width: 220px; height: min(360px, 50vh); }
.donut-center {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; pointer-events: none;
  text-align: center; padding: 0 20%;
}
.donut-center b {
  font-size: clamp(19px, 3vw, 24px); font-weight: 800; letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
  transition: opacity .12s ease;
}
.donut-center small { color: var(--muted); font-size: 12px; font-weight: 600; margin-top: 2px; }

.cat-legend-wrap {
  flex: 1 1 200px; display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 10px; align-self: stretch;
}
.cat-legend-hint {
  font-size: 12px; font-weight: 600; color: var(--muted);
  text-align: center; margin: 0;
}
.cat-legend {
  display: flex; flex-direction: column; gap: 4px; width: 100%; max-width: 240px;
  padding: 10px; border-radius: 16px;
  background: color-mix(in srgb, var(--card) 55%, transparent);
  border: 1px solid color-mix(in srgb, var(--ink) 8%, transparent);
  backdrop-filter: blur(14px) saturate(160%);
  -webkit-backdrop-filter: blur(14px) saturate(160%);
  box-shadow: 0 4px 20px rgba(0,0,0,.06);
}
.cat-legend-item {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 10px; border-radius: 10px; cursor: pointer;
  background: transparent; border: 0; width: 100%; text-align: left; font: inherit;
  color: var(--ink); transition: background .15s ease, opacity .15s ease, transform .1s ease;
}
.cat-legend-item:hover { background: color-mix(in srgb, var(--brand) 12%, transparent); transform: translateX(2px); }
.cat-legend-item:active { transform: scale(.98); }
.cat-legend-swatch {
  width: 14px; height: 14px; border-radius: 5px; flex: none;
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.08);
  transition: background .15s ease, box-shadow .15s ease;
}
.cat-legend-label { flex: 1; font-size: 13.5px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cat-legend-item.is-off { opacity: .4; }
.cat-legend-item.is-off .cat-legend-label { text-decoration: line-through; }

#anomaly-card { background: var(--warn-bg); }
#subscription-card { background: color-mix(in srgb, var(--brand) 8%, var(--card)); }
.anomaly-list, .merchant-list, .subscription-list { list-style: none; }
.anomaly-list li, .merchant-list li, .subscription-list li {
  display: flex; justify-content: space-between; gap: 12px;
  padding: 11px 4px; border-bottom: 1px solid var(--line);
  font-variant-numeric: tabular-nums;
}
.anomaly-list li:last-child, .merchant-list li:last-child, .subscription-list li:last-child { border-bottom: 0; }
.merchant-bar { height: 6px; border-radius: 6px; background: var(--brand); margin-top: 6px; transition: width .8s cubic-bezier(.2,.8,.2,1); }
/* ── Merchant brand badges ──
   img brightness(0)+invert(1): Simple Icons ship monochrome (black-fill)
   SVGs meant to be recoloured by the consumer — an <img> can't be
   recoloured with `color`/`fill` like an inlined SVG can, so this forces
   solid black to solid white cheaply, which then sits on the brand's own
   colour as the badge background. */
.merchant-badge {
  flex: none; width: 30px; height: 30px; border-radius: 50%;
  display: grid; place-items: center; overflow: hidden;
  color: #fff; font-weight: 800; font-size: 13px;
}
.merchant-badge img { filter: brightness(0) invert(1); }
.m-left { flex: 1; min-width: 0; }
.m-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.amount { font-weight: 700; white-space: nowrap; }

#txn-search {
  width: 100%; font-size: 16px; padding: 12px 14px; margin-bottom: 10px;
  border: 1.5px solid var(--line); border-radius: 12px; background: var(--bg); color: var(--ink);
}
#txn-search:focus { border-color: var(--brand); outline: none; }
.txn-filter-note {
  font-size: 13.5px; font-weight: 600; color: var(--brand); margin: -2px 0 10px;
}
.txn-scroll { max-height: 430px; overflow: auto; border-radius: 12px; }
table { width: 100%; border-collapse: collapse; font-size: 15px; table-layout: fixed; }
/* Proportional columns — Date/Amount are short fixed content, Merchant
   gets the lion's share since it's the column people actually scan. */
col.col-date { width: 14%; } col.col-merchant { width: 46%; }
col.col-category { width: 24%; } col.col-amount { width: 16%; }
th { text-align: left; font-size: 13px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); padding: 8px 10px; position: sticky; top: 0; background: var(--card); }
td { padding: 10px; border-top: 1px solid var(--line); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Merchant cell has two children now (badge + name) instead of plain
   text, so the ellipsis truncation moves from the <td> itself onto the
   name span — a flex container can't ellipsis its own overflow the way a
   block with text-overflow does. */
.td-merchant { display: flex; align-items: center; gap: 8px; }
.td-merchant .merchant-badge { width: 20px; height: 20px; font-size: 10px; }
.td-merchant-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
th.num, td.num { text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }
tbody tr { cursor: default; transition: background .12s ease; }
tbody tr:hover { background: color-mix(in srgb, var(--ink) 5%, transparent); }
tr.flag td { background: color-mix(in srgb, var(--neg) 7%, transparent); }
tr.flag:hover td { background: color-mix(in srgb, var(--neg) 12%, transparent); }

/* Dime-inspired day-group header (see renderTable() in app.js) — a plain
   row using the SAME <td colspan="4"> as any transaction row, so none of
   the existing column-width/mobile-grid CSS above needs to know about it
   specially. Not hoverable/clickable, so no cursor/hover treatment.
   The flex layout lives on an inner <div>, NOT the <td> itself — a <td>
   needs to stay `display: table-cell` for the table's own column/colspan
   sizing algorithm to compute its width correctly; overriding that to
   `flex` directly made the browser stop treating it as a real table cell,
   so the colspan="4" span collapsed to a sliver the width of one column
   instead of the full row (confirmed visually: the label and amount were
   crushed together at the row's left edge, clipped by the base <td>
   rule's overflow:hidden). */
tr.txn-day-header td {
  padding: 12px 10px 6px; border-top: none;
  font-size: 11.5px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; color: var(--muted);
  overflow: visible; white-space: normal;
}
tr.txn-day-header td div { display: flex; justify-content: space-between; align-items: baseline; }
tr.txn-day-header:first-child td { padding-top: 4px; }
tr.txn-day-header:hover { background: transparent; }
tr.txn-day-header td b { font-size: 13px; font-weight: 800; letter-spacing: normal; text-transform: none; }
tr.txn-day-header td b.pos { color: var(--pos); }
tr.txn-day-header td b.neg { color: var(--neg); }

/* Mobile: 4 fixed-width columns (14/46/24/16%) on a ~340px viewport leaves
   almost no room per cell — merchant names truncate to a handful of
   characters and the category chip barely fits its own label. Below
   560px the table stops being a table (grid on each row instead) and
   becomes one small card per transaction: merchant name gets a full-width
   line to itself, date/amount share a row, category sits under its own —
   the same "list of cards" pattern already used for merchants/anomalies/
   subscriptions elsewhere on this page. thead is dropped since the grid
   areas below are self-labelling (date is obviously a date, amount is
   right-aligned and bold) the way a table header would have been. */
@media (max-width: 560px) {
  .txn-scroll { max-height: 60vh; }
  #txn-table thead { display: none; }
  #txn-table, #txn-table tbody, #txn-table tr, #txn-table td { display: block; width: auto; }
  #txn-table tr {
    display: grid;
    grid-template-columns: 1fr auto;
    grid-template-areas: "merchant merchant" "date category" "amount amount";
    row-gap: 5px; column-gap: 10px;
    padding: 12px 8px; border-top: 1px solid var(--line);
  }
  #txn-table tbody tr:first-child { border-top: 0; }
  #txn-table td {
    padding: 0; border-top: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  #txn-table td:nth-child(1) { grid-area: date; align-self: center; font-size: 12.5px; color: var(--muted); }
  #txn-table td:nth-child(2) { grid-area: merchant; font-weight: 700; font-size: 15.5px; white-space: normal; }
  /* #txn-table td (line above) forces display:block at higher specificity
     (id+tag beats .td-merchant's plain class) — restore flex for the badge
     layout, and let the name wrap onto a 2nd line instead of ellipsis-ing,
     since the mobile card has vertical room a table row didn't. */
  #txn-table td.td-merchant { display: flex; align-items: flex-start; }
  #txn-table td.td-merchant .td-merchant-name { white-space: normal; overflow: visible; text-overflow: clip; }
  #txn-table td.td-merchant .merchant-badge { margin-top: 1px; }
  #txn-table td:nth-child(3) { grid-area: category; justify-self: start; align-self: center; }
  #txn-table td:nth-child(4) { grid-area: amount; justify-self: end; align-self: center; font-size: 16px; }
  /* The flag tint is normally painted per-<td> (fine on a real table where
     cells sit flush), which on this grid layout would show as separate
     patches with gaps between them instead of one solid highlighted card —
     move it to the row itself and neutralise the per-cell version. */
  #txn-table tr.flag { background: color-mix(in srgb, var(--neg) 7%, transparent); }
  #txn-table tr.flag:hover { background: color-mix(in srgb, var(--neg) 12%, transparent); }
  #txn-table tr.flag td { background: transparent; }

  /* Day-header row only has one <td colspan="4">, so the 4-column grid
     above doesn't apply — override display:grid back to a simple flex row
     (higher specificity than the td:nth-child(1) rules above, via the
     .txn-day-header class, so those don't leak font-size/color onto it). */
  #txn-table tr.txn-day-header {
    display: block; padding: 14px 8px 6px; border-top: none;
  }
  #txn-table tr.txn-day-header td {
    display: block; padding: 0; border-top: 0;
    font-size: 11.5px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; color: var(--muted);
  }
  #txn-table tr.txn-day-header td div { display: flex; justify-content: space-between; align-items: baseline; }
}
/* ── Headline band: the page's entry point ── */
.headline {
  background: linear-gradient(135deg, color-mix(in srgb, var(--brand) 14%, var(--card)), var(--card) 70%);
  border: 1.5px solid color-mix(in srgb, var(--brand) 28%, transparent);
  border-radius: var(--radius); padding: clamp(16px, 3vw, 22px) clamp(18px, 3.5vw, 26px);
  margin-bottom: 16px; animation: rise .5s ease both;
}
.headline p { margin: 0; font-size: clamp(17px, 2.6vw, 21px); line-height: 1.45; letter-spacing: -.01em; }
.headline b { font-weight: 800; color: var(--brand); }
.headline-sub { margin-top: 7px !important; font-size: 14px !important; color: var(--muted); line-height: 1.55 !important; }
.headline-sub b { color: var(--ink); font-weight: 700; }

/* ── Net-total hero (Dime-inspired) ──
   One big typographic number instead of four equal-weight cards. Purely
   visual — no new data, reads d.stats via render() same as the existing
   stat cards right below it, which are untouched. */
.net-hero { text-align: center; margin: 4px 0 18px; animation: rise .5s ease both; }
.net-hero-label {
  display: inline-flex; align-items: center; font-size: 13px; font-weight: 600; color: var(--muted);
  background: var(--card); border: 1px solid var(--line); border-radius: 999px; padding: 4px 14px; margin-bottom: 10px;
}
.net-hero-amount { margin: 0; font-size: clamp(34px, 8vw, 46px); font-weight: 800; letter-spacing: -0.02em; }
.net-hero-amount.pos { color: var(--pos); }
.net-hero-amount.neg { color: var(--neg); }
.net-hero-sub { margin-top: 4px; font-size: 14.5px; font-weight: 700; }
.net-hero-in { color: var(--pos); }
.net-hero-out { color: var(--neg); }
.net-hero-sep { color: var(--muted); font-weight: 400; margin: 0 6px; }

/* ── Section nav ──
   No bar behind the links — each link is its own floating pill (a solid
   background + shadow, not a strip spanning the row), so what's sticky is
   a handful of buttons, not a bar that reads as a stray UI layer. */
.section-nav {
  display: flex; gap: 8px; overflow-x: auto; margin-bottom: 14px;
  padding: 2px 2px 8px; scrollbar-width: none;
  /* Sticky under the topbar so it stays reachable through a long scroll. */
  /* --topbar-h is measured in app.js: the topbar wraps to two rows on
     narrow screens, so a hard-coded offset parks this nav underneath it. */
  position: sticky; top: calc(var(--topbar-h, 66px) + 2px); z-index: 4;
}
.section-nav::-webkit-scrollbar { display: none; }
.section-nav a {
  flex: none; font-size: 13px; font-weight: 600; text-decoration: none;
  color: var(--muted); background: var(--card); border: 1.5px solid var(--line);
  border-radius: 999px; padding: 6px 13px;
  box-shadow: 0 2px 10px rgba(0,0,0,.08);
  transition: color .15s ease, background .15s ease, box-shadow .15s ease, transform .15s ease;
}
.section-nav a:hover { color: var(--brand); background: color-mix(in srgb, var(--brand) 10%, var(--card)); box-shadow: 0 3px 14px rgba(0,0,0,.12); transform: translateY(-1px); }
/* Anchor jumps must clear the sticky topbar + this nav, or the heading
   lands underneath them. */
#cats-card, #mom-card, #months-card, #subscription-card, #anomaly-card, #txn-card {
  scroll-margin-top: calc(var(--topbar-h, 66px) + var(--nav-h, 44px) + 12px);
}

/* ── KPI sparklines ── */
.spark { display: block; height: 26px; margin-top: 6px; opacity: .9; }
.spark svg { width: 100%; height: 26px; display: block; }
.spark:empty { display: none; }

/* ── Empty states ── */
.empty-state {
  font-size: 14px; color: var(--muted); line-height: 1.6;
  background: var(--bg); border: 1.5px dashed var(--line);
  border-radius: 12px; padding: 14px 16px; margin: 12px 0 0;
}

/* ── Upload-screen preview sketch ── */
.preview-peek { max-width: 680px; margin: 22px auto 0; width: 100%; animation: rise .6s .3s ease both; }
.preview-label {
  text-align: center; font-size: 12px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .08em; color: var(--muted); margin-bottom: 10px;
}
.peek {
  background: var(--card); border: 1.5px solid var(--line); border-radius: var(--radius);
  padding: 14px; box-shadow: var(--shadow);
  display: grid; gap: 12px;
  /* Muted via colour/contrast alone, not filter:blur or a mask fade — those
     read as an out-of-focus/broken image rather than a deliberate sketch.
     "Not real data" is communicated by the shapes having no numbers or
     labels at all, not by making them hard to see. */
}
.peek-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
.peek-stats span {
  height: 34px; border-radius: 9px; background: color-mix(in srgb, var(--brand) 16%, var(--bg));
  /* Pre-animate state. Transform/opacity only — no width/height tween — so
     this never triggers layout, matching the rest of the app's motion. */
  opacity: 0; transform: translateY(6px);
  transition: opacity .5s ease, transform .5s var(--spring);
}
.peek-body { display: grid; grid-template-columns: 84px 1fr; gap: 16px; align-items: center; }
.peek-donut {
  width: 84px; height: 84px; overflow: visible;
  /* overflow:visible — a popped-out segment's stroke extends past the
     viewBox's own 0-100 bounds (the pop distance was chosen assuming
     room to breathe outside it); clipping would flatten the pop back down
     right at the edge instead of showing it. */
  opacity: 0; transform: scale(.8);
  transition: opacity .5s ease, transform .5s var(--spring);
}
.peek-donut-seg {
  /* Rest position — the idle cycle below animates FROM here, so a segment
     between its own turns (or before .pk-idle ever starts, e.g. reduced-
     motion) sits exactly where the original flat gradient donut drew it. */
  transform: translate(0, 0);
}
.peek-lines { display: grid; gap: 9px; }
.peek-lines i {
  display: block; height: 9px; width: var(--w); border-radius: 999px;
  background: color-mix(in srgb, var(--brand) 26%, var(--bg));
  /* scaleX from the left edge, not a width tween — GPU-composited, and the
     bar visibly "draws" left-to-right the way a reading eye already moves. */
  transform-origin: left; transform: scaleX(0);
  transition: transform .55s var(--spring);
}
.peek-bars { display: flex; align-items: flex-end; gap: 7px; height: 62px; }
.peek-bars i {
  flex: 1; height: var(--h); border-radius: 5px 5px 0 0;
  background: linear-gradient(180deg, color-mix(in srgb, var(--pos) 55%, transparent), color-mix(in srgb, var(--pos) 12%, transparent));
  transform-origin: bottom; transform: scaleY(0);
  transition: transform .6s var(--spring);
}

/* Scroll-triggered reveal for the sketch above — see the IntersectionObserver
   in app.js (mirrors animateMonthsChartOnScroll's play-once pattern). Each
   group staggers on its own nth-child track so the eye reads stats, then
   the donut+lines, then the bars, rather than everything snapping at once.
   #peek.pk-in is the ONLY thing JS ever toggles; every other rule above is
   pure CSS, and prefers-reduced-motion already zeroes all transitions
   globally (see the app's top-level rule), so this degrades to an instant
   full-strength reveal with zero extra code for that case. */
#peek.pk-in .peek-stats span { opacity: 1; transform: translateY(0); }
#peek.pk-in .peek-donut { opacity: 1; transform: scale(1); }
#peek.pk-in .peek-lines i { transform: scaleX(1); }
#peek.pk-in .peek-bars i { transform: scaleY(1); }
#peek.pk-in .peek-stats span:nth-child(1) { transition-delay: 0ms; }
#peek.pk-in .peek-stats span:nth-child(2) { transition-delay: 60ms; }
#peek.pk-in .peek-stats span:nth-child(3) { transition-delay: 120ms; }
#peek.pk-in .peek-stats span:nth-child(4) { transition-delay: 180ms; }
#peek.pk-in .peek-donut { transition-delay: 160ms; }
#peek.pk-in .peek-lines i:nth-child(1) { transition-delay: 220ms; }
#peek.pk-in .peek-lines i:nth-child(2) { transition-delay: 260ms; }
#peek.pk-in .peek-lines i:nth-child(3) { transition-delay: 300ms; }
#peek.pk-in .peek-lines i:nth-child(4) { transition-delay: 340ms; }
#peek.pk-in .peek-lines i:nth-child(5) { transition-delay: 380ms; }
#peek.pk-in .peek-bars i:nth-child(1) { transition-delay: 380ms; }
#peek.pk-in .peek-bars i:nth-child(2) { transition-delay: 415ms; }
#peek.pk-in .peek-bars i:nth-child(3) { transition-delay: 450ms; }
#peek.pk-in .peek-bars i:nth-child(4) { transition-delay: 485ms; }
#peek.pk-in .peek-bars i:nth-child(5) { transition-delay: 520ms; }
#peek.pk-in .peek-bars i:nth-child(6) { transition-delay: 555ms; }
#peek.pk-in .peek-bars i:nth-child(7) { transition-delay: 590ms; }
#peek.pk-in .peek-bars i:nth-child(8) { transition-delay: 625ms; }

/* Idle motion — kicks in once the entrance transition finishes (JS adds
   .pk-idle ~1.25s after .pk-in; see app.js), so it never fights the
   transition above for the same `transform` property. Small amplitude,
   slow, staggered per element — reads as "quietly alive," not a gimmick.
   Bars bob like a soft equalizer, lines sway, the donut breathes; nothing
   here changes size/position enough to look like a loading indicator.
   animation-play-state is flipped to paused while #peek scrolls out of
   view (same observer that starts it), so this never spends cycles on an
   element nobody's looking at. */
@keyframes pk-bob { 0%, 100% { transform: scaleY(1); } 50% { transform: scaleY(.9); } }
@keyframes pk-sway { 0%, 100% { transform: scaleX(1); } 50% { transform: scaleX(.96); } }
/* Each segment sits at rest for 4/5 of the shared 6500ms cycle and pops
   out for its own 1300ms turn — one segment "active" at a time, matching
   how a real doughnut chart pops exactly one slice, not the whole ring
   breathing together. The 6%/15% plateau (not a bare 0%/20% snap) gives
   the pop a held moment at full extension rather than an instant flick.

   FIVE separate keyframe rules, one hardcoded translate destination each
   — each segment's own arc-bisector direction at a 7-unit pop distance in
   the SVG's 0-100 viewBox space, expressed in %. The % matters: an
   EARLIER version wrote these as bare unitless numbers (translate(6.51,
   -2.58)), on the assumption that CSS transforms on SVG shapes treat
   unitless translate() arguments as native user-space units the way the
   legacy SVG `transform` ATTRIBUTE does. They don't — confirmed by reading
   back anim.effect.getKeyframes(): the 6%/15% keyframes had NO `transform`
   entry at all, meaning the whole declaration was silently rejected as
   invalid at parse time and dropped, leaving those offsets undefined and
   interpolating as if they'd never been declared. translate()'s CSS spec
   requires a real <length-percentage>; % resolves correctly against the
   viewBox (the default transform-box for SVG shape elements) and IS valid. */
@keyframes pk-slice-pop-1 { 0%, 100% { transform: translate(0, 0); } 6%, 15% { transform: translate(6.51%, -2.58%); } 21% { transform: translate(0, 0); } }
@keyframes pk-slice-pop-2 { 0%, 100% { transform: translate(0, 0); } 6%, 15% { transform: translate(0, 7%); }         21% { transform: translate(0, 0); } }
@keyframes pk-slice-pop-3 { 0%, 100% { transform: translate(0, 0); } 6%, 15% { transform: translate(-6.78%, 1.74%); } 21% { transform: translate(0, 0); } }
@keyframes pk-slice-pop-4 { 0%, 100% { transform: translate(0, 0); } 6%, 15% { transform: translate(-5.39%, -4.46%); } 21% { transform: translate(0, 0); } }
@keyframes pk-slice-pop-5 { 0%, 100% { transform: translate(0, 0); } 6%, 15% { transform: translate(-1.74%, -6.78%); } 21% { transform: translate(0, 0); } }
#peek.pk-idle .peek-bars i { animation: pk-bob 2.6s ease-in-out infinite; transform-origin: bottom; }
#peek.pk-idle .peek-lines i { animation: pk-sway 3.2s ease-in-out infinite; transform-origin: left; }
#peek.pk-idle .peek-donut-seg { animation-duration: 6500ms; animation-timing-function: ease-in-out; animation-iteration-count: infinite; }
#peek.pk-idle .peek-donut-seg:nth-child(1) { animation-name: pk-slice-pop-1; animation-delay: 0ms; }
#peek.pk-idle .peek-donut-seg:nth-child(2) { animation-name: pk-slice-pop-2; animation-delay: 1300ms; }
#peek.pk-idle .peek-donut-seg:nth-child(3) { animation-name: pk-slice-pop-3; animation-delay: 2600ms; }
#peek.pk-idle .peek-donut-seg:nth-child(4) { animation-name: pk-slice-pop-4; animation-delay: 3900ms; }
#peek.pk-idle .peek-donut-seg:nth-child(5) { animation-name: pk-slice-pop-5; animation-delay: 5200ms; }
#peek.pk-idle .peek-bars i:nth-child(1) { animation-delay: 0ms; }
#peek.pk-idle .peek-bars i:nth-child(2) { animation-delay: 130ms; }
#peek.pk-idle .peek-bars i:nth-child(3) { animation-delay: 260ms; }
#peek.pk-idle .peek-bars i:nth-child(4) { animation-delay: 390ms; }
#peek.pk-idle .peek-bars i:nth-child(5) { animation-delay: 520ms; }
#peek.pk-idle .peek-bars i:nth-child(6) { animation-delay: 650ms; }
#peek.pk-idle .peek-bars i:nth-child(7) { animation-delay: 780ms; }
#peek.pk-idle .peek-bars i:nth-child(8) { animation-delay: 910ms; }
#peek.pk-idle .peek-lines i:nth-child(1) { animation-delay: 0ms; }
#peek.pk-idle .peek-lines i:nth-child(2) { animation-delay: 220ms; }
#peek.pk-idle .peek-lines i:nth-child(3) { animation-delay: 440ms; }
#peek.pk-idle .peek-lines i:nth-child(4) { animation-delay: 660ms; }
#peek.pk-idle .peek-lines i:nth-child(5) { animation-delay: 880ms; }
#peek.pk-idle.pk-paused .peek-bars i,
#peek.pk-idle.pk-paused .peek-lines i,
#peek.pk-idle.pk-paused .peek-donut-seg { animation-play-state: paused; }

/* "What you'll get" is a hint of the dashboard, not something worth
   scrolling past on a phone — shrunk so the dropzone (the actual next
   action) stays closer to the fold. Placed after the base .peek rules
   above (same specificity, source order decides the winner regardless
   of the media query being true — an earlier mobile override here would
   silently lose to those later, unscoped rules). */
@media (max-width: 620px) {
  .preview-peek { margin-top: 14px; }
  .preview-label { font-size: 10.5px; margin-bottom: 6px; }
  .peek { padding: 10px; gap: 8px; }
  .peek-stats { gap: 6px; }
  .peek-stats span { height: 22px; border-radius: 7px; }
  .peek-body { gap: 10px; }
  .peek-lines { gap: 6px; }
  .peek-lines i { height: 7px; }
  .peek-bars { height: 40px; gap: 5px; }
}
@media (max-width: 480px) {
  .peek-body { grid-template-columns: 52px 1fr; }
  .peek-donut { width: 52px; height: 52px; }
}

/* ── Month-over-month card ── */
.mom-head h2 { margin-bottom: 2px; }
.mom-head .muted { font-size: 13px; margin-bottom: 14px; }
.mom-hero {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 12px; margin-bottom: 20px;
}
.mom-metric {
  background: var(--bg); border: 1.5px solid var(--line); border-radius: 14px;
  padding: 13px 15px;
}
.mom-metric small { display: block; color: var(--muted); font-size: 12.5px; font-weight: 600; }
.mom-metric b { display: block; font-size: clamp(20px, 3.4vw, 25px); letter-spacing: -.02em; margin: 3px 0 5px; }
.delta { font-size: 12.5px; font-weight: 700; display: inline-block; }
.delta.is-good { color: var(--pos); }
.delta.is-bad { color: var(--neg); }
.delta.is-flat { color: var(--muted); }
.mom-sub-head { font-size: 14px; margin-bottom: 10px; color: var(--muted); }
.mom-cats { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.mom-cats li {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 10px 13px; background: var(--bg); border: 1.5px solid var(--line); border-radius: 12px;
}
.mom-cat-name { font-weight: 700; font-size: 14.5px; }
.mom-cats small { font-size: 12.5px; }
.mom-cat-delta { font-size: 13px; font-weight: 700; white-space: nowrap; text-align: right; }
.mom-cat-delta.is-up { color: var(--neg); }
.mom-cat-delta.is-down { color: var(--pos); }

/* ── Subscription total ── */
.sub-total {
  background: color-mix(in srgb, var(--pos) 10%, transparent);
  border: 1.5px solid color-mix(in srgb, var(--pos) 35%, transparent);
  border-radius: 14px; padding: 14px 16px; margin: 12px 0 14px;
}
.sub-total-label { display: block; font-size: 12.5px; font-weight: 600; color: var(--muted); }
.sub-total-main b {
  font-size: clamp(26px, 5vw, 34px); letter-spacing: -.03em; color: var(--pos);
  display: inline-block; margin-top: 2px;
}
.sub-total-unit { font-size: 14px; font-weight: 700; color: var(--pos); margin-left: 5px; }
.sub-total-meta { font-size: 12.5px; color: var(--muted); margin-top: 3px; }

/* ── Category correction ── */
.txn-hint { font-size: 13px; margin-bottom: 8px; }
.cat-filter-bar { display: flex; gap: 7px; overflow-x: auto; padding: 2px 2px 10px; scrollbar-width: none; }
.cat-filter-bar::-webkit-scrollbar { display: none; }
.cat-pill {
  flex: none; font: inherit; font-size: 13px; font-weight: 600; white-space: nowrap;
  color: var(--muted); background: var(--bg); border: 1.5px solid var(--line);
  border-radius: 999px; padding: 6px 13px; cursor: pointer;
  transition: color .15s ease, background .15s ease, border-color .15s ease, transform .15s ease;
}
.cat-pill:hover { color: var(--ink); transform: translateY(-1px); }
.cat-pill.is-active {
  color: var(--brand-ink); background: linear-gradient(135deg, var(--brand), var(--brand-2));
  border-color: transparent;
}
.cat-override-note {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  font-size: 12.5px; font-weight: 600; color: var(--brand);
  background: color-mix(in srgb, var(--brand) 10%, transparent);
  border-radius: 10px; padding: 8px 12px; margin-bottom: 10px;
}
.cat-chip-btn { font-family: inherit; border: 1.5px solid transparent; cursor: pointer; transition: box-shadow .15s ease, background .15s ease; }
.cat-chip-btn:hover { background: color-mix(in srgb, var(--brand) 22%, transparent); }
.cat-chip-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 35%, transparent); }
/* An edited category is marked so a corrected row is distinguishable from
   one the classifier got right on its own. */
.cat-chip-btn.is-overridden { border-color: currentColor; }
.cat-chip-btn.is-overridden::after { content: " ✎"; font-size: 10px; opacity: .75; }
.cat-menu {
  position: absolute; z-index: 70; width: 210px; max-width: calc(100vw - 16px);
  background: var(--card); border: 1px solid var(--line); border-radius: 14px;
  box-shadow: 0 14px 38px rgba(0,0,0,.2); padding: 6px;
  animation: pop-in .15s ease both;
}
.cat-menu-head {
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--muted); padding: 6px 10px 8px; border-bottom: 1px solid var(--line); margin-bottom: 5px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cat-menu-item {
  display: block; width: 100%; text-align: left; font: inherit; font-size: 14px;
  background: none; border: 0; border-radius: 9px; padding: 8px 10px; cursor: pointer; color: var(--ink);
}
.cat-menu-item:hover, .cat-menu-item:focus-visible { background: color-mix(in srgb, var(--brand) 12%, transparent); outline: none; }
.cat-menu-item.is-current { color: var(--brand); font-weight: 700; }
.cat-menu-item.is-current::after { content: " ✓"; }

.cat-chip { font-size: 12.5px; font-weight: 600; background: color-mix(in srgb, var(--brand) 12%, transparent); color: var(--brand); border-radius: 999px; padding: 3px 10px; white-space: nowrap; }

.muted { color: var(--muted); font-size: 15px; margin-bottom: 8px; }
.foot { text-align: center; color: var(--muted); font-size: 14px; padding: 28px 0 10px; }

/* Cross-links to my other tools. Kept the quietest thing in the footer —
   muted, smaller than the footer line above it — because this is a
   trust-first tool people hand their bank statement to, and a prominent row
   of outbound links reads as an ad. flex-wrap so the long descriptive
   anchors (descriptive on purpose: that text is what search engines read)
   wrap on a phone instead of forcing a horizontal scroll. */
.foot-tools {
  margin-top: 10px; font-size: 12.5px; line-height: 1.6;
  display: flex; flex-wrap: wrap; justify-content: center;
  align-items: baseline; gap: 2px 6px;
}
.foot-tools a {
  color: var(--muted); font-weight: 600; text-decoration: underline;
  text-underline-offset: 2px; text-decoration-thickness: 1px;
  padding: 2px 4px; border-radius: 6px;
  transition: color .2s ease, background-color .2s ease;
}
.foot-tools a:hover { color: var(--brand); background: color-mix(in srgb, var(--brand) 10%, transparent); }

/* ── Motion ── */
@keyframes rise { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: none; } }
@keyframes scan { 0%,100% { top: 6%; } 50% { top: 92%; } }
/* Timed to match .scan-line's own keyframe percentages so the icon visibly
   "reacts" as the beam passes over its midpoint — brightens and glows
   right as the line crosses it, dims again as it moves away. */
@keyframes doc-pulse {
  0%, 100% { filter: brightness(1) drop-shadow(0 0 0 transparent); opacity: .82; }
  50% { filter: brightness(1.45) drop-shadow(0 0 16px color-mix(in srgb, var(--brand) 55%, transparent)); opacity: 1; }
}
@keyframes shake { 0%,100% { transform: none; } 25% { transform: translateX(-6px); } 75% { transform: translateX(6px); } }


/* ── Theme toggle FAB (thumb zone) ── */
.btn-theme {
  position: fixed; z-index: 50;
  right: 18px; bottom: calc(18px + env(safe-area-inset-bottom));
  width: 54px; height: 54px; border-radius: 50%;
  border: 1.5px solid var(--line); background: var(--card); color: var(--ink);
  box-shadow: 0 10px 30px rgba(0, 0, 0, .25);
  cursor: pointer; display: grid; place-items: center;
  transition: transform .35s cubic-bezier(.34,1.56,.64,1), border-color .2s ease;
}
.btn-theme:hover { transform: scale(1.12); border-color: var(--brand); }
.btn-theme:active { transform: scale(.88); }

/* Mirrors .btn-theme on the opposite corner — same size/z-index/shadow so
   the two read as a matched pair rather than one being an afterthought,
   and neither ever overlaps the other regardless of viewport width. */
.btn-feedback { left: 18px; right: auto; color: var(--brand); }
.btn-feedback:hover { border-color: var(--brand); }

.feedback-type-row { display: flex; gap: 8px; margin: 4px 0 10px; }
.feedback-type {
  flex: 1; font: inherit; font-size: 13.5px; font-weight: 700;
  padding: 9px 6px; border-radius: 10px; border: 1.5px solid var(--line);
  background: var(--bg); color: var(--muted); cursor: pointer;
  transition: color .15s ease, border-color .15s ease, background .15s ease;
}
.feedback-type:hover { color: var(--ink); }
.feedback-type.is-active { color: var(--brand); border-color: var(--brand); background: color-mix(in srgb, var(--brand) 10%, transparent); }
.feedback-textarea {
  width: 100%; min-height: 100px; font: inherit; font-size: 16px;
  padding: 12px 14px; border: 1.5px solid var(--line); border-radius: 12px;
  background: var(--bg); color: var(--ink); resize: vertical;
  margin-bottom: 4px;
}
.feedback-textarea:focus { border-color: var(--brand); outline: none; }
.feedback-shake-note {
  font-size: 13px; color: var(--brand); background: color-mix(in srgb, var(--brand) 10%, transparent);
  border-radius: 10px; padding: 9px 12px; margin: 8px 0 0;
}
/* Opt-in row for shake-to-report. Only rendered on iOS, and only while the
   permission hasn't been granted — see renderShakeOptin() in app.js. */
.shake-optin {
  display: flex; align-items: center; gap: 12px;
  margin: 12px 0 0; padding: 11px 13px;
  border: 1px solid var(--line); border-radius: 12px;
  background: color-mix(in srgb, var(--card) 70%, transparent);
}
.shake-optin b { display: block; font-size: 13.5px; color: var(--ink); }
.shake-optin small { display: block; margin-top: 3px; font-size: 12px; line-height: 1.45; color: var(--muted); }
.btn-mini-action {
  flex: 0 0 auto; font: inherit; font-size: 13px; font-weight: 700;
  color: var(--brand-ink); background: linear-gradient(135deg, var(--brand), var(--brand-2));
  border: 0; border-radius: 999px; padding: 9px 16px; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  transition: transform .15s ease, opacity .15s ease;
}
.btn-mini-action:hover { transform: translateY(-1px); }
.btn-mini-action:active { transform: scale(.95); }
.btn-mini-action:disabled { opacity: .6; cursor: wait; transform: none; }
.btn-theme svg { position: absolute; transition: transform .5s cubic-bezier(.34,1.56,.64,1), opacity .35s ease; }
.ico-sun  { opacity: 0; transform: rotate(-90deg) scale(.4); }
.ico-moon { opacity: 1; transform: rotate(0) scale(1); }
html[data-theme="light"] .ico-sun  { opacity: 1; transform: rotate(0) scale(1); }
html[data-theme="light"] .ico-moon { opacity: 0; transform: rotate(90deg) scale(.4); }
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) .ico-sun  { opacity: 1; transform: rotate(0) scale(1); }
  :root:not([data-theme="dark"]) .ico-moon { opacity: 0; transform: rotate(90deg) scale(.4); }
}

/* ── History drawer ── */
.topbar-left { display: flex; align-items: center; gap: 8px; }
.btn-history {
  width: 40px; height: 40px; border-radius: 12px; border: 0;
  background: none; color: var(--muted); cursor: pointer;
  display: grid; place-items: center;
  transition: transform .3s cubic-bezier(.34,1.56,.64,1), background .2s ease, color .2s ease;
}
.btn-history:hover { background: color-mix(in srgb, var(--muted) 12%, transparent); color: var(--ink); transform: scale(1.08); }
.btn-history:active { transform: scale(.88); }
.drawer {
  position: fixed; z-index: 60; top: 0; bottom: 0; left: 0;
  width: min(320px, 86vw);
  background: var(--card); box-shadow: 0 0 60px rgba(0, 0, 0, .35);
  padding: 18px 16px calc(18px + env(safe-area-inset-bottom));
  transform: translateX(-105%);
  transition: transform .45s cubic-bezier(.34,1.56,.64,1);
  display: flex; flex-direction: column;
}
.drawer.open { transform: translateX(0); }
.drawer-head { display: flex; justify-content: space-between; align-items: center; }
.drawer-head h2 { font-size: 18px; }
.drawer-note { color: var(--muted); font-size: 13px; margin: 8px 0 12px; }
.btn-mini {
  font: inherit; font-size: 14px; font-weight: 700; color: var(--brand);
  background: color-mix(in srgb, var(--brand) 12%, transparent);
  border: 0; border-radius: 999px; padding: 8px 16px; cursor: pointer;
  transition: transform .3s cubic-bezier(.34,1.56,.64,1), background .2s ease;
}
.btn-mini:hover { transform: scale(1.07); }
.btn-mini:active { transform: scale(.9); }
.drawer-backdrop {
  position: fixed; inset: 0; z-index: 55;
  background: rgba(0, 0, 0, .45); backdrop-filter: blur(2px);
  opacity: 0; transition: opacity .3s ease;
}
.drawer-backdrop.show { opacity: 1; }
.history-list { list-style: none; overflow-y: auto; flex: 1; }
.history-list li {
  display: flex; gap: 10px; align-items: center;
  padding: 11px 10px; border-radius: 12px; cursor: pointer;
  transition: background .2s ease, transform .25s cubic-bezier(.34,1.56,.64,1);
}
.history-list li:hover { background: color-mix(in srgb, var(--brand) 8%, transparent); transform: translateX(3px); }
.history-list li:active { transform: scale(.97); }
.h-badge {
  flex: none; min-width: 44px; height: 44px; border-radius: 12px; padding: 0 6px;
  display: grid; place-items: center;
  font-size: 12px; font-weight: 800;
  background: color-mix(in srgb, var(--brand) 12%, transparent); color: var(--brand);
}
.h-meta { min-width: 0; }
.h-title { font-weight: 700; font-size: 14.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.h-sub { font-size: 12px; color: var(--muted); }
.drawer-empty { color: var(--muted); font-size: 14.5px; text-align: center; margin-top: 30px; }
