@layer theme, base, components, utilities, pfa-app, pfa-layout;

@layer pfa-app {
/* =====================================================================
   Personal Finance Analyser - visual design system
   Calm, modern, trustworthy. Light and dark themes driven by tokens.
   Colour values come from config.json (applied as CSS variables at boot);
   this file maps those into semantic tokens and lays out every component.
   ===================================================================== */

:root {
  color-scheme: light;
  /* config injects: --accent, --accent-dark, --accent-deep, --good, --warn,
     --ink, --muted, --background, --surface, --line, and the --dark-* set.
     Sensible fallbacks below keep the app usable even before config loads. */
  --accent: #0f6cbd;
  --accent-dark: #0a4c87;
  --accent-deep: #0a4c87;
  --good: #1e7a46;
  --warn: #b4460e;
  --ink: #1b1b1f;
  --muted: #5b5b66;
  --background: #ffffff;
  --surface: #f5f7fa;
  --line: #e3e6ea;
  --dark-background: #0c0e13;
  --dark-surface: #222739;
  --dark-ink: #eceef2;
  --dark-muted: #9aa0ab;
  --dark-line: #333a4d;

  /* semantic tokens (light) */
  --bg: var(--background);
  --panel: #ffffff;
  --panel-2: var(--surface);
  --fg: var(--ink);
  --dim: var(--muted);
  --edge: var(--line);
  --accent-ink: #ffffff;
  /* Text-only companions to --good / --warn.
     Those two are dual-purpose: they fill pills and bars AND colour words. A
     fill wants a saturated colour; text on a dark panel wants a light one, and
     one token cannot be both - so fills keep --good/--warn and words use these.
     In light mode they are the same colour; the dark blocks below give them
     light-on-dark values. */
  --good-ink: var(--good);
  --warn-ink: var(--warn);
  /* --muted is one of the primitives config.json injects as an inline style on
     :root at boot, and an inline style beats any stylesheet rule - which is why
     the dark theme remaps ALIASES (--fg, --dim) and never the primitives
     themselves. Text that wants the muted tone reads this alias, so the dark
     theme can actually reach it. */
  --muted-ink: var(--muted);
  /* Accent used as TEXT, as distinct from --accent (the fill) and
     --accent-ink (the text drawn ON that fill). Three different jobs that were
     sharing two names. In dark mode #0F6CBD scores 2.99 as text on a panel, so
     every link-coloured word in the app - merchant names included - sat under
     AA while looking correct in light mode. */
  --accent-text: var(--accent);
  --shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 6px 18px rgba(16, 24, 40, 0.06);
  --shadow-lg: 0 8px 30px rgba(16, 24, 40, 0.14);
  --radius: var(--r-card);
  --radius-sm: var(--r-inner);
  --radius-pill: 999px;
  --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  --tap-min: 40px;
  --tap-lg: 44px;
  --dock-edge: 22px;
  --dock-nav: 80px;
  --dock-toast: 84px;
  --dock-raised: 96px;
  --dock-nav-toast: 152px;
  /* 8pt-based step scale: the ONLY spacing values the card system uses. */
  --s0: 2px;
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 20px;
  --s6: 24px;
  --s7: 32px;
  --s8: 40px;

  /* The height of a single-line text field. Declared rather than left to fall
     out of font-size plus padding, so anything that must line up WITH a field -
     a count, a button, a unit - can ask for the same number instead of
     guessing it. */
  --field-h: 40px;

  /* Inline icon box. Every icon in the set is authored in a 24-unit viewBox and
     declares width="16", so any CSS that resizes one to 15px scales a stroked
     path by 0.625 and puts its edges between pixels. One token, one size,
     everywhere an icon sits inside a line of text. */
  --icon-inline: 16px;

  --card-pad-y: var(--s5); /* 20px */
  --card-pad-x: var(--s6); /* 24px */
  --card-gap: var(--s4); /* 16px */
  --up: #b4460e;
  --down: #1e7a46; /* spending up = warm, down = calm green */
  /* Semantic MONEY-DIRECTION tokens (bank domain). The app's colour language is
     valence-based: green = money toward the person / position improved, warm =
     money away / cost. On an ACCOUNT the primary metric is the balance, so money
     IN (balance up) is green and Cash outflow (balance down) is warm. These name
     that meaning outright, so the account in/out indicators stop borrowing the
     spending-valenced --down / --up names (where --down is confusingly the
     green). */
  --flow-in: var(--down); /* money in  - balance up   - calm green */
  --flow-out: var(--up); /* Cash outflow - balance down - warm       */
  --tip-bg: #14161c;
  --tip-fg: #f2f4f8;
}

@media (max-width: 639px) {
  :root {
    --card-pad-x: 16px;
    --card-pad-y: 16px;
  }
}

[data-theme='dark'] {
  color-scheme: dark;
  --bg: var(--dark-background);
  --panel: var(--dark-surface);
  --panel-2: #232733;
  --fg: var(--dark-ink);
  --dim: var(--dark-muted);
  --edge: var(--dark-line);
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 22px rgba(0, 0, 0, 0.35);
  --shadow-lg: 0 10px 34px rgba(0, 0, 0, 0.5);
  --up: #f0915f;
  --down: #56c98a;
  /* Measured against --panel (#222739), not chosen by eye: the light theme's
     --muted (#5b5b66) scores 2.40 there, and --good / --warn score 3.01 / 2.93
     as text - all well under the 4.5 that AA asks for. They were the only
     semantic tokens the dark theme never remapped, which is exactly why small
     labels, "all reconcile" and the status pills read faint in dark mode and
     fine in light. Fills are untouched; only the ink changes. */
  --muted-ink: #8b9099;
  --accent-text: #4da3e8;
  --good-ink: #56c98a;
  --warn-ink: #f0915f;
  --tip-bg: #eef1f6;
  --tip-fg: #14161c;
}
@media (prefers-color-scheme: dark) {
  [data-theme='auto'] {
    color-scheme: dark;
    --bg: var(--dark-background);
    --panel: var(--dark-surface);
    --panel-2: #232733;
    --fg: var(--dark-ink);
    --dim: var(--dark-muted);
    --edge: var(--dark-line);
    --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 22px rgba(0, 0, 0, 0.35);
    /* Was missing here and present in [data-theme='dark'] - the two blocks
       must stay identical, so "Dark" and "Auto at night" cannot render
       differently. */
    --shadow-lg: 0 10px 34px rgba(0, 0, 0, 0.5);
    --up: #f0915f;
    --down: #56c98a;
    /* Measured against --panel (#222739), not chosen by eye: the light theme's
       --muted (#5b5b66) scores 2.40 there, and --good / --warn score 3.01 / 2.93
       as text - all well under the 4.5 that AA asks for. They were the only
       semantic tokens the dark theme never remapped, which is exactly why small
       labels, "all reconcile" and the status pills read faint in dark mode and
       fine in light. Fills are untouched; only the ink changes. */
    --muted-ink: #8b9099;
    --accent-text: #4da3e8;
    --good-ink: #56c98a;
    --warn-ink: #f0915f;
    --tip-bg: #eef1f6;
    --tip-fg: #14161c;
  }
}

* {
  box-sizing: border-box;
}
html {
  height: 100%;
}
/* min-height (not height) lets the body grow with content so the sticky
   toolbar/period selector stay pinned through long lists instead of releasing
   after one screen. */
body {
  min-height: 100%;
}
body {
  margin: 0;
  font-family:
    'Segoe UI',
    system-ui,
    -apple-system,
    BlinkMacSystemFont,
    Roboto,
    Helvetica,
    Arial,
    sans-serif;
  font-size: 14px;
  line-height: 1.5;
  color: var(--fg);
  background:
    radial-gradient(
      1100px 720px at 72% -12%,
      color-mix(in srgb, var(--accent) 13%, transparent),
      transparent
    ),
    radial-gradient(
      900px 640px at 10% 22%,
      color-mix(in srgb, var(--accent-deep) 9%, transparent),
      transparent
    ),
    radial-gradient(
      1000px 720px at 90% 96%,
      color-mix(in srgb, var(--accent-dark) 8%, transparent),
      transparent
    ),
    var(--bg);
  background-attachment: fixed;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  padding-bottom: calc(var(--s8) + var(--s2) + max(var(--dock-bottom, 0), env(safe-area-inset-bottom)));
}
body.dropping::after {
  content: 'Drop your statement PDFs to add them';
  position: fixed;
  inset: 14px;
  z-index: 60;
  display: grid;
  place-items: center;
  border: 2px dashed var(--accent);
  border-radius: 18px;
  background: color-mix(in srgb, var(--accent) 10%, var(--bg));
  color: var(--accent-dark);
  font-weight: 650;
  font-size: 18px;
}
/* The base button reset.

   `font` and `color` were inherited here but `background` was not, so every
   button in the app that did not declare its own fill fell through to the user
   agent's default button surface. That surface is near-invisible against the
   dark theme and a muddy grey against the light one - which is how the Plan
   card's "100% of take-home" pill came to render as a grey blob in light mode
   while looking correct in dark, for as long as only one theme was checked.

   Declaring it transparent here means a button shows a fill only when one is
   asked for. Guarded in tests/wiring_contracts_proof.mjs. */
button {
  font: inherit;
  color: inherit;
  background: transparent;
}
h2,
h3 {
  margin: 0;
}
/* Honour the hidden attribute even on flex/grid display elements. */
[hidden] {
  display: none !important;
}

/* ---------- Skip link ---------- */
/* Keyboard-only "Skip to content" jump. Held off-screen until it is focused,
   then it appears at the top-left. Without this rule the anchor rendered as
   ordinary visible underlined text at the very top of the page. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 10px 14px;
  background: var(--panel);
  color: var(--fg);
  border: 1px solid var(--edge);
  border-radius: 8px;
  font-size: 13.5px;
  font-weight: 650;
  text-decoration: none;
}
.skip-link:focus {
  left: 12px;
  top: 12px;
}

/* ---------- Top bar ---------- */
.topbar-stack {
  position: sticky;
  top: 0;
  z-index: 30;
}
.topbar {
  position: relative;
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  min-height: calc(var(--s8) + var(--s6) + env(safe-area-inset-top));
  padding: calc(var(--s2) + env(safe-area-inset-top))
    calc(clamp(14px, 3vw, 28px) + env(safe-area-inset-right)) var(--s2)
    calc(clamp(14px, 3vw, 28px) + env(safe-area-inset-left));
  border-bottom: 1px solid color-mix(in srgb, var(--fg) 8%, transparent);
  box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 12%, transparent);
}
.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 700;
  letter-spacing: 0.2px;
  min-width: 0;
  flex: 1 1 auto;
}
@media (max-width: 639px) {
  .brand {
    flex-wrap: wrap;
    gap: var(--s1) var(--s2);
  }
}
@media (min-width: 1100px) {
  .topbar {
    min-height: calc(max(var(--s8) + var(--s6), var(--t-body) * 1.5 + var(--s8) + var(--s0)) + env(safe-area-inset-top));
  }
}
.brand-mark {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: 10px;
  color: var(--accent-ink);
  background: linear-gradient(140deg, var(--accent), var(--accent-dark));
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.24),
    0 8px 24px -16px var(--accent);
}
.brand-name {
  font-size: var(--t-title);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.top-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 9px 14px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--hairline);
  background: var(--panel);
  color: var(--fg);
  font-weight: 650;
  font-size: 13.5px;
  cursor: pointer;
  transition:
    background 0.15s,
    border-color 0.15s,
    transform 0.08s,
    box-shadow 0.15s;
  white-space: nowrap;
}
.btn:hover {
  border-color: color-mix(in srgb, var(--accent) 24%, var(--edge));
  background: var(--soft-hover);
}
.btn:active {
  transform: translateY(1px);
}
:where(.btn, .icon-btn):disabled {
  cursor: not-allowed;
  opacity: 0.46;
  filter: none;
  transform: none;
  box-shadow: none;
}
:where(.btn, .icon-btn):disabled:hover {
  border-color: var(--edge);
  background: var(--panel);
  filter: none;
}
.btn.primary {
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--accent) 92%, #fff),
    var(--accent-dark)
  );
  color: var(--accent-ink);
  border-color: transparent;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.28),
    0 10px 26px -18px var(--accent);
}
.btn.primary:hover {
  filter: brightness(1.03) saturate(1.02);
}
.btn.sm {
  padding: 6px 11px;
  font-size: 12.5px;
}
.btn.lg {
  padding: 12px 20px;
  font-size: 15px;
}
.btn.ghost {
  background: transparent;
  border-color: var(--edge);
}
.btn.ghost:hover {
  background: var(--soft-hover);
  border-color: color-mix(in srgb, var(--accent) 24%, var(--edge));
}
.btn.icon-btn {
  min-width: 62px;
  justify-content: center;
}
.btn-ic {
  display: inline-flex;
}
/* Secondary-action glyphs (Export, theme) - hidden on desktop, shown on phones
   so those controls collapse to icons and the header stays a single row. */
.nav-ic {
  display: none;
}
#theme-btn .nav-ic {
  display: inline-flex;
}
.caret {
  display: inline-flex;
  opacity: 0.7;
}
@media (max-width: 639px) {
  .btn .btn-label {
    display: none;
  }
  .btn.primary .btn-label {
    display: none;
  }
}
#add-btn.btn.primary {
  min-width: 44px;
  justify-content: center;
  padding-left: 12px;
  padding-right: 12px;
}

.file-trigger {
  position: relative;
  overflow: hidden;
}
.file-trigger-input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.file-trigger:has(> .file-trigger-input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.file-menu-item {
  display: block;
}

.menu-wrap {
  position: relative;
}
.menu {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  z-index: var(--z-menu);
  min-width: min(320px, calc(100vw - 2 * var(--s3)));
  padding: var(--s2);
  border-radius: 14px;
  background: var(--panel);
  border: 1px solid var(--edge);
  box-shadow: var(--shadow-lg);
}
.menu-item {
  display: flex;
  align-items: center;
  gap: var(--s2);
  width: 100%;
  text-align: left;
  padding: var(--s2) var(--s3);
  border: 0;
  background: transparent;
  border-radius: 10px;
  cursor: pointer;
  font-size: 13.5px;
  white-space: nowrap;
}
.menu-item:hover {
  background: var(--soft-hover);
}
@media (max-width: 639px) {
  .menu {
    min-width: min(
      280px,
      calc(100vw - 24px - env(safe-area-inset-left) - env(safe-area-inset-right))
    );
  }
  .menu-item {
    min-height: var(--tap-lg);
  }
}
@media (pointer: coarse) and (min-width: 640px) {
  .menu-item {
    min-height: var(--tap-lg);
  }
}
.menu-ic {
  display: inline-flex;
  flex: none;
  width: 16px;
  height: 16px;
  color: var(--dim);
}
.menu-label {
  padding: var(--s2) var(--s3) var(--s1);
  font-size: 11px;
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: 0.3px;
  color: var(--dim);
}
.menu-sep {
  height: 1px;
  margin: var(--s2);
  background: var(--edge);
}

.period-bar {
  position: sticky;
  top: var(--stack-h, 0px);
  z-index: calc(var(--z-sticky-chrome, 30) - 1);
  background: var(--panel);
  box-sizing: border-box;
  width: 100%;
  max-width: var(--workspace-max);
  margin-inline: auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3);
  padding: var(--s3) calc(var(--s5) + env(safe-area-inset-right)) var(--s2)
    calc(var(--s5) + env(safe-area-inset-left));
  border-bottom: 1px solid color-mix(in srgb, var(--fg) 6%, transparent);
}
.period-bar:empty {
  display: none;
}
.period-left {
  display: flex;
  align-items: center;
  gap: 8px;
}
.period-icon {
  color: var(--dim);
  display: inline-flex;
  opacity: 0.72;
}
.period-select,
select.mini {
  -webkit-appearance: none;
  appearance: none;
  padding: 8px 30px 8px 12px;
  border-radius: 10px;
  border: 1px solid var(--edge);
  background: var(--panel);
  color: var(--fg);
  font-weight: 650;
  cursor: pointer;
  background-image: var(--select-chevron);
  background-repeat: no-repeat;
  background-position: right 10px center;
}
.mini {
  font-weight: 550;
  padding-block: 6px;
}
.period-range {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* ---------- Layout + cards ---------- */
.wrap {
  max-width: 1240px;
  margin: 0 auto;
  padding: 20px calc(clamp(14px, 3vw, 28px) + env(safe-area-inset-right)) 40px
    calc(clamp(14px, 3vw, 28px) + env(safe-area-inset-left));
  display: flex;
  flex-direction: column;
  gap: var(--card-gap);
}

/* The view container carries tabindex="-1" so a tab switch can move focus into
   the new view for a screen reader. Nothing can TAB to it - only the app can
   focus it - but the browser still paints its default focus ring, which here
   meant a 1180x1150 amber rectangle drawn around the entire page after every
   tab change, in both themes and on first load. The announcement is what this
   focus is for; the ring is noise. Roving-tabindex controls (.ledger-tab) are
   deliberately NOT covered: those a person really does move focus between. */
.wrap[tabindex='-1']:focus,
.wrap[tabindex='-1']:focus-visible {
  outline: none;
}
.card {
  background: var(--panel);
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.card-title svg {
  color: var(--accent-text);
}
.card-fitness p.muted.small {
  font-style: italic;
}
.muted {
  color: color-mix(in srgb, var(--dim) 92%, var(--fg));
}
.small {
  font-size: var(--t-label);
  line-height: 1.45;
}
.pad {
  padding: 6px 2px;
}
.strong {
  font-weight: 680;
}
.swatch {
  display: inline-block;
  width: 11px;
  height: 11px;
  border-radius: 3px;
  vertical-align: middle;
  margin-right: 7px;
}
.swatch.sm {
  width: 9px;
  height: 9px;
  margin-right: 6px;
}
.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  font-size: 12px;
  font-weight: 650;
}
.pill.caution {
  color: var(--up);
  background: color-mix(in srgb, var(--up) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--up) 35%, transparent);
}
.pill svg {
  width: 13px;
  height: 13px;
}

.hero-figure {
  min-width: 260px;
}
.fact-value {
  font-size: 18px;
  font-weight: 720;
  display: flex;
  align-items: baseline;
  font-variant-numeric: tabular-nums lining-nums;
}
}
