/* SPACING: reach for a token — `--card-pad`, `--card-gap`, `--sp-1`…`--sp-6` —
   not a raw rem, and not the value in the rule above. Cards on the padding
   contract are enforced by `test/layout-tokens.test.js`; the contract and the
   list of what is on it are in `docs/visual-language.md` → Tokens. Raw values
   still in this file are legacy from a 2px grid, not a precedent to follow. */
/* Base: the app shell and sidebar, controls, tables, inline edit rows. */
/* Moved here from 01-tokens.css, which is fonts, colour tokens and focus only. */
/* ── Phase-2 shell: left sidebar navigation + top bar ── */
.app{display:flex;min-height:100vh;}
/* 140px fits the widest nav row (icon + "Overview" + padding needs 131px) and
   the "Ledger" wordmark; the old 168px was sized for the longer former name. */
/* ── sidebar: wider, grouped, crimson active state ── */
.sidebar{
  flex:0 0 auto;
  border-inline-end:1px solid var(--border);
  display:flex;
  flex-direction:column;
  position:sticky;
  top:0;
  height:100vh;
  width:var(--sidebar-w);
  background:var(--sidebar-bg);
  padding:1.375rem 0 1.125rem;
}
.side-logo{display:flex;align-items:center;gap:0.625rem;padding:2px var(--sp-2) 1.25rem;}
.logo-mark{width:2.5rem;height:2.5rem;display:flex;align-items:center;justify-content:center;flex:0 0 auto;}
.logo-mark svg{width:2.5rem;height:2.5rem;display:block;}
.side-logo span{font-size:var(--fs-body);font-weight:600;white-space:nowrap;}
.side-nav{display:flex;flex-direction:column;position:relative;padding:0 var(--sp-3);gap:1px;}
.nav-cat{
  font-size:var(--fs-micro);
  font-weight:500;
  letter-spacing:.09em;
  text-transform:uppercase;
  color:var(--muted);
  padding:0.8125rem var(--sp-3) 0.3125rem;
}
.nav-cat:first-of-type{padding-top:2px;}
.tab svg{flex:0 0 auto;width:1.1875rem;height:1.1875rem;opacity:.9;}
/* Markup for one host only (see js/00-platform.js): hidden in the other. */
:root[data-platform="web"]:not([data-show-all="on"]) [data-only="desktop"],:root[data-platform="desktop"]:not([data-show-all="on"]) [data-only="web"]{
  display:none !important;
}
/* Markup that needs the converter (see js/00-platform.js), and markup that replaces it. */
:root[data-converter="off"]:not([data-show-all="on"]) [data-needs="converter"],:root[data-converter="on"]:not([data-show-all="on"]) [data-needs="no-converter"]{
  display:none !important;
}
/* The dev app (app/scripts/run-dev.sh) and the GUI lab (serve.py) set data-show-all="on"
   so every card and section is on screen at once, whichever host and converter state it
   is really in. Wording that has two versions then shows both, so keep the pair apart. */
:root[data-show-all="on"] span[data-only]+span[data-only]::before,:root[data-show-all="on"] span[data-needs]+span[data-needs]::before{content:" / ";}
/* Raised, button-like look for the active-section pill — soft drop shadow only. */
.tab-indicator{
  position:absolute;
  z-index:0;
  top:0;
  left:0;
  width:0;
  height:0;
  border-radius:var(--radius);
  background:var(--accent-deep);
  opacity:0;
  box-shadow:0 3px 8px rgba(0,0,0,.45),0 7px 16px -6px rgba(0,0,0,.55);
  transition:left var(--hover-dur) var(--hover-ease),top var(--hover-dur) var(--hover-ease),width var(--hover-dur) var(--hover-ease),height var(--hover-dur) var(--hover-ease);
}
.side-spacer{flex:1;}
/* Horizontal padding lives on .topbar-inner and main (one gutter, one edge);
   doubling it here is what pushed the content into a 1200px island. */
.content{flex:1;min-width:0;display:flex;flex-direction:column;padding:0 0 3.75rem;}
/* The bar itself spans the full width (its background and hairline should run
   edge to edge); .topbar-inner carries the page width so the title sits on the
   same left edge as the content below it. */
/* ── topbar: big editorial title ── */
.topbar{
  position:sticky;
  top:0;
  z-index:100;
  padding:1.125rem 0;
  background:var(--bg);
  border-bottom:1px solid var(--border);
}
.topbar-inner{
  max-width:var(--page-max);
  width:100%;
  margin:0 auto;
  padding:0 var(--page-gutter);
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:var(--sp-3);
}
.topbar h1{font-size:var(--fs-figure);letter-spacing:-.01em;}
/* clip, not hidden: overflow-x:hidden forces overflow-y to auto, which makes
   this a scroll container and stops the sticky table headers inside from ever
   sticking. clip contains the same overflow without that side effect. */
main{
  flex:1;
  padding:1.75rem var(--page-gutter) 0;
  max-width:var(--page-max);
  width:100%;
  margin:0 auto;
  overflow-x:clip;
}
/* PDF button lives at the bottom of every section (order:99 pushes it last in the flex column). */
.panel{display:none;}
.panel.active{display:flex;flex-direction:column;}
/* Sections fade in on switch — no translation, so a long panel doesn't shift
   under the pointer while it arrives. */
.panel.active.slide-in{animation:panelIn .45s ease-out;}
@keyframes panelIn{
  from{opacity:0;}
  to{opacity:1;}
}
/* No up/down spinner on number inputs — type or paste only. */
input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{
  -webkit-appearance:none;
  margin:0;
}
input[type=number]{-moz-appearance:textfield;}
/* align-self:flex-end so the rule spans the control row, not the label row above it. */
.sort-ind{font-size:var(--fs-micro);color:var(--accent);margin-inline-start:2px;}
/* Primary buttons on the page/modal ground: legibility ring only. Fill is .btn.primary
   in 02-components.css. */
/* A ring only on the two that sit on the page or a modal's ground: a bright gold is
   close enough to the light theme's white to lose its edge there (1.88:1), and the
   ring puts it back at 3.51:1. Form buttons sit on cards and do not need it. */
.modal-actions .btn.primary{
  text-align:center;
  box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent-bright) 72%,#000);
  min-width:9.875rem;
}
.topbar .btn.primary{min-width:8rem;text-align:center;}
.recur-check{
  display:flex;
  align-items:center;
  gap:0.5625rem;
  font-size:var(--fs-body);
  color:var(--text);
  cursor:pointer;
  user-select:none;
}
.recur-check input{width:1.0625rem;height:1.0625rem;accent-color:var(--pos);cursor:pointer;}
.recur-check .hint{color:var(--muted);font-size:var(--fs-label);}
/* A ledger row's category bar, on the date cell at the row's start (--c: the category's colour). */
td.row-mark{box-shadow:inset 3px 0 0 var(--c);}
:root[dir="rtl"] td.row-mark{box-shadow:inset -3px 0 0 var(--c);}
/* Variable recurring "needs filling" highlight (consistent red) */
tr.needs-amt-row td{background:var(--tint-red);}
tr.needs-amt-row td:first-child{box-shadow:inset 3px 0 0 var(--neg);}
/* A shadow has no inline-start: the bar is moved to the right edge by hand. */
:root[dir="rtl"] tr.needs-amt-row td:first-child{box-shadow:inset -3px 0 0 var(--neg);}
.rec-fill-note{color:var(--neg);font-weight:600;font-size:var(--fs-label);}
/* Row tags in the Expenses/Income tables get their own line under the
   description, as compact pills, so they never crowd or trail the text. */
.desc-tags{display:flex;flex-wrap:wrap;gap:var(--sp-1);margin-top:0.1875rem;}
.desc-tags select{min-width:8rem;max-width:12rem;}
.desc-tags .pill{margin-inline-start:0;vertical-align:middle;}
.desc-tags .items-pop{vertical-align:middle;}
/* Custom hover tooltip — native title="" is unreliable on small inline pills
   (inconsistent delay/suppression across browsers), so tag elements needing a
   hover explanation use data-tip="" + this CSS tooltip instead. */
[data-tip]{position:relative;}
[data-tip]:hover::after{
  content:attr(data-tip);
  position:absolute;
  top:calc(100% + 6px);
  left:50%;
  transform:translateX(-50%);
  background:var(--surface3);
  color:var(--text);
  border:1px solid var(--border);
  padding:var(--sp-1) 0.5625rem;
  border-radius:var(--radius-sm);
  font-size:var(--fs-micro);
  font-weight:600;
  text-transform:none;
  letter-spacing:normal;
  white-space:nowrap;
  z-index:60;
  box-shadow:0 6px 16px -4px rgba(0,0,0,.5);
  pointer-events:none;
}
/* Table containers clip overflow (rounded corners), so a tooltip dropping below
   the last row gets cut off — flip it upward there instead. */
tbody tr:last-child [data-tip]:hover::after{top:auto;bottom:calc(100% + 6px);}
/* Theme picker (Admin > Appearance). */
.theme-picker{display:flex;gap:var(--sp-2);flex-wrap:wrap;}
.theme-opt{
  background:var(--input-bg);
  border:1px solid var(--border);
  color:var(--text);
  border-radius:var(--radius-sm);
  padding:0.5625rem 1.125rem;
  font-size:var(--fs-body);
  font-weight:600;
  cursor:pointer;
  transition:border-color .15s,background .15s;
}
.theme-opt:hover{border-color:var(--border-hover);}
.theme-opt.sel{border-color:var(--accent);background:var(--tint-amber);color:var(--accent);}
/* Expenses search box lives in the month/year controls row. */
/* Sits inside a .ctrl-group so its input lines up with the month/year selects
   instead of floating between the label row and the control row. */
.exp-search{position:relative;display:flex;align-items:center;}
.exp-search input{
  background:var(--input-bg);
  border:1px solid var(--border);
  color:var(--text);
  border-radius:var(--radius-sm);
  padding-block:var(--sp-2);padding-inline:var(--sp-3) 1.875rem;
  font-size:var(--fs-body);
  outline:none;
  width:12.5rem;
  transition:border-color .15s;
}
.exp-search input:focus{border-color:var(--accent);}
.exp-search .exp-search-clear{
  position:absolute;
  inset-inline-end:8px;
  background:none;
  border:none;
  color:var(--muted);
  cursor:pointer;
  font-size:var(--fs-body);
  line-height:1;
  padding:0;
  display:none;
}
.exp-search .exp-search-clear.show{display:block;}
/* Starting-balance admin card: locked display after it's set, editable inputs before/while editing. */
.start-bal-display{display:flex;align-items:center;gap:0.625rem;flex-wrap:wrap;font-size:var(--fs-body);}
.start-bal-display .sb-val{font-weight:600;font-size:var(--fs-figure);}
/* overflow:clip, not hidden — hidden makes this a scroll container, which stops
   position:sticky headers inside from ever sticking to the viewport. clip still
   keeps content inside the rounded corners. */
.table-wrap{
  background:var(--surface);
  border:1px solid var(--border);
  border-radius:var(--radius-lg);
  overflow:clip;
  margin-bottom:var(--sp-5);
}
table{width:100%;border-collapse:collapse;}
/* Sticky under the topbar so column meanings stay visible down a long month. */
thead th{
  background:var(--surface3);
  padding:0.625rem var(--sp-4);
  text-align:start;
  font-size:var(--fs-label);
  font-weight:500;
  color:var(--muted);
  text-transform:none;
  letter-spacing:0;
  position:sticky;
  top:var(--topbar-h);
  z-index:5;
}
tbody tr{border-top:1px solid var(--border);}
.select-day{min-width:4rem;}
.rule-start-year{width:4.625rem;}
tbody td{padding:0.6875rem var(--sp-4);font-size:var(--fs-body);}
.amt{font-weight:600;color:var(--neg);white-space:nowrap;}
.amt-green{font-weight:600;color:var(--pos);white-space:nowrap;}
.amt .money-sym,.amt-green .money-sym{color:inherit;}
.row-actions{display:flex;gap:0.375rem;white-space:nowrap;}
/* Hint that right-clicking a row opens the edit/delete menu — invisible until
   the row is hovered, so it doesn't add permanent visual noise to every table. */
.ctx-hint{
  opacity:0;
  transition:opacity .12s;
  background:transparent;
  border:1px solid transparent;
  color:var(--muted);
  border-radius:var(--radius-sm);
  padding:2px 0.4375rem;
  font-size:var(--fs-body);
  line-height:1;
  cursor:pointer;
}
tr:hover .ctx-hint,.cat-item:hover .ctx-hint{opacity:1;}
.ctx-hint:hover{border-color:var(--border);color:var(--text);background:var(--surface3);}
.cat-item .ctx-hint{margin-inline-start:auto;flex-shrink:0;}
tbody td input:focus,tbody td select:focus,.exp-tile.editing input:focus,.exp-tile.editing select:focus{border-color:var(--accent);}
tr.editing-row{background:var(--surface2);}
/* Inline edit rows: uniform, top-aligned fields so every box lines up. */
tr.editing-row td{vertical-align:top;}
tr.editing-row td input,tr.editing-row td select,.exp-tile.editing input,.exp-tile.editing select{
  padding:var(--sp-2) 0.625rem;
  font-size:var(--fs-label);
  min-height:2.1875rem;
  border-radius:var(--radius-sm);
  box-sizing:border-box;
  text-align:start;
}
/* Safari/WebKit ignores min-height & padding on native <select> (appearance:menulist),
   so force a custom appearance and re-add a chevron — Day/Category/Source then match the inputs. */
tr.editing-row td select,.exp-tile.editing select{
  -webkit-appearance:none;
  -moz-appearance:none;
  appearance:none;
  padding-inline-end:1.625rem;
  background-image:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='10'%20height='10'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%238b8ba7'%20stroke-width='3'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpolyline%20points='6%209%2012%2015%2018%209'/%3E%3C/svg%3E");
  background-repeat:no-repeat;
  background-position:right 9px center;
}
:root[dir="rtl"] tr.editing-row td select,:root[dir="rtl"] .exp-tile.editing select{background-position:left 9px center;}
tbody td input[type=checkbox]{width:1.0625rem;height:1.0625rem;min-height:0;padding:0;flex:0 0 auto;}
/* Itemized edit-row amount is a read-only total (not an input) — box it like
   the sibling fields, with the figure at the start. */
tr.editing-row .ee-amt-view,.exp-tile.editing .ee-amt-view{
  background:var(--input-bg);
  border:1px solid var(--border);
  color:var(--text);
  border-radius:var(--radius-sm);
  padding:var(--sp-2) 0.625rem;
  min-height:2.1875rem;
  box-sizing:border-box;
  display:flex;
  align-items:center;
  justify-content:flex-start;
  font-size:var(--fs-body);
  font-weight:600;
  font-variant-numeric:tabular-nums;
}
/* Save / cancel match the field height and are square. */
tr.editing-row td.row-actions .btn,
.entry-edit-actions .btn{
  box-sizing:border-box;
  width:2.1875rem;
  height:2.1875rem;
  min-width:2.1875rem;
  min-height:2.1875rem;
  padding:0;
}
.rxe-fixed-lbl{
  display:flex;
  align-items:center;
  gap:0.3125rem;
  white-space:nowrap;
  font-size:var(--fs-micro);
  color:var(--muted);
  margin-top:0.4375rem;
  cursor:pointer;
  user-select:none;
}
.rxe-fixed-lbl input[type=checkbox]{width:0.6875rem;height:0.6875rem;}
.empty-state{
  text-align:center;
  padding:var(--sp-6) var(--card-pad);
  color:var(--muted);
  font-size:var(--fs-body);
  background:var(--surface);
  border:1px solid var(--border);
  border-radius:var(--radius);
  box-shadow:var(--elev);
  max-width:var(--card-wide);
  margin:var(--sp-4) auto;
}
/* The same message inside a table is already inside a card — the row spans the
   body, so it takes the copy and drops the card chrome around it. */
td.empty-state{
  background:none;
  border:none;
  border-bottom:1px solid var(--border);
  border-radius:0;
  box-shadow:none;
  max-width:none;
  margin:0;
  padding:var(--sp-5) var(--sp-4);
}
/* Top Categories bars (analytics) */
.catbar-row{margin-bottom:0.8125rem;}
.catbar-row:last-child{margin-bottom:2px;}
.catbar-head{
  display:flex;
  justify-content:space-between;
  align-items:baseline;
  gap:var(--sp-3);
  font-size:var(--fs-body);
  margin-bottom:var(--sp-1);
}
.catbar-head .cat-mark{flex:1;min-width:0;}
.catbar-meta{font-size:var(--fs-meta);color:var(--muted);font-variant-numeric:tabular-nums;white-space:nowrap;}
/* display:contents means the row itself paints nothing — highlight its cells. */
/* ── Category Trends: small multiples ──
   Each cell owns its vertical scale, so the grid reads as a set of shapes. The
   stroke keeps its width under the non-uniform scale via non-scaling-stroke. */
.spark-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:0.875rem;}
.spark-cell{
  background:var(--surface);
  border-radius:var(--radius);
  padding:var(--sp-3) 0.875rem;
  box-shadow:inset 0 0 0 1px var(--border);
  transition:box-shadow .15s ease;
}
.spark-cell:hover{
  box-shadow:inset 0 0 0 1px var(--border-hover),var(--elev-hover);
}
.spark-head{
  display:flex;
  align-items:baseline;
  justify-content:space-between;
  gap:var(--sp-2);
  margin-bottom:0.4375rem;
}
.spark-nm{font-size:var(--fs-body);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.spark-delta{font-size:var(--fs-micro);font-weight:600;white-space:nowrap;flex:0 0 auto;}
.spark-delta.up{color:var(--neg);}
.spark-delta.down{color:var(--pos);}
.spark-delta.flat{color:var(--muted);}
.spark-foot{
  display:flex;
  align-items:baseline;
  justify-content:space-between;
  gap:var(--sp-2);
  margin-top:0.4375rem;
  font-size:var(--fs-micro);
  color:var(--muted);
}
.spark-last{font-size:var(--fs-body);font-weight:600;color:var(--text);font-variant-numeric:tabular-nums;}
@media(max-width:560px){
  .spark-grid{grid-template-columns:1fr;}
}
/* Trends card */
/* Yearly / Budget headline figures: `.kpi-strip` + `.kpi` (06-overview.css). Trends
   and Forecast lay their figures out as separate cards, in their own section files. */
.yr-kpis,.sr-kpis,.bg-kpis{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(0,1fr);column-gap:1.25rem;}
.bg-kpis{margin-bottom:1.375rem;}
.sr-kpis{margin-bottom:1.125rem;}
.yr-kpis > .kpi,.sr-kpis > .kpi,.bg-kpis > .kpi{
  padding:var(--card-pad);
  min-height:4.75rem;
  display:flex;
  flex-direction:column;
  justify-content:center;
}
.tr-tiles .kpi .v,.yr-kpis .kpi .v,.sr-kpis .kpi .v,.bg-kpis .kpi .v{font-size:var(--fs-figure);}
/* The comparison chip a step smaller, the size it is in the Overview's tiles.
   Trends dropped out of this list: its cells state a delta as the figure itself. */
/* These strips stack on `--sp-4`, the gap the stacked hairline is centred in
   (06-overview.css, which owns `.kpi-strip`). The hairline's own narrow-tier
   rule used to live here and never applied: the base rule is in 06, which loads
   after this file, so it won at equal specificity and the line stayed vertical
   and 1px wide on top of stacked panes. */
@media (max-width:760px){
  .yr-kpis,.sr-kpis,.bg-kpis{grid-auto-flow:row;row-gap:var(--sp-4);}
}
.tr-row{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:var(--sp-3);
  padding:0.5625rem 0;
  border-bottom:1px solid var(--border);
}
.tr-row:last-child{border-bottom:none;}
.tr-cat{display:flex;align-items:center;gap:0.5625rem;font-size:var(--fs-body);}
.tr-right{display:flex;align-items:center;gap:0.875rem;}
.tr-prevnext{font-size:var(--fs-label);color:var(--muted);}
.tr-delta{font-size:var(--fs-body);min-width:7.75rem;text-align:end;font-variant-numeric:tabular-nums;}
.tr-up{color:var(--neg);}
.tr-down{color:var(--pos);}
.tr-avg-row{margin-bottom:var(--sp-4);}
.tr-avg-row:last-child{margin-bottom:0;}
.tr-avg-head{display:flex;justify-content:space-between;font-size:var(--fs-label);margin-bottom:var(--sp-1);}
/* Paired "now" vs "avg" bars — two bars on a shared scale so the comparison is a
   direct length-vs-length read (no overlaid tick to decode). Which bar is which
   comes from the legend in the section label, the colour (category hue vs the
   neutral baseline) and the fixed order; the rows used to repeat the words "now"
   and "avg" down the card, twelve labels to say what the legend says once. */
.tr-pair{
  display:grid;
  grid-template-columns:minmax(0,1fr) 5rem;
  align-items:center;
  gap:var(--sp-3);
  margin-top:0.3125rem;
}
.tr-pair-val,.exp-sum-src-row .pct{font-size:var(--fs-meta);color:var(--muted);font-variant-numeric:tabular-nums;text-align:end;}
/* This month's figure is the answer on the row; the average beside it is context. */
.tr-pair-val.now{color:var(--text);font-weight:500;}
/* Average bar: a neutral slate that reads as a "baseline reference" and is not in
   the category PALETTE, so it never collides with a category's own bar color. */
.tr-legend{
  display:flex;
  align-items:center;
  gap:0.375rem;
  margin-top:0.875rem;
  font-size:var(--fs-micro);
  color:var(--muted);
}
/* Sparkline end dot: HTML, not an SVG circle, so the stretched plot can't squash it. */
.spark-plot{position:relative;}
.spark-dot{
  position:absolute;
  width:0.375rem;
  height:0.375rem;
  border-radius:50%;
  border:1.5px solid;
  transform:translate(-50%,-50%);
  pointer-events:auto;
  box-sizing:border-box;
}
/* The dot is a few pixels wide. The hit area is what a pointer can actually land on. */
.spark-dot::before{content:'';position:absolute;inset:-0.45rem;}
.spark-dot:hover{z-index:2;}
/* Taller plot so month-to-month swings read as slopes, not a near-flat line; one
   small dot per month marks where each data point sits. */
.spark-svg{width:100%;display:block;overflow:visible;height:4rem;}
.spark-dot-month{width:0.25rem;height:0.25rem;border:none;opacity:.85;}
/* Sparklines: an estimated month gets a hollow dot. */
.spark-dot-month.est{border:1px solid;background:var(--surface2);}
/* Category identity: coloured dot + name (visual-language.md). */
.cat-mark{
  display:inline-flex;
  align-items:center;
  gap:var(--sp-2);
  min-width:0;
  font-size:var(--fs-body);
  font-weight:400;
  color:var(--text);
}
.cat-mark i{
  flex:0 0 auto;
  width:0.5rem;
  height:0.5rem;
  border-radius:50%;
  background:color-mix(in srgb,var(--c) 72%,var(--surface));
  box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--c) 40%,transparent);
}
.cat-mark > span,#panel-admin .cat-name,#panel-income .exp-name.desc-text,#panel-expenses .desc-text.tip-if-trunc,.exp-name.desc-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
/* Names the user typed, cut with an ellipsis. On a right-to-left page an English name
   takes its own direction, so it is cut at its end ("Fo…", not "…ing"), and stays
   against the right edge beside its dot. */
:root[dir="rtl"] :is(.cat-mark > span,#panel-admin .cat-name,.desc-text,.spark-nm,.subs-nm,.ov-catsplit .cs-nm,.ch-donut-legend .nm,.exp-sum-src-row .nm,.sum-line b,.pill.source){unicode-bidi:plaintext;text-align:right;}
.money-sym{font-size:var(--fs-meta);color:var(--muted);font-weight:500;margin-inline-end:0.125em;}
td.amt,td.amt-green,.stmt-amt{font-variant-numeric:tabular-nums;}
