/* Dashboard. Product register: the density tier, not the brand tier.
   tokens.css must be loaded before this file. */

/* style.css owns this reset for the marketing page, and the dashboard
   deliberately does not load style.css, so it needs its own copy.
   Without it every padded, full-width control overflows its column. */
*, *::before, *::after { box-sizing: border-box; }

body.app, body.login {
  margin: 0;
  background: var(--obsidian);
  color: var(--on-obsidian-70);
  font-family: var(--font-text);
  /* 380, not the marketing page's 300. "One step lighter on dark" was
     written for display sizes; at 15px on obsidian a 300 loses its
     stroke and the hint under every heading went grey and thin. */
  font-variation-settings: "opsz" 14, "wght" 380;
  font-size: var(--t-app-body);
  line-height: 1.6;
  overflow-x: clip;
}

/* ============================================================
   The ground

   Static, not drifting. A moving light behind a transaction table is
   noise; the marketing page keeps that behaviour. What the dashboard
   has instead is a lit block: three fixed lights, one per destination,
   refracting through the obsidian. Four layers, bottom to top, all on
   one fixed, pointer-events:none element so scrolling content never
   repaints because of them:

   1. the block: a vignette that lifts the middle of the ground one
      step, plus two fracture planes, hard-edged diagonal facets a hair
      lighter or darker than the ground, the conchoidal faces DESIGN.md
      describes
   2. the three lights, static spheres at the top right (spend), the
      left (tax) and the floor (save)
   3. the caustics canvas, drawn once by app/components/ground.js:
      refracted ribbons and fracture hairlines in each light's colour
   4. grain, so the gradients stop banding and the surface reads as
      glass rather than a screen
   ============================================================ */
.app__ground {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  contain: strict;
  /* The deeper block (D2, 2026-09-14). The first version lifted the
     centre to obsidian-2 and read too bright behind a daily-use screen.
     Darkening by dimming the lights drifted back to the flat look, so
     the ground sinks instead: no centre lift, obsidian falling to
     obsidian-0 at the edges, and the lights kept close to full strength
     so the colour survives. */
  background:
    /* 2. the lights */
    radial-gradient(circle 42vmax at 84% 2%,
      color-mix(in oklch, var(--light-spend) 22%, transparent) 0%, transparent 68%),
    radial-gradient(circle 36vmax at 4% 42%,
      color-mix(in oklch, var(--light-tax) 12%, transparent) 0%, transparent 68%),
    radial-gradient(circle 40vmax at 60% 100%,
      color-mix(in oklch, var(--light-save) 14%, transparent) 0%, transparent 68%),
    /* 1. the block: two fracture planes, then the vignette */
    linear-gradient(-24deg,
      transparent 0 44%,
      oklch(100% 0 0 / 0.022) 44% 45%,
      oklch(100% 0 0 / 0.010) 45% 100%),
    linear-gradient(62deg,
      oklch(0% 0 0 / 0.20) 0 31%,
      oklch(100% 0 0 / 0.020) 31% 31.4%,
      transparent 31.4% 100%),
    radial-gradient(ellipse 90% 70% at 50% 40%, var(--obsidian) 0%, var(--obsidian-0) 100%);
}

.app__caustics {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  opacity: 0.8;
}

/* 4. grain. SVG turbulence, tiled, blended over the top. Opacity is the
   only dial here; above 0.06 it starts to read as dirt. */
.app__ground::after {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0.045;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

.app__header, .app__main, .app__footer { position: relative; z-index: 1; }

.app__header {
  display: flex;
  align-items: center;
  gap: var(--s6);
  max-width: var(--app-max);
  margin-inline: auto;
  padding: var(--s5) var(--app-gutter);
}

.app__nav { display: flex; gap: var(--s5); margin-left: auto; }

/* padding-block s3, not s2: at s2 these measured 28px tall and sign-out
   measured 19px, under the 24px floor SC 2.5.8 sets for a target. They
   are the dashboard's whole navigation, so they get a real hit area
   rather than relying on the spacing exemption. */
.app__nav a {
  color: var(--on-obsidian-70);
  text-decoration: none;
  font-size: var(--t-app-meta);
  font-family: var(--font-num);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding-block: var(--s3);
  border-bottom: 1px solid transparent;
}

.app__nav a:hover { color: var(--on-obsidian); }

.app__nav a[aria-current="page"] {
  color: var(--on-obsidian);
  border-bottom-color: var(--light);
}

.app__signout {
  color: var(--on-obsidian-70);
  font-size: var(--t-app-meta);
  font-family: var(--font-num);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  text-decoration: none;
  padding: var(--s3) var(--s4);
  border-radius: var(--r-s);
  border: 1px solid transparent;
}

.app__signout:hover {
  color: var(--on-obsidian);
  border-color: color-mix(in oklch, var(--facet-edge) 60%, transparent);
}

.app__main {
  max-width: var(--app-max);
  margin-inline: auto;
  padding: var(--s6) var(--app-gutter) var(--s9);
}

.app__footer {
  max-width: var(--app-max);
  margin-inline: auto;
  padding: var(--s7) var(--app-gutter);
  border-top: 1px solid color-mix(in oklch, var(--facet-edge) 30%, transparent);
}

.app-fine { color: var(--on-obsidian-70); opacity: 0.75; max-width: 68ch; font-size: var(--t-app-meta); }

/* Grouping by surface and spacing, not by boxing everything.

   Cut into the block, not laid on it. The ground is lit, so a panel is
   the one place the light does not reach: a facet a step darker than
   what surrounds it, with a hairline where the cut catches the light.
   The previous build faded each panel from obsidian-2 down to the page
   colour, which left the bottom two thirds of every panel invisible. */
.app-panel {
  background: color-mix(in oklch, var(--obsidian-0) 82%, transparent);
  border: 1px solid color-mix(in oklch, var(--facet-edge) 30%, transparent);
  border-radius: var(--r-app);
  padding: var(--s6);
}

.app-label {
  display: block;
  font-family: var(--font-num);
  font-size: var(--t-app-meta);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--on-obsidian-70);
  margin-bottom: var(--s2);
}

.app-input {
  width: 100%;
  font-family: var(--font-text);
  font-size: var(--t-app-body);
  padding: var(--s3) var(--s4);
  border: 1px solid color-mix(in oklch, var(--facet-edge) 70%, transparent);
  border-radius: var(--r-s);
  /* A full step above the panel it sits in. At 70% obsidian-2 the fill
     measured the same as the panel and only the hairline drew the field. */
  background: var(--obsidian-2);
  color: var(--on-obsidian);
  margin-bottom: var(--s5);
}

/* A real outline, not only a border tint plus a blurred bloom. Those
   two together are close to colour-only focus indication, and these are
   the two selects and three inputs the whole dashboard is driven from. */
.app-input:focus {
  outline: 2px solid var(--light);
  outline-offset: 3px;
  border-color: var(--light);
  box-shadow: 0 0 24px -10px color-mix(in oklch, var(--light) 80%, transparent);
}

.app-btn {
  font-family: var(--font-num);
  font-size: var(--t-app-meta);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: var(--s3) var(--s6);
  border-radius: var(--r-s);
  border: 1px solid var(--on-obsidian);
  background: transparent;
  color: var(--on-obsidian);
  cursor: pointer;
  transition: background-color var(--d-fast) var(--e-out),
              color var(--d-fast) var(--e-out),
              transform var(--d-fast) var(--e-out);
}

.app-btn:hover { background: var(--on-obsidian); color: var(--obsidian); }
.app-btn:active { transform: translateY(1px); }
.app-btn--primary { background: var(--on-obsidian); color: var(--obsidian); }
.app-btn--primary:hover { background: transparent; color: var(--on-obsidian); }

/* Duplicated from style.css for the same reason as .wordmark below: the
   dashboard does not load the marketing stylesheet, and a product surface
   without a focus ring is a keyboard trap in everything but name. */
:focus-visible {
  outline: 2px solid var(--light);
  outline-offset: 3px;
}

.u-visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Duplicated from style.css for the same reason as .wordmark: the
   dashboard does not load the marketing stylesheet. */
.skip {
  position: fixed;
  top: var(--s4);
  left: var(--s4);
  z-index: 100;
  padding: var(--s3) var(--s5);
  border-radius: var(--r-s);
  background: var(--on-obsidian);
  color: var(--obsidian);
  font-family: var(--font-num);
  font-size: var(--t-app-meta);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  clip-path: inset(50%);
  opacity: 0;
  pointer-events: none;
}

.skip:focus {
  clip-path: none;
  opacity: 1;
  pointer-events: auto;
}
.app__main:focus, .login__panel:focus { outline: none; }

.app-skeleton {
  background: color-mix(in oklch, var(--obsidian-3) 70%, transparent);
  border-radius: var(--r-s);
  height: 1em;
}

/* Duplicated from style.css: the dashboard does not load the marketing
   stylesheet, and linking it would drag the brand-register type scale
   into a product surface. */
.wordmark {
  display: inline-flex;
  align-items: center;
  gap: var(--s3);
  font-family: var(--font-display);
  font-variation-settings: "opsz" 24, "wght" 700;
  font-size: 1.125rem;
  letter-spacing: -0.01em;
  color: var(--on-obsidian);
  text-decoration: none;
  margin: 0;
}

.wordmark__mark { width: 1.45em; height: 1.45em; flex: none; }

/* Login */
/* Same ground as the dashboard, so logging in does not change rooms.

   Phone first: the form sits alone in a cut facet, centred, exactly like
   a dashboard panel. From 768px the page splits 5 / 7: the form comes out
   of its facet into a left rail and the right seven columns hold the
   milled card from the hero sequence, the one product image this site
   has. The card is a <picture> whose only source is gated on the same
   breakpoint, so phones never download it. */
.login {
  display: grid;
  place-items: center;
  min-height: 100dvh;
  padding: var(--s6) var(--app-gutter);
}

.login__panel {
  position: relative;
  z-index: 1;
  width: min(440px, 100%);
  display: grid;
  gap: var(--s4);
  padding: var(--s7) var(--s6);
  background: color-mix(in oklch, var(--obsidian-0) 82%, transparent);
  border: 1px solid color-mix(in oklch, var(--facet-edge) 30%, transparent);
  border-top-color: color-mix(in oklch, var(--light-spend) 55%, transparent);
  border-radius: var(--r-l);
}

.login__stage { display: none; }

.login__title { font-family: var(--font-display); font-size: var(--t-app-hero);
  font-variation-settings: "opsz" 48, "wght" 700; color: var(--on-obsidian);
  margin: var(--s5) 0 0; }

@media (min-width: 768px) {
  .login {
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    place-items: stretch;
    padding: 0;
  }

  .login__panel {
    align-self: center;
    justify-self: end;
    width: min(440px, 100%);
    padding: var(--s7) var(--app-gutter);
    background: none;
    border: 0;
    border-radius: 0;
  }

  .login__title { font-size: clamp(2.5rem, 5vw, 4.25rem); }

  .login__stage {
    position: relative;
    z-index: 1;
    display: grid;
    place-items: center;
    overflow: hidden;
    pointer-events: none;
  }

  /* Sized by height, because the poster is taller than it is wide and a
     width-driven size overflowed short laptop screens. The tilt is the
     only transform, and it is static. */
  .login__poster {
    display: block;
    width: auto;
    height: min(78dvh, 700px);
    transform: rotate(-4deg);
    filter: drop-shadow(0 60px 80px oklch(0% 0 0 / 0.7));
  }
}

.login__demo {
  color: var(--light);
  font-family: var(--font-num);
  font-size: var(--t-app-meta);
  letter-spacing: 0.08em;
  margin: 0;
}

.login__form { margin-top: var(--s5); }
.login__form .app-btn { width: 100%; }

.app-h1 {
  font-family: var(--font-display);
  font-size: var(--t-app-hero);
  font-variation-settings: "opsz" 48, "wght" 700;
  color: var(--on-obsidian);
  margin: 0 0 var(--s6);
  line-height: 1.05;
}

@media (max-width: 767px) {
  .app__header { flex-wrap: wrap; gap: var(--s4); }
  .app__nav { width: 100%; margin-left: 0; overflow-x: auto; }
}

/* The card is the one object on the page allowed hard corners at full
   scale; it is a different, harder material than the surfaces around it.
   At product scale it takes a small radius so it reads as a real card. */
.cardv { display: grid; gap: var(--s4); justify-items: start; perspective: 1200px; }

.cardv__inner {
  position: relative;
  width: min(360px, 100%);
  aspect-ratio: 1.586;
  transform-style: preserve-3d;
  transition: transform var(--d-slow) var(--e-out);
}

.cardv__inner.is-flipped { transform: rotateY(180deg); }

.cardv__face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  border-radius: var(--r-m);
  padding: var(--s5);
  display: flex;
  flex-direction: column;
  background:
    linear-gradient(152deg,
      var(--metal-1) 0%,
      var(--metal-2) 46%,
      var(--metal-3) 100%);
  border: 1px solid color-mix(in oklch, var(--facet-edge) 60%, transparent);
  color: var(--on-obsidian);
}

.cardv__back { transform: rotateY(180deg); }
/* Freezing fades. setFrozen recomputes the ledger but announces on the ui
   channel, so the card is patched instead of rebuilt, and the face, the
   chip and the note all cross the same 400ms beat. The earlier build
   froze instantly for exactly the reason a rebuilt element always does:
   it has no previous state to transition from. */
.cardv__face { transition: filter var(--d-base) var(--e-out); }
.cardv.is-frozen .cardv__face { filter: saturate(0.35) brightness(0.7); }

.cardv__top { display: flex; align-items: center; justify-content: space-between; }

.cardv__network, .cardv__meta {
  font-family: var(--font-num);
  font-size: var(--t-app-meta);
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

/* In the markup whether or not the card is frozen. It needs a from-state
   to fade from, and holding its width stops the card header reflowing at
   the moment the chip appears. */
.cardv__chip {
  font-family: var(--font-num);
  font-size: var(--t-app-meta);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--light-spend);
  border: 1px solid currentColor;
  border-radius: 999px;
  padding: 2px var(--s3);
  opacity: 0;
  transform: scale(0.92);
  transition: opacity var(--d-base) var(--e-out),
              transform var(--d-base) var(--e-out);
}

.cardv.is-frozen .cardv__chip { opacity: 1; transform: none; }

/* Keeps its line while hidden. Fading the note is motion; resizing the
   panel underneath it is layout, and layout does not animate here. */
.cardv__note {
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--d-base) var(--e-out);
}

.cardv__note.is-on { opacity: 1; pointer-events: auto; }

.cardv__mark { width: 34px; height: 34px; margin-top: var(--s5); flex: none; }

/* At phone width the card is about 174px tall and its face content was
   taller than that, so the flex column shrank the mark to roughly 12px.
   flex: none stops the squeeze; the tighter padding and mark make the
   content fit instead of overflowing. */
@media (max-width: 767px) {
  .cardv__face { padding: var(--s4); }
  .cardv__mark { width: 28px; height: 28px; margin-top: var(--s3); }
  /* The back face's stripe bleeds to the edges by the face padding, so it
     follows the padding down or it would stick out 8px each side. */
  .cardv__back .cardv__stripe { margin-inline: calc(var(--s4) * -1); }
}

.cardv__pan {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-size: 1.0625rem;
  letter-spacing: 0.06em;
  margin: auto 0 var(--s4);
}

.cardv__bottom {
  display: flex;
  justify-content: space-between;
  font-family: var(--font-num);
  font-size: var(--t-app-meta);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.cardv__stripe {
  height: 42px;
  margin: var(--s4) calc(var(--s5) * -1) var(--s5);
  background: var(--obsidian);
}

/* Home view */
.app-h2 {
  font-family: var(--font-display);
  font-size: var(--t-app-h2);
  font-variation-settings: "opsz" 24, "wght" 700;
  color: var(--on-obsidian);
  margin: 0 0 var(--s3);
}

.app-hint { color: var(--on-obsidian-70); margin: 0 0 var(--s5); }
.app-error { color: var(--light-tax); font-family: var(--font-num); font-size: var(--t-app-meta); min-height: 1.2em; margin: 0 0 var(--s3); }
.app-error:empty { visibility: hidden; }

/* ============================================================
   Shared screen pieces

   Page head and panel head are defined with Insights below; these are
   the pieces Home and Card add to them.
   ============================================================ */
.panel-head .app-hint { margin: var(--s1) 0 0; max-width: 48ch; }

/* A quiet way out of a panel, in its header. The first build used
   underlined uppercase accent links at the bottom of each panel, which
   were the loudest text in the panel and sat nowhere near its title. */
.panel-link {
  font-family: var(--font-num);
  font-size: var(--t-app-meta);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--on-obsidian-70);
  text-decoration: none;
  padding: var(--s2) 0;
  white-space: nowrap;
  transition: color var(--d-fast) var(--e-out);
}

.panel-link:hover, .panel-link:focus-visible { color: var(--light-spend); }

.grid-5-7 {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: var(--s5);
  margin-bottom: var(--s5);
  align-items: start;
}

@media (max-width: 899px) {
  .grid-5-7 { grid-template-columns: minmax(0, 1fr); }
}

.actions { display: flex; flex-wrap: wrap; gap: var(--s3); margin-top: var(--s5); }

.status {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s3);
  margin: var(--s4) 0 0;
  font-family: var(--font-num);
  font-size: var(--t-app-meta);
  color: var(--on-obsidian-70);
}

/* Active in the save light, frozen in the spend light: the same colour the
   Frozen chip on the card face already uses. */
.status-chip {
  font-family: var(--font-num);
  font-size: var(--t-app-meta);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--light-save);
  border: 1px solid currentColor;
  border-radius: 999px;
  padding: 2px var(--s3);
  white-space: nowrap;
  transition: color var(--d-base) var(--e-out);
}

.status-chip.is-frozen { color: var(--light-spend); }

/* A destination's name in its own light, with a dot so the colour has a
   mark to sit on. The name is always written; the dot is never alone. */
.dest-label {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  margin: 0;
  font-family: var(--font-num);
  font-size: var(--t-app-meta);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--light);
}

.dest-label::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: currentColor;
}

/* ============================================================
   Home: your split

   Balances and split rates are one panel. The first build had three
   identical balance tiles with 64px figures, the hero-metric row DESIGN.md
   bans, and a separate rates form that never showed spend's share.
   ============================================================ */
.split { list-style: none; margin: 0; padding: 0; }

.split__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 7.5rem 8.5rem;
  gap: var(--s2) var(--s4);
  align-items: center;
  padding: var(--s4) 0 var(--s3);
  border-bottom: 1px solid color-mix(in oklch, var(--facet-edge) 30%, transparent);
}

.split__row:first-child { padding-top: 0; }

.split__rate {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-family: var(--font-num);
  color: var(--on-obsidian-70);
}

.split__rate .app-input { margin: 0; width: 4.75rem; padding: var(--s2) var(--s3); }
.split__rate--money .app-input { width: 6.5rem; }

.split__derived {
  padding-left: var(--s3);
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  color: var(--on-obsidian);
}

.split__amount {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-size: 1.125rem;
  color: var(--on-obsidian);
  text-align: right;
  text-shadow: 0 0 28px color-mix(in oklch, var(--light) 30%, transparent);
}

.split__meter {
  grid-column: 1 / -1;
  height: 3px;
  border-radius: 999px;
  background: color-mix(in oklch, var(--facet-edge) 22%, transparent);
  overflow: hidden;
}

.split__meter i {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--light);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--d-enter) var(--e-out);
}

.split.is-in .split__meter i { transform: scaleX(var(--w, 0)); }

.split__foot {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s3) var(--s4);
  margin-top: var(--s5);
}

.split__foot .app-label { margin: 0; }
.split__foot .app-btn { margin-left: auto; }
.split__error { margin: var(--s3) 0 0; }

@media (max-width: 767px) {
  .split__row { grid-template-columns: minmax(0, 1fr) auto auto; }
  .split__foot .app-btn { margin-left: 0; width: 100%; }
}

/* ============================================================
   Home: recent activity

   Rows carry the same facts the Activity feed shows for them: date,
   category and channel under the name.
   ============================================================ */
.rec { list-style: none; margin: 0; padding: 0; }

.rec__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto 7.5rem;
  gap: var(--s4);
  align-items: center;
  padding: var(--s3) 0;
  border-bottom: 1px solid color-mix(in oklch, var(--facet-edge) 30%, transparent);
}

.rec__row:first-child { padding-top: 0; }
.rec__row:last-child { border-bottom: 0; padding-bottom: 0; }

.rec__merchant {
  display: block;
  color: var(--on-obsidian);
  font-variation-settings: "opsz" 14, "wght" 500;
}

.rec__meta {
  display: block;
  font-family: var(--font-num);
  font-size: var(--t-app-meta);
  color: var(--on-obsidian-70);
}

.rec__amount {
  grid-column: 3;
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  color: var(--on-obsidian);
  text-align: right;
}

.rec__amount--in { color: var(--light-save); }

/* Declined rows are dimmed, struck through, and say the word. Colour
   and a strikethrough carry no accessible semantics on their own, so
   the status text is what actually marks the decline. */
.rec__status {
  font-family: var(--font-num);
  font-size: var(--t-app-meta);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--light-tax);
}

.rec__row.is-declined .rec__merchant,
.rec__row.is-declined .rec__amount { color: var(--on-obsidian-70); text-decoration: line-through; }

@media (max-width: 767px) {
  .rec__row { grid-template-columns: minmax(0, 1fr) auto; row-gap: var(--s1); }
  .rec__amount { grid-column: 2; grid-row: 1; }
  .rec__status { grid-column: 1; grid-row: 2; justify-self: start; }
}

/* flex, not a stretching grid: the triggers size to their content now,
   which is what stops them reading as text inputs. */
.feed__filters { display: flex; flex-wrap: wrap; gap: var(--s5); margin-bottom: var(--s5); }
.feed { list-style: none; margin: 0; padding: 0; }

.feed__row {
  border-bottom: 1px solid color-mix(in oklch, var(--facet-edge) 30%, transparent);
  color: var(--on-obsidian);
}

/* The fifth track is the disclosure column. It exists on every row,
   including the ones that do not expand, so the four data columns stay
   aligned down the whole feed. */
.feed__summary {
  display: grid;
  /* Fixed tracks for status and amount. Each row is its own grid, so
     auto tracks sized to each row's own status text and the date column
     started anywhere from 522px to 610px down the same feed. The status
     track has a fixed min and max, never a content size, so every row
     resolves it identically, and a long reason wraps inside it. */
  grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.3fr) minmax(8rem, 17rem) 7.5rem 12px;
  gap: var(--s4);
  align-items: baseline;
  padding-block: var(--s4);
  cursor: default;
}

/* Only deposits expand. Without a marker nothing told a reader which
   rows were interactive, and the default marker was being removed with
   nothing put back. This is the replacement: a chevron that turns. */
summary.feed__summary { cursor: pointer; list-style: none; }
summary.feed__summary::-webkit-details-marker { display: none; }
summary.feed__summary::marker { content: ""; }

summary.feed__summary:hover .feed__merchant,
summary.feed__summary:focus-visible .feed__merchant { color: var(--light); }

.feed__toggle { justify-self: end; align-self: center; width: 12px; height: 12px; }

summary .feed__toggle::before {
  content: "";
  display: block;
  width: 6px;
  height: 6px;
  margin: 1px 0 0 1px;
  border-right: 1.5px solid var(--on-obsidian-70);
  border-bottom: 1.5px solid var(--on-obsidian-70);
  transform: rotate(45deg);
  transition: transform var(--d-fast) var(--e-out);
}

details[open] summary .feed__toggle::before { transform: rotate(-135deg); }
summary:hover .feed__toggle::before { border-color: var(--light); }

.feed__merchant { font-variation-settings: "opsz" 14, "wght" 500; }
.feed__meta { color: var(--on-obsidian-70); font-family: var(--font-num); font-size: var(--t-app-meta); }

.feed__status {
  font-family: var(--font-num);
  font-size: var(--t-app-meta);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--on-obsidian-70);
  text-align: right;
}

.feed__status--in { color: var(--light-save); }
.feed__amount { font-family: var(--font-num); font-variant-numeric: tabular-nums; text-align: right; }
.feed__amount--in { color: var(--light-save); }

/* Declined rows are dimmed and carry their reason in words. Colour alone
   never marks a decline. */
.feed__row.is-declined .feed__merchant,
.feed__row.is-declined .feed__amount { color: var(--on-obsidian-70); text-decoration: line-through; }
.feed__row.is-declined .feed__status { color: var(--light-tax); text-decoration: none; }

.feed__split {
  display: flex;
  gap: var(--s5);
  padding: 0 0 var(--s4);
  font-family: var(--font-num);
  font-size: var(--t-app-meta);
}

.feed__split span { color: var(--light); }
.feed__empty { text-align: left; }

/* The feed sits in the same cut facet as every other section. Rows keep
   their hairlines; the last one hands its edge to the pager so the panel
   does not end on a doubled rule. Vertical padding drops a step because
   the rows carry their own. */
.feed-panel { padding-block: var(--s3) var(--s5); }
.feed-panel .feed__row:last-child { border-bottom: 0; }

/* ============================================================
   Pager

   Ten rows a page. Summary on the left in the numeric face, so the
   range reads as ledger metadata rather than prose; steps and page
   numbers on the right. Every target is at least 36px tall, 44 on a
   touch screen, and the current page is marked by fill and by
   aria-current, never by colour alone.
   ============================================================ */
.pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s4);
  flex-wrap: wrap;
  margin-top: var(--s4);
  padding-top: var(--s4);
  border-top: 1px solid color-mix(in oklch, var(--facet-edge) 30%, transparent);
}

.pager__summary {
  margin: 0;
  font-family: var(--font-num);
  font-size: var(--t-app-meta);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--on-obsidian-70);
}

.pager__num { color: var(--on-obsidian); font-variant-numeric: tabular-nums; }

.pager__controls { display: flex; align-items: center; gap: var(--s2); }
.pager__pages { display: flex; align-items: center; gap: var(--s1); }

.pager__step, .pager__page {
  font-family: var(--font-num);
  font-size: var(--t-app-meta);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
  min-height: 36px;
  border-radius: var(--r-s);
  border: 1px solid transparent;
  background: transparent;
  color: var(--on-obsidian-70);
  cursor: pointer;
  transition: color var(--d-fast) var(--e-out),
              border-color var(--d-fast) var(--e-out),
              background-color var(--d-fast) var(--e-out),
              transform var(--d-fast) var(--e-out);
}

.pager__step { padding: 0 var(--s4); border-color: color-mix(in oklch, var(--facet-edge) 60%, transparent); }
.pager__page { min-width: 36px; padding: 0 var(--s2); }

.pager__step:hover:not(:disabled), .pager__page:hover:not([aria-current="page"]) {
  color: var(--on-obsidian);
  border-color: var(--facet-edge);
}

.pager__step:active:not(:disabled), .pager__page:active { transform: translateY(1px); }

.pager__page[aria-current="page"] {
  background: var(--on-obsidian);
  color: var(--obsidian);
  border-color: var(--on-obsidian);
  cursor: default;
}

/* Dimmed, not hidden. A missing Previous on page one reads as a bug; a
   dimmed one reads as the edge. */
.pager__step:disabled { opacity: 0.35; cursor: default; }

.pager__gap {
  font-family: var(--font-num);
  color: var(--on-obsidian-70);
  min-width: 24px;
  text-align: center;
  letter-spacing: 0.1em;
}

@media (pointer: coarse) {
  .pager__step, .pager__page { min-height: 44px; }
  .pager__page { min-width: 44px; }
}

/* On a phone the numbers go and the steps carry the whole control. The
   summary still says where you are. */
@media (max-width: 767px) {
  .pager { flex-direction: column; align-items: stretch; }
  .pager__pages { display: none; }
  .pager__controls { justify-content: space-between; }
  .pager__step { flex: 1; }
}

@media (max-width: 767px) {
  .feed__summary { grid-template-columns: 1fr auto 12px; row-gap: var(--s2); }
  .feed__meta { grid-column: 1 / -1; }
  .feed__status { grid-column: 1; text-align: left; }
}

/* ============================================================
   Card: status first, controls below

   The card beside a ledger of what it will do today, then every control
   in one panel with the three groups as columns.
   ============================================================ */
.card-hero { margin-bottom: var(--s5); }

.card-hero__grid {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: var(--s7);
  align-items: center;
}

.card-hero__stage { display: grid; }
.card-hero__stage > .cardv { justify-items: center; }
/* Stays a row on phones. The shared panel head stacks under 768px, and
   stacked with center alignment the title and chip floated to the middle
   of a left-aligned ledger. */
.card-hero .panel-head { flex-direction: row; align-items: center; }
.card-hero__note { margin: var(--s4) 0 0; }

.summary { list-style: none; margin: 0; padding: 0; }

.summary__row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s4);
  padding: var(--s3) 0;
  border-bottom: 1px solid color-mix(in oklch, var(--facet-edge) 30%, transparent);
}

.summary__row:first-child { padding-top: 0; }

.summary__label { color: var(--on-obsidian); font-variation-settings: "opsz" 14, "wght" 500; }

.summary__value {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  color: var(--on-obsidian);
  text-align: right;
  overflow-wrap: anywhere;
}

/* Off and blocked say so in words; the colour only repeats it. */
.summary__value.is-off { color: var(--on-obsidian-70); }
.summary__value.is-blocked { color: var(--light-tax); }

.controls {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 5fr) minmax(0, 4fr);
  gap: var(--s6);
}

.controls .ctl__head { margin-top: 0; }

@media (max-width: 899px) {
  .card-hero__grid, .controls { grid-template-columns: minmax(0, 1fr); }
  .card-hero__grid { gap: var(--s6); }
}

.ctl__head {
  font-family: var(--font-num);
  font-size: var(--t-app-meta);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--on-obsidian-70);
  margin: var(--s6) 0 var(--s3);
}

.ctl__group { display: flex; flex-wrap: wrap; gap: var(--s3); }

.ctl {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s2) var(--s4);
  border: 1px solid color-mix(in oklch, var(--facet-edge) 60%, transparent);
  border-radius: 999px;
  color: var(--on-obsidian);
  cursor: pointer;
}

.ctl:focus-within { border-color: var(--light); }

/* Checked means opposite things per group: blocked for categories and
   regions, enabled for channels. The accent must follow the meaning, not
   just the checked state, or coral (used for declines everywhere else)
   would wrongly paint an enabled channel the same as a blocked category. */
.ctl--blocked input { accent-color: var(--light-tax); }
.ctl--enabled input { accent-color: var(--light-save); }

.ctl--blocked:has(input:checked) { border-color: var(--light-tax); color: var(--light-tax); }
.ctl--enabled:has(input:checked) { border-color: var(--light-save); color: var(--light-save); }

/* ============================================================
   Insights

   Trend and ledger side by side, the 7 / 5 split Home uses. The first
   build stacked two full-width panels of 36px solid bars and a bare
   line, which made the brightest paint in the whole app a bar chart.
   The lights here are glows and hairlines like everywhere else: a 2px
   line over a fading area, one lit endpoint, 3px meters.
   ============================================================ */
.page-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s3) var(--s5);
  flex-wrap: wrap;
  margin-bottom: var(--s6);
}

.page-head .app-h1 { margin: 0; }

.page-meta {
  margin: 0;
  font-family: var(--font-num);
  font-size: var(--t-app-meta);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--on-obsidian-70);
}

.ins-grid {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: var(--s5);
  margin-bottom: var(--s6);
}

.panel-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--s4);
  margin-bottom: var(--s5);
}

.panel-head .app-h2 { margin: 0; }

/* One figure, in the panel header, not a hero number. Bloom behind it is
   refraction mechanism 4, same as the balance tiles. */
.panel-total {
  margin: 0;
  text-align: right;
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-size: 1.25rem;
  line-height: 1.2;
  color: var(--on-obsidian);
  text-shadow: 0 0 28px color-mix(in oklch, var(--light) 35%, transparent);
}

.panel-total small {
  display: block;
  margin-top: var(--s1);
  font-size: var(--t-app-meta);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--on-obsidian-70);
  text-shadow: none;
}

/* Under 900px, not 768: at five columns the ledger's name, share and
   amount stop fitting on one line well before the phone breakpoint. */
@media (max-width: 899px) {
  .ins-grid { grid-template-columns: minmax(0, 1fr); }
}

/* On a phone the title and the total cannot share a line: the title
   wrapped to two and the total's caption to three. Stack them, total
   under the title, both left-aligned. */
@media (max-width: 767px) {
  .panel-head { flex-direction: column; gap: var(--s2); }
  .panel-total { text-align: left; }
}

/* --- The line chart --------------------------------------- */
.chart-wrap { position: relative; }
.chart { display: block; width: 100%; height: auto; overflow: visible; }

.chart__grid { stroke: var(--facet-edge); stroke-opacity: 0.2; stroke-width: 1; }
.chart__baseline { stroke: var(--facet-edge); stroke-opacity: 0.55; stroke-width: 1; }
.chart__stop--top { stop-color: var(--light); stop-opacity: 0.3; }
.chart__stop--bottom { stop-color: var(--light); stop-opacity: 0; }
.chart__line { stroke: var(--light); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.chart__dot { fill: var(--light); stroke: var(--obsidian-0); stroke-width: 2; }
.chart__halo { fill: var(--light); fill-opacity: 0.14; }
.chart__cross { stroke: var(--on-obsidian-70); stroke-opacity: 0; stroke-width: 1; stroke-dasharray: 3 4; }
.chart__hit { cursor: default; outline: none; }

.chart-wrap.is-hovering .chart__cross { stroke-opacity: 0.45; }
.chart__point.is-active .chart__dot { stroke: var(--on-obsidian); }

/* Focus lands on a transparent column, which has nothing to draw a ring
   around, so keyboard focus is shown on the point it names instead. */
.chart-wrap:has(.chart__hit:focus-visible) .chart__point.is-active .chart__dot {
  stroke: var(--light);
  stroke-width: 3;
}

.chart__tick, .chart__label {
  fill: var(--on-obsidian-70);
  font-family: var(--font-num);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

.chart__value {
  fill: var(--on-obsidian);
  font-family: var(--font-num);
  font-size: 14px;
  font-variant-numeric: tabular-nums;
}

.chart__tip {
  position: absolute;
  top: 0;
  left: 0;
  display: grid;
  gap: 2px;
  padding: var(--s2) var(--s3);
  border-radius: var(--r-s);
  background: var(--obsidian-2);
  border: 1px solid color-mix(in oklch, var(--facet-edge) 60%, transparent);
  border-top-color: color-mix(in oklch, var(--light) 55%, transparent);
  pointer-events: none;
  white-space: nowrap;
  z-index: 2;
}

/* display: grid above beats the UA's [hidden] rule, and app.css has no
   global reset for it, so without this the empty tooltip shows as a pill
   in the chart's top-left corner until the first hover. */
.chart__tip[hidden] { display: none; }

.chart__tip-label {
  font-family: var(--font-num);
  font-size: var(--t-app-meta);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--on-obsidian-70);
}

.chart__tip-value {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  color: var(--on-obsidian);
}

/* Chart text is sized in viewBox units, so it shrinks with the svg. At a
   phone width the 640-unit chart renders near half size and 12px labels
   would land around 6px. */
@media (max-width: 767px) {
  .chart__tick, .chart__label { font-size: 22px; }
  .chart__value { font-size: 24px; }
}

/* --- The category ledger ---------------------------------- */
.cat { list-style: none; margin: 0; padding: 0; }

.cat__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto 6.5rem;
  gap: var(--s2) var(--s4);
  align-items: baseline;
  padding: var(--s4) 0 var(--s3);
  border-bottom: 1px solid color-mix(in oklch, var(--facet-edge) 30%, transparent);
}

.cat__row:first-child { padding-top: 0; }
.cat__row:last-child { border-bottom: 0; padding-bottom: 0; }

.cat__name {
  color: var(--on-obsidian);
  font-variation-settings: "opsz" 14, "wght" 500;
  overflow-wrap: anywhere;
}

.cat__share {
  font-family: var(--font-num);
  font-size: var(--t-app-meta);
  font-variant-numeric: tabular-nums;
  color: var(--on-obsidian-70);
}

.cat__value {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  color: var(--on-obsidian);
  text-align: right;
}

.cat__meter {
  grid-column: 1 / -1;
  height: 3px;
  border-radius: 999px;
  background: color-mix(in oklch, var(--facet-edge) 22%, transparent);
  overflow: hidden;
}

.cat__meter i {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--light);
}

/* ============================================================
   Route transition

   Only a genuine route change fades. Duration matches --d-route and
   the setTimeout in router.js; changing one means changing all three.
   ============================================================ */

.app__main {
  transition: opacity var(--d-route) var(--e-out);
}

.app__main.is-leaving { opacity: 0; }

/* ============================================================
   Arrival motion

   Applied on first entry to a route only. A ledger re-render reuses the
   same classes already in their settled state, so nothing replays when
   a number changes.
   ============================================================ */

.enter {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity var(--d-base) var(--e-out),
              transform var(--d-base) var(--e-out);
}

.enter.is-in { opacity: 1; transform: none; }

/* Meters grow from the left edge. transform, so no geometry is
   recalculated per frame; --w is the row's length as a 0..1 fraction. */
.cat__meter i {
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--d-enter) var(--e-out);
}

.cat.is-in .cat__meter i { transform: scaleX(var(--w, 1)); }

.chart__line {
  stroke-dasharray: var(--len, 1000);
  stroke-dashoffset: var(--len, 1000);
  transition: stroke-dashoffset var(--d-enter) var(--e-out);
}

.chart.is-in .chart__line { stroke-dashoffset: 0; }

/* Ticks and grid stay put: they are the frame the line draws into. The
   area, points and labels follow the line in. */
.chart__area, .chart__dot, .chart__halo, .chart__value, .chart__label {
  opacity: 0;
  transition: opacity var(--d-base) var(--e-out) 260ms;
}

.chart.is-in .chart__area,
.chart.is-in .chart__dot,
.chart.is-in .chart__halo,
.chart.is-in .chart__value,
.chart.is-in .chart__label { opacity: 1; }

/* Hover feedback is state, so it is quick and never delayed. */
.chart__cross { transition: stroke-opacity var(--d-fast) var(--e-out); }

/* The feed crossfades when a filter changes the rows underneath it. */
.feed { transition: opacity var(--d-fast) var(--e-out); }

/* ============================================================
   Reduced motion

   app.css had no blanket rule, only the one cardv line. Everything
   collapses to its end state.
   ============================================================ */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  .enter { opacity: 1; transform: none; }
  .cat__meter i { transform: scaleX(var(--w, 1)); }
  .chart__line { stroke-dashoffset: 0; }
  .chart__area, .chart__dot, .chart__halo, .chart__value, .chart__label { opacity: 1; }
}

/* ============================================================
   Listbox

   Replaces two native selects whose OS arrow sat hard against the
   border and which stretched to the full grid column, so they read as
   text fields rather than filters.
   ============================================================ */

.lb { position: relative; display: inline-grid; gap: var(--s2); }

.lb__trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--s5);
  min-width: 200px;
  padding: var(--s3) var(--s4);
  border: 1px solid color-mix(in oklch, var(--facet-edge) 70%, transparent);
  border-radius: var(--r-s);
  background: var(--obsidian-2);
  color: var(--on-obsidian);
  font-family: var(--font-text);
  font-size: var(--t-app-body);
  text-align: left;
  cursor: pointer;
}

.lb__value { flex: 1; }

.lb__trigger:hover { border-color: color-mix(in oklch, var(--facet-edge) 100%, transparent); }

.lb__chev {
  width: 8px;
  height: 8px;
  flex: none;
  border-right: 1.5px solid var(--on-obsidian-70);
  border-bottom: 1.5px solid var(--on-obsidian-70);
  transform: translateY(-2px) rotate(45deg);
  transition: transform var(--d-fast) var(--e-out);
}

.lb.is-open .lb__chev { transform: translateY(1px) rotate(-135deg); }

.lb__list {
  position: absolute;
  top: calc(100% + var(--s2));
  left: 0;
  z-index: 20;
  min-width: 100%;
  margin: 0;
  padding: var(--s2);
  list-style: none;
  border: 1px solid color-mix(in oklch, var(--facet-edge) 70%, transparent);
  border-radius: var(--r-s);
  background: var(--obsidian-2);
  max-height: 320px;
  overflow-y: auto;
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity var(--d-fast) var(--e-out),
              transform var(--d-fast) var(--e-out);
}

.lb.is-open .lb__list { opacity: 1; transform: none; }

/* 44px so the control is usable on touch. */
.lb__opt {
  display: flex;
  align-items: center;
  min-height: 44px;
  padding: 0 var(--s4);
  border-radius: var(--r-s);
  color: var(--on-obsidian-70);
  cursor: pointer;
}

.lb__opt.is-active {
  background: color-mix(in oklch, var(--light) 14%, transparent);
  color: var(--on-obsidian);
}

.lb__opt[aria-selected="true"] { color: var(--light); }

.lb__opt[aria-selected="true"]::after {
  content: "";
  width: 5px;
  height: 9px;
  margin-left: auto;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translate(-1px, -1px);
}

@media (prefers-reduced-motion: reduce) {
  .lb__list, .lb__chev { transition: none; }
}
