/* ============================================================
   v9 mockup v2 — tabbed-app structure, v8 content, v9 visual
   Palette E (forest/stone/brass) primary; Palette A toggle.
   ============================================================ */

/* -- palette E — forest / sage paper / brass (default) ------- */
:root {
  --canvas:        #E8EDDC;  /* sage-tinted cream */
  --surface:       #F4F6EC;
  --ink:           #1C2A20;  /* deep forest */
  --ink-muted:     #4F5D54;
  /* Adjusted again 2026-08-17. The 2026-07-12 pass fixed this against --canvas
     and stopped there, but faint ink is used on the TINTED rows too — the
     delinquency and watch tiles — which are darker than canvas, and it measured
     3.96:1 on --row-trouble here. Every palette had the same hole; palette D had
     it worst, failing on five different grounds because in dark mode --surface is
     LIGHTER than --canvas, so the canvas check is the easy one, not the binding
     one. Each value below now clears 4.5:1 against canvas, every surface, AND
     both row tints, at the same hue (all three channels scaled together). The
     worst ground is quoted per line. Check the tints, not just the canvas. */
  --ink-faint:     #5B635C;  /* 4.52:1 on --row-trouble, its tightest ground */
  --rule:          #D5DAC8;
  --rule-strong:   #B5BAA8;
  --accent:        #7A5A2F;  /* brass */
  --accent-ink:    #7A5A2F;  /* --accent as TEXT */
  --positive:      #2F5F3F;
  --negative:      #8B3F2F;
  --watch:         #A87B36;
  --negative-ink:  #8B3F2F;  /* --negative as TEXT */
  --watch-ink:     #7E5C29;  /* --watch as TEXT — the fill is too light to read on */
  --positive-ink:  #2F5F3F;  /* --positive as TEXT */
  /* Text/icon colour for anything sitting ON an --accent background (the bug
     FAB, the impersonation bar). MUST be per-palette: the old hardcoded #fff
     was 2.48:1 on palette-dark's light-gold accent and 2.70:1 on navy's, both
     failing AA. There is no single value that works — navy needs dark ink,
     every other palette needs a light tint — so each block states its own.
     Ratio against that palette's --accent is noted per line. (2026-08-17) */
  --on-accent:     #F4F6EC;  /* 5.78:1 on #7A5A2F */
  /* Same idea for text on a filled --positive / --negative swatch. One token
     covers both because they land on the same value in every palette today;
     if either ever changes luminance enough to diverge, split this in two.
     In palette-dark the old #fff was 3.00:1 on --negative and 2.32:1 on
     --positive — both failed. Ratios per line are negative/positive. */
  --on-fill:       #F4F6EC;  /* 6.75 / 6.80 */
  --hover-tint:    rgba(28, 42, 32, 0.04);
  --row-trouble:   #FAD9CD;
  --row-watch:     #FEF2BD;
  /* font pairings — F1 default (Source Serif 4 + IBM Plex Sans) */
  --font-serif:    'Source Serif 4', 'Source Serif Pro', Georgia, serif;
  --font-sans:     'IBM Plex Sans', system-ui, -apple-system, sans-serif;
  --font-mono:     'IBM Plex Mono', 'Menlo', monospace;
  /* -- motion tokens (2026-07-12) — three durations, one easing curve,
     used everywhere instead of ad-hoc numbers so the whole app moves at
     one consistent pace. --ease-out is a snappy deceleration (no bounce/
     overshoot) — the same shape used across Claude's own product surfaces. */
  --dur-fast:      0.12s;  /* hover states — color/background/border */
  --dur-base:      0.18s;  /* panels, reveals, tab switches */
  --dur-slow:      0.28s;  /* page-load entrance */
  --ease-out:      cubic-bezier(0.16, 1, 0.3, 1);

  /* -- type scale (2026-08-17) ------------------------------------------
     Was 19 distinct font-size values, four of them a half-pixel apart
     (9.5 / 10.5 / 11.5 / 12.5) — differences nobody can perceive but which
     guarantee misaligned baselines between neighbouring components. These
     steps were chosen to sit ON the sizes already dominant in the file, so
     adopting them moved no text by more than 2px. Names describe the ROLE,
     because that is what a caller is choosing.

     11px is the FLOOR, and only for tracked capitals; anything in sentence
     case stops at 12px (--t-micro). The file used to run down to 8px, which
     is not a size you read a rent roll at. The two sub-11px steps that
     briefly existed for the migration (--t-label-sm 10px, --t-label-xs 9px)
     are gone: their 45 rules were sorted by whether the rule itself sets
     text-transform: uppercase — 26 tracked labels went to 11px, 19
     sentence-case ones to 12px. Do not reintroduce a smaller step. */
  --t-display:     36px;  /* .doc-header h1 — one per page */
  --t-metric:      24px;  /* the big number in a dashboard/region tile */
  --t-metric-sm:   22px;  /* KPI-strip value, side-panel + kanban h2 */
  --t-head:        18px;  /* section heads, card titles (absorbs 19/17/16) */
  --t-body:        14px;  /* prose, table cells */
  --t-ui:          13px;  /* controls, nav, buttons */
  --t-micro:       12px;  /* sub-values, captions (absorbs 12.5/11.5) */
  --t-label:       11px;  /* tracked uppercase labels — the dominant size */

  /* -- space scale (2026-08-17) -----------------------------------------
     Twelve steps: 2px increments at the small end where a pixel is visible
     in a dense table, 4px through the middle, 8px at the top.

     Deliberately NOT a minimal scale. The goal is no ARBITRARY values, not
     arbitrarily few — 10px and 20px are load-bearing here (10px is the table
     cell rhythm, 20px a common panel gap), and forcing 36 declarations off
     them to prove a point would move layout for nothing. What got snapped
     instead were the genuinely off-grid strays: 3 / 5 / 7 / 9 / 18 / 36px.

     1px is left alone where it appears: that is a hairline, not spacing. */
--sp-2:            2px;
  --sp-4:            4px;
  --sp-6:            6px;
  --sp-8:            8px;
  --sp-10:          10px;
  --sp-12:          12px;
  --sp-16:          16px;
  --sp-20:          20px;
  --sp-24:          24px;
  --sp-32:          32px;
  --sp-40:          40px;
  --sp-48:          48px;

  /* One corner radius. The file had 0 / 8 / 9px in CSS and 3 / 5 / 6 / 7 /
     8 / 999px in inline styles — six radii for the same rounded corner. */
  --radius:         6px;
}

/* Respect the OS-level motion preference — every transition/animation
   collapses to near-instant. This must win over every rule below. */
@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;
  }
}

/* Font pairings F2-F6 were here and are gone (2026-08-17): each one repointed
   --font-serif and --font-sans at families the app never loaded, so every one of
   them resolved to Georgia + system-ui. Five variables' worth of CSS that could
   not do anything. The single pairing the app does load is the :root default. */

/* -- palette A — navy / vellum / gold (warm yellow cream) ----- */
body.palette-navy {
  --canvas:        #F4EED5;  /* warm vellum yellow */
  --surface:       #FCF8E5;
  --ink:           #1A2548;  /* deep navy */
  --ink-muted:     #4F5870;
  --ink-faint:     #5B6079;  /* darkened again 2026-08-17 — 4.51:1 on --row-trouble. See palette E note */
  --rule:          #DDD3B5;
  --rule-strong:   #BFB597;
  --accent:        #C49555;
  /* --accent as TEXT. The fill value is a mid gold here and measured
     2.32:1 on --canvas, so the "Sync now" control was the least readable
     thing in the header. */
  --accent-ink:    #7F6137;  /* --accent as TEXT */
  --positive:      #2F5F3F;
  --negative:      #8B3F2F;
  --watch:         #C49555;
  --negative-ink:  #8B3F2F;  /* --negative as TEXT */
  --watch-ink:     #7F6137;  /* --watch as TEXT — the fill is too light to read on */
  --positive-ink:  #2F5F3F;  /* --positive as TEXT */
  --on-accent:     #1A2548;  /* 5.55:1 on #C49555 — dark ink, not a light tint: this
                                accent is a mid gold and every light value fails on it */
  --on-fill:       #FCF8E5;  /* 6.91 / 6.96 */
  --hover-tint:    rgba(26, 37, 72, 0.04);
  --row-trouble:   #FFDFCD;
  --row-watch:     #FFF1BD;
  /* Impact-marker ink (the ✦ marks and their labels). Needs its own token
     because --watch is a mid-tone amber that sits at 2.18:1 on --row-watch —
     invisible on the chip it labels. Dark gold here, light gold in
     palette-dark; ≥5.0:1 on both the chip tint and the canvas. */
  --impact-ink:    #854F0B;
}

/* -- palette W — walnut / parchment / brass (woody) ---------- */
body.palette-woody {
  --canvas:        #E8D8BC;
  --surface:       #F4E8CD;
  --ink:           #2D1F12;
  --ink-muted:     #5C4530;
  --ink-faint:     #675A49;  /* darkened again 2026-08-17 — 4.50:1 on --row-trouble. See palette E note */
  --rule:          #CFBA98;
  --rule-strong:   #B09E7C;
  --accent:        #7A5A2F;
  /* --accent as TEXT. The fill value is a mid gold here and measured
     2.32:1 on --canvas, so the "Sync now" control was the least readable
     thing in the header. */
  --accent-ink:    #76572D;  /* --accent as TEXT */
  --positive:      #4A6F4F;
  --negative:      #8B3F2F;
  --watch:         #A87B36;
  --negative-ink:  #8B3F2F;  /* --negative as TEXT */
  --watch-ink:     #765726;  /* --watch as TEXT — the fill is too light to read on */
  --positive-ink:  #436447;  /* --positive as TEXT */
  --on-accent:     #F4E8CD;  /* 5.18:1 on #7A5A2F */
  --on-fill:       #F4E8CD;  /* 6.06 / 4.70 */
  --hover-tint:    rgba(45, 31, 18, 0.05);
  --row-trouble:   #FFCCA7;
  --row-watch:     #FFE9AF;
  --impact-ink:    #854F0B;
}

/* -- palette C — charcoal / cool neutral (office paper) ------ */
body.palette-charcoal {
  --canvas:        #E8E9E3;  /* cool neutral gray */
  --surface:       #F5F5F0;
  --ink:           #2A2A28;
  --ink-muted:     #5F5F5C;
  --ink-faint:     #626259;  /* darkened again 2026-08-17 — 4.53:1 on --row-trouble. See palette E note */
  --rule:          #D5D5CD;
  --rule-strong:   #B8B8AE;
  --accent:        #8A5226;
  --accent-ink:    #8A5226;  /* --accent as TEXT */
  --positive:      #4A6F4F;
  --negative:      #9C4030;
  --watch:         #B8753C;
  --negative-ink:  #9C4030;  /* --negative as TEXT */
  --watch-ink:     #8D5A2E;  /* --watch as TEXT — the fill is too light to read on */
  --positive-ink:  #496E4E;  /* --positive as TEXT */
  --on-accent:     #F5F5F0;  /* 5.79:1 on #8A5226 */
  --on-fill:       #F5F5F0;  /* 6.02 / 5.22 */
  --hover-tint:    rgba(42, 42, 40, 0.04);
  --row-trouble:   #F8E0D3;
  --row-watch:     #FDF1C2;
  --impact-ink:    #854F0B;
}

/* -- palette D — dark mode (deep forest-charcoal canvas) ----- */
body.palette-dark {
  --canvas:        #1A1D17;
  --surface:       #232722;
  --ink:           #E8E4D8;
  --ink-muted:     #B0AB9D;
  --ink-faint:     #A29B95;  /* lightened again 2026-08-17 — was 4.11:1 on --surface,
                                3.36:1 on --row-watch. Now 4.52:1 at its tightest.
                                Dark mode: lighten, not darken. See palette E note */
  --rule:          #353833;
  --rule-strong:   #4A4D46;
  --accent:        #C29F6E;
  --accent-ink:    #C29F6E;  /* --accent as TEXT */
  --positive:      #88B58E;
  --negative:      #D08070;
  --watch:         #D4A562;
  --negative-ink:  #DA8675;  /* --negative as TEXT */
  --watch-ink:     #D4A562;  /* --watch as TEXT — the fill is too light to read on */
  --positive-ink:  #88B58E;  /* --positive as TEXT */
  --on-accent:     #1A1D17;  /* 6.88:1 on #C29F6E — dark on light gold. This is the
                                palette the old #fff failed hardest in, at 2.48:1 */
  --on-fill:       #1A1D17;  /* 5.68 / 7.34 — dark, because this palette's
                                --positive and --negative are both light tints */
  --hover-tint:    rgba(232, 228, 216, 0.05);
  --row-trouble:   #412016;
  --row-watch:     #423111;
  --impact-ink:    #D4A562;
}

/* -- palette B — burgundy / warm rose (private bank) --------- */
body.palette-burgundy {
  --canvas:        #EFDFD6;  /* warm rose-pink cream */
  --surface:       #F8ECE3;
  --ink:           #2A1820;  /* deep burgundy-black */
  --ink-muted:     #5E4448;
  --ink-faint:     #635959;  /* darkened again 2026-08-17 — 4.51:1 on --row-trouble. See palette E note */
  --rule:          #DDC9C0;
  --rule-strong:   #BBA59B;
  --accent:        #7A2A35;  /* deeper burgundy */
  --accent-ink:    #7A2A35;  /* --accent as TEXT */
  --positive:      #2F5F3F;
  --negative:      #8B3F2F;
  --watch:         #A87B36;
  --negative-ink:  #8B3F2F;  /* --negative as TEXT */
  --watch-ink:     #745525;  /* --watch as TEXT — the fill is too light to read on */
  --positive-ink:  #2F5F3F;  /* --positive as TEXT */
  --on-accent:     #F8ECE3;  /* 8.19:1 on #7A2A35 */
  --on-fill:       #F8ECE3;  /* 6.35 / 6.40 */
  --hover-tint:    rgba(42, 24, 32, 0.04);
  --row-trouble:   #FFC8B3;
  --row-watch:     #FFE9C5;
  --impact-ink:    #854F0B;
}

/* -- reset + base --------------------------------------------- */
* { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: var(--t-body); }
body {
  background: var(--canvas);
  color: var(--ink);
  font-family: var(--font-sans);
  font-feature-settings: "tnum" 1, "lnum" 1;
  font-variant-numeric: tabular-nums;
  line-height: 1.45;
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
}
table, .num, .verdict-bullets, .driver-tile, .wo-row, .delta,
.kpi-value, .watchpoint, .exception-row, .occ, .coll, .delq, .age,
.tile-value, .total-value, .total-strip {
  font-variant-numeric: tabular-nums;
}
h1, h2, h3, .serif {
  font-family: var(--font-serif);
  font-weight: 400;
  letter-spacing: -0.01em;
  color: var(--ink);
}
a { color: var(--ink); text-decoration: none; }
a:hover { color: var(--accent-ink); }

/* -- left rail (wide, named, state-grouped) ------------------ */
.rail-wide {
  position: fixed;
  top: 0; left: 0; bottom: 0;
  width: 220px;
  background: var(--surface);
  border-right: 1px solid var(--rule);
  padding: var(--sp-20) 0 var(--sp-24);
  overflow-y: auto;
  z-index: 10;
}
.rail-wide .brand {
  font-family: var(--font-serif);
  font-size: var(--t-head);
  padding: 0 var(--sp-20) var(--sp-16);
  border-bottom: 1px solid var(--rule);
  letter-spacing: -0.01em;
}
.rail-wide .brand .sub {
  display: block;
  font-family: var(--font-sans);
  font-size: var(--t-label);
  color: var(--ink-faint);
  letter-spacing: 0.08em;
  margin-top: var(--sp-2);
  text-transform: uppercase;
}
.rail-wide .nav-group { padding: var(--sp-8) 0; border-bottom: 1px solid var(--rule); }
.rail-wide .nav-group:last-child { border-bottom: none; }
.rail-wide .nav-group-label {
  padding: var(--sp-10) var(--sp-20) var(--sp-4);
  font-size: var(--t-label);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-faint);
  font-weight: 500;
}
.rail-wide .nav-item {
  display: block;
  padding: var(--sp-6) var(--sp-20);
  font-size: var(--t-ui);
  color: var(--ink-muted);
  text-decoration: none;
  border-left: 2px solid transparent;
  position: relative;
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out), border-left-color var(--dur-fast) var(--ease-out);
}
.rail-wide .nav-item:hover { background: var(--canvas); color: var(--ink); }
.rail-wide .nav-item.active {
  background: var(--canvas);
  color: var(--ink);
  border-left-color: var(--accent);
  font-weight: 500;
}
.rail-wide .nav-item .meta {
  float: right;
  font-size: var(--t-label);
  color: var(--ink-faint);
  font-weight: 400;
}
.rail-wide .nav-item .ext {
  float: right;
  font-size: var(--t-label);
  color: var(--ink-faint);
}
/* Offset for the fixed rail. This has to be padding on the BODY, not
   `margin-left` on .shell — a margin-left overrides only the left half of
   .shell's `margin: 0 auto`, so the shell went flush against the rail and every
   pixel of leftover width piled up on the right edge. As padding, the shell
   still centres itself inside whatever space is left beside the rail, so any
   remaining slack is split evenly. Overlays (.side-panel, .backdrop) are
   position:fixed and so are unaffected by this. */
body.has-rail-wide { padding-left: 220px; }

/* -- global search (pinned atop the left rail, Cmd/Ctrl+K) ---- */
.rail-search {
  padding: var(--sp-12) var(--sp-20);
  border-bottom: 1px solid var(--rule);
  position: relative;
}
.rail-search .box { position: relative; }
.rail-search input {
  width: 100%;
  box-sizing: border-box;
  padding: var(--sp-8) var(--sp-32) var(--sp-8) var(--sp-10);
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  color: var(--ink);
  background: var(--canvas);
  border: 1px solid var(--rule-strong);
  outline: none;
  transition: border-color var(--dur-fast) var(--ease-out);
}
.rail-search input::placeholder { color: var(--ink-faint); }
.rail-search input:focus { border-color: var(--accent); }
.rail-search .kbd-hint {
  position: absolute;
  right: 7px; top: 50%;
  transform: translateY(-50%);
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  color: var(--ink-faint);
  background: var(--surface);
  border: 1px solid var(--rule);
  padding: 1px 4px;
  pointer-events: none;
}
.rail-search-results {
  position: absolute;
  left: 20px; right: 20px; top: 100%;
  margin-top: -1px;
  background: var(--surface);
  border: 1px solid var(--rule-strong);
  box-shadow: 0 8px 24px rgba(28, 38, 32, 0.10);
  max-height: 65vh;
  overflow-y: auto;
  z-index: 30;
  display: none;
}
.rail-search-results.open { display: block; }
.rail-search-results .grp-label {
  padding: var(--sp-8) var(--sp-12) var(--sp-4);
  font-size: var(--t-label);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-faint);
  font-weight: 500;
}
.rail-search-results .hit {
  display: block;
  padding: var(--sp-6) var(--sp-12);
  font-size: var(--t-micro);
  color: var(--ink);
  text-decoration: none;
  cursor: pointer;
  border-left: 2px solid transparent;
}
.rail-search-results .hit .sub {
  display: block;
  font-size: var(--t-micro);
  color: var(--ink-muted);
  margin-top: 1px;
}
.rail-search-results .hit:hover,
.rail-search-results .hit.sel {
  background: var(--canvas);
  border-left-color: var(--accent);
}
.rail-search-results .empty {
  padding: var(--sp-10) var(--sp-12);
  font-size: var(--t-micro);
  color: var(--ink-faint);
  font-style: italic;
}

/* -- shell + page header ------------------------------------- */
.shell {
  /* 1280 was leaving ~300px of dead space to the right of an 11-column rent
     roll on a 1920px screen. These pages are dense tables and KPI strips, not
     prose, so they earn the width — but it stays bounded so an ultrawide
     monitor doesn't stretch a row past what the eye can track across. */
  max-width: 1600px;
  margin: 0 auto;
  padding: 32px 40px 80px;
  animation: shell-in var(--dur-slow) var(--ease-out) both;
}
@keyframes shell-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}
.doc-header { margin-bottom: var(--sp-24); position: relative; }
.doc-header h1 {
  font-size: var(--t-display);
  line-height: 1.1;
  margin-bottom: var(--sp-6);
}
.doc-meta {
  font-size: var(--t-label);
  color: var(--ink-muted);
  letter-spacing: 0.04em;
  display: flex;
  gap: var(--sp-12);
  align-items: baseline;
  flex-wrap: wrap;
}
.doc-meta .dot { color: var(--ink-faint); }
.doc-actions {
  position: absolute;
  top: 0;
  right: 56px;   /* clear the fixed account avatar (top-right) */
  display: flex;
  gap: var(--sp-16);
}
.doc-actions a {
  font-size: var(--t-label);
  color: var(--ink-muted);
  border-bottom: 1px solid var(--rule-strong);
  padding-bottom: 1px;
  letter-spacing: 0.04em;
}

/* -- portfolio totals strip ---------------------------------- */
.totals-strip {
  background: var(--surface);
  border: 1px solid var(--rule);
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  margin-bottom: var(--sp-16);
}
.total-cell {
  padding: var(--sp-12) var(--sp-20);
  border-right: 1px solid var(--rule);
  border-left: 3px solid transparent;
}
.total-cell:last-child { border-right: none; }
.total-cell .label {
  font-size: var(--t-label);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-muted);
  margin-bottom: var(--sp-6);
}
.total-cell .total-value {
  font-family: var(--font-serif);
  font-size: var(--t-metric);
  line-height: 1.1;
  letter-spacing: -0.01em;
}
.total-cell .sub {
  font-size: var(--t-label);
  color: var(--ink-muted);
  margin-top: var(--sp-4);
}
.total-cell .delta { font-size: var(--t-label); }
.total-cell .delta.up { color: var(--positive-ink); }
.total-cell .delta.dn { color: var(--negative-ink); }
.total-cell .source {
  font-size: var(--t-label);
  color: var(--ink-faint);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin-top: var(--sp-6);
  font-style: italic;
}
.total-cell .spark { width: 100%; height: 18px; margin-top: var(--sp-6); display: block; }

/* -- section header ------------------------------------------ */
.section-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin: var(--sp-32) 0 var(--sp-10);
  padding-bottom: var(--sp-8);
  border-bottom: 1px solid var(--rule-strong);
}
.section-head h2 {
  font-size: var(--t-head);
  font-weight: 400;
}
.section-head .meta {
  font-size: var(--t-label);
  color: var(--ink-muted);
  letter-spacing: 0.04em;
}
.section-head .meta a {
  color: var(--ink-muted);
  border-bottom: 1px solid var(--rule-strong);
  padding-bottom: 1px;
  margin-left: var(--sp-12);
}

/* -- verdict band -------------------------------------------- */
.verdict-band {
  background: var(--surface);
  border: 1px solid var(--rule);
  padding: var(--sp-20) var(--sp-24);
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--sp-24);
  margin-bottom: var(--sp-16);
}
.verdict-band-main { display: flex; gap: var(--sp-16); align-items: baseline; flex-wrap: wrap; }
.verdict-label {
  font-family: var(--font-serif);
  font-size: var(--t-head);
}
.verdict-label .units {
  font-family: var(--font-sans);
  font-size: var(--t-label);
  color: var(--ink-muted);
  letter-spacing: 0.04em;
  margin-left: var(--sp-6);
}
.verdict-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-6);
  font-size: var(--t-label);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: var(--sp-4) var(--sp-10);
  border: 1px solid currentColor;
}
.verdict-chip.healthy { color: var(--positive-ink); }
.verdict-chip.watch { color: var(--watch-ink); }
.verdict-chip.trouble { color: var(--negative-ink); }
.verdict-chip .glyph {
  font-family: var(--font-serif);
  font-weight: 600;
  font-style: italic;
}
.verdict-bullets {
  display: flex;
  gap: var(--sp-24);
  font-size: var(--t-micro);
  color: var(--ink-muted);
}
.verdict-bullets b {
  font-weight: 500;
  color: var(--ink);
  font-feature-settings: "tnum" 1;
}
.verdict-bullets .delta { font-size: var(--t-label); }
.verdict-bullets .delta.up { color: var(--positive-ink); }
.verdict-bullets .delta.dn { color: var(--negative-ink); }
.verdict-bullets .label-mini {
  color: var(--ink-faint);
  font-size: var(--t-label);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin-right: var(--sp-4);
}
.verdict-band-sparkline { height: 32px; width: 220px; }

/* -- data tables (general) ----------------------------------- */
.data-table {
  background: var(--surface);
  border: 1px solid var(--rule);
  width: 100%;
  border-collapse: collapse;
  font-size: var(--t-micro);
}
.data-table thead th {
  text-align: left;
  font-weight: 400;
  font-size: var(--t-label);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-muted);
  padding: var(--sp-10) var(--sp-12);
  border-bottom: 1px solid var(--rule-strong);
  vertical-align: bottom;
  background: var(--surface);
}
.data-table thead th.num { text-align: right; }
.data-table tbody td {
  padding: var(--sp-10) var(--sp-12);
  border-bottom: 1px solid var(--rule);
  vertical-align: middle;
}
.data-table tbody td.num { text-align: right; font-feature-settings: "tnum" 1; }
.data-table tbody tr:hover { background: var(--hover-tint); }
.data-table tbody tr:last-child td { border-bottom: none; }
.data-table tbody tr.row-watch { background: var(--row-watch); }
.data-table tbody tr.row-trouble { background: var(--row-trouble); }
.data-table .prop-cell { font-weight: 500; }
.data-table .prop-cell .rm { color: var(--ink-faint); font-size: var(--t-micro); font-weight: 400; margin-left: var(--sp-4); }
/* Per-property breakdown beneath a rollup row (by-manager: one PM, >1 property).
   Recessed + tree-marked so it reads as detail under its parent, not a peer. */
.data-table tbody tr.mgr-subrow td { background: var(--canvas); border-bottom-color: var(--rule); }
/* Mute the plain figures, but never a tiered one — the whole point of breaking
   a manager out per property is seeing WHICH property trips the flag. */
.data-table tbody tr.mgr-subrow td.num:not(.warn):not(.alarm) { color: var(--ink-muted); }
.data-table tbody tr.mgr-subrow .sub-prop { padding-left: var(--sp-20); font-size: var(--t-label); color: var(--ink-muted); }
.data-table tbody tr.mgr-subrow .sub-prop::before { content: "└"; color: var(--ink-faint); margin-right: var(--sp-6); }
.data-table .micro { display: inline-flex; flex-direction: column; gap: 1px; font-size: var(--t-label); line-height: 1.3; }
.data-table .label { color: var(--ink-faint); font-size: var(--t-label); letter-spacing: 0.06em; text-transform: uppercase; }
.data-table .spark { width: 100px; height: 22px; display: block; }
.data-table .verdict-mini {
  display: inline-block;
  font-family: var(--font-serif);
  font-style: italic;
  font-size: var(--t-body);
  font-weight: 600;
  width: 18px;
  text-align: center;
}
.data-table .verdict-mini.healthy { color: var(--positive-ink); }
.data-table .verdict-mini.watch { color: var(--watch-ink); }
.data-table .verdict-mini.trouble { color: var(--negative-ink); }

/* -- KPI tile (atomic) --------------------------------------- */
.tile {
  background: var(--surface);
  border: 1px solid var(--rule);
  padding: var(--sp-16) var(--sp-20);
  display: flex;
  flex-direction: column;
  gap: var(--sp-8);
}
.tile-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: var(--t-label);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.tile-head .right { color: var(--ink-faint); font-weight: 400; }
.tile-value {
  font-family: var(--font-serif);
  font-size: var(--t-metric);
  line-height: 1.1;
}
.tile-sub { font-size: var(--t-label); color: var(--ink-muted); display: flex; gap: var(--sp-8); align-items: baseline; }
.tile-sub .delta.up { color: var(--positive-ink); }
.tile-sub .delta.dn { color: var(--negative-ink); }
.tile-spark { width: 100%; height: 28px; }
.tile h3 {
  font-family: var(--font-sans);
  font-size: var(--t-label);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 500;
}

/* -- KPI strip (per-property) -------------------------------- */
.kpi-strip {
  display: grid;
  /* One row regardless of how many tiles there are. This was repeat(6, 1fr),
     which silently wrapped the 7th (Collections) onto a second row and left the
     first row looking short. auto-flow:column sizes to the actual child count,
     so the strip also works for the 5-tile rent-roll totals and the 3-tile
     Collections page without another hardcoded number. minmax(0,1fr) rather
     than 1fr so long values shrink instead of overflowing the track. */
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  background: var(--surface);
  border: 1px solid var(--rule);
  margin-bottom: var(--sp-16);
}
.kpi-strip .kpi {
  padding: var(--sp-10) var(--sp-16);
  border-right: 1px solid var(--rule);
  border-left: 3px solid transparent;
}
.kpi-strip .kpi:last-child { border-right: none; }
.kpi-strip .kpi.clickable { cursor: pointer; position: relative; transition: background var(--dur-fast) var(--ease-out); }
.kpi-strip .kpi.clickable:hover { background: var(--canvas); }
.kpi-strip .kpi.clickable::after {
  content: '↗';
  position: absolute;
  top: 10px;
  right: 12px;
  color: var(--ink-faint);
  font-size: var(--t-micro);
}
.kpi-strip .kpi.clickable:hover::after { color: var(--accent-ink); }
.kpi-strip .label {
  font-size: var(--t-label);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-muted);
  margin-bottom: var(--sp-4);
}
.kpi-strip .v {
  font-family: var(--font-serif);
  font-size: var(--t-metric-sm);
  line-height: 1.1;
}
.kpi-strip .delta { font-size: var(--t-label); }
.kpi-strip .delta.up { color: var(--positive-ink); }
.kpi-strip .delta.dn { color: var(--negative-ink); }
.kpi-strip .source {
  font-size: var(--t-micro);
  color: var(--ink-faint);
  letter-spacing: 0.02em;
  margin-top: var(--sp-4);
  font-style: italic;
}
.kpi-strip .spark { display: block; width: 100%; height: 16px; margin-top: var(--sp-4); }

/* -- exception strip / aging callout ------------------------- */
.exception-strip {
  background: var(--surface);
  border: 1px solid var(--rule);
  margin-bottom: var(--sp-16);
}
.exception-strip-head {
  padding: var(--sp-10) var(--sp-24);
  border-bottom: 1px solid var(--rule);
  display: flex;
  justify-content: space-between;
  font-size: var(--t-label);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.exception-row {
  padding: var(--sp-12) var(--sp-24);
  border-bottom: 1px solid var(--rule);
  display: grid;
  grid-template-columns: 180px 1fr 80px 140px;
  gap: var(--sp-24);
  font-size: var(--t-micro);
  align-items: baseline;
}
.exception-row:last-child { border-bottom: none; }
.exception-row .prop { font-weight: 500; color: var(--ink); }
.exception-row .prop .rm { color: var(--ink-faint); font-size: var(--t-micro); margin-left: var(--sp-4); }
.exception-row .desc { color: var(--ink); }
.exception-row .desc .driver { color: var(--ink-muted); }
.exception-row .age {
  font-size: var(--t-micro);
  text-align: right;
  color: var(--negative-ink);
  font-weight: 500;
}
.exception-row .age.warn { color: var(--watch-ink); }
.exception-row .owner { font-size: var(--t-label); color: var(--ink-muted); }
.exception-row .reason {
  grid-column: 2 / -1;
  margin-top: var(--sp-4);
  font-size: var(--t-label);
  color: var(--ink-faint);
  font-style: italic;
}

/* -- watchpoint strip ---------------------------------------- */
.watchpoints {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-12);
  font-size: var(--t-label);
  color: var(--ink-muted);
  letter-spacing: 0.02em;
}
.watchpoint-squares { display: inline-flex; gap: var(--sp-2); }
.wp { width: 10px; height: 10px; border: 1px solid var(--rule-strong); background: transparent; }
.wp.s1 { background: var(--watch); border-color: var(--watch); }
.wp.s2 { background: var(--negative); border-color: var(--negative); }
.wp.s3 { background: var(--negative); border-color: var(--negative); opacity: 0.85; }
.wp.healthy { background: var(--rule); border-color: var(--rule); }

/* -- recurrence callout band -------------------------------- */
.recurrence-band {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-left: 2px solid var(--accent);
  padding: var(--sp-16) var(--sp-20);
  margin-top: var(--sp-16);
  font-size: var(--t-micro);
}
.recurrence-band .label {
  font-size: var(--t-label);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-ink);
  margin-bottom: var(--sp-8);
}
.recurrence-band .row {
  padding: var(--sp-6) 0;
  border-bottom: 1px solid var(--rule);
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--sp-16);
  align-items: baseline;
}
.recurrence-band .row:last-child { border-bottom: none; }
.recurrence-band .row b { font-weight: 500; }
.recurrence-band .row a { border-bottom: 1px solid var(--rule-strong); padding-bottom: 1px; font-size: var(--t-label); }

/* -- grid system --------------------------------------------- */
.row { display: grid; gap: var(--sp-16); margin-bottom: var(--sp-16); }
.row.cols-3 { grid-template-columns: repeat(3, 1fr); }
.row.cols-2 { grid-template-columns: repeat(2, 1fr); }
.row.cols-2-asym { grid-template-columns: 1fr 1.5fr; }
.row.cols-1 { grid-template-columns: 1fr; }

/* -- aging buckets (horizontal) ------------------------------ */
.aging {
  display: flex;
  font-size: var(--t-label);
  margin-top: var(--sp-8);
  border: 1px solid var(--rule-strong);
}
.aging .bucket {
  flex: 1;
  padding: var(--sp-8) var(--sp-10);
  text-align: center;
  border-right: 1px solid var(--rule-strong);
}
.aging .bucket:last-child { border-right: none; }
.aging .bucket .b-val {
  font-family: var(--font-serif);
  font-size: var(--t-head);
  display: block;
  line-height: 1.1;
}
.aging .bucket .b-label {
  font-size: var(--t-label);
  letter-spacing: 0.05em;
  color: var(--ink-faint);
  text-transform: uppercase;
}
.aging .bucket.warn { color: var(--watch-ink); }
.aging .bucket.alarm { color: var(--negative-ink); }

/* -- tag chips (today's actions, etc.) ----------------------- */
.tag {
  display: inline-block;
  padding: 1px 8px;
  font-size: var(--t-micro);
  letter-spacing: 0.04em;
  border: 1px solid var(--rule-strong);
  color: var(--ink-muted);
  margin-right: var(--sp-4);
}
.tag.urgent { color: var(--negative-ink); border-color: var(--negative); }
.tag.watch { color: var(--watch-ink); border-color: var(--watch); }
.tag.ok { color: var(--positive-ink); border-color: var(--positive); }

/* -- segmented waitlist -------------------------------------- */
.segment-list { font-size: var(--t-micro); }
.segment-row {
  display: grid;
  grid-template-columns: 60px 1fr auto;
  gap: var(--sp-12);
  padding: var(--sp-8) 0;
  border-bottom: 1px solid var(--rule);
  align-items: baseline;
}
.segment-row:last-child { border-bottom: none; }
.segment-row .seg-label { font-weight: 500; }
.segment-row .seg-detail { color: var(--ink-muted); font-size: var(--t-label); }
.segment-row .seg-spark { width: 60px; height: 14px; }
.segment-row .seg-verified { font-size: var(--t-label); color: var(--positive-ink); }
.segment-row .seg-unverified { font-size: var(--t-label); color: var(--ink-faint); }

/* -- footer -------------------------------------------------- */
.foot {
  margin-top: var(--sp-40);
  padding-top: var(--sp-16);
  border-top: 1px solid var(--rule);
  font-size: var(--t-micro);
  color: var(--ink-faint);
  letter-spacing: 0.04em;
  display: flex;
  justify-content: space-between;
}

/* -- expiration distribution chart --------------------------- */
.expiration-chart {
  display: flex;
  align-items: flex-end;
  height: 80px;
  gap: var(--sp-2);
  margin: var(--sp-16) 0 var(--sp-20);
  padding-bottom: var(--sp-16);
  border-bottom: 1px solid var(--rule-strong);
  position: relative;
}
.expiration-chart .week {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  position: relative;
}
.expiration-chart .bar {
  width: 100%;
  background: var(--ink-muted);
  min-height: 1px;
}
.expiration-chart .bar.cluster { background: var(--watch); }
.expiration-chart .bar.alarm { background: var(--negative); }
.expiration-chart .wk-label {
  font-size: var(--t-micro);
  color: var(--ink-faint);
  letter-spacing: 0.04em;
  position: absolute;
  bottom: -14px;
  white-space: nowrap;
}
.expiration-chart .ct {
  font-size: var(--t-micro);
  color: var(--ink);
  font-feature-settings: "tnum" 1;
  position: absolute;
  top: -14px;
}

/* -- side panel (drill-in) ----------------------------------- */
.side-panel {
  position: fixed;
  top: 0; right: 0; bottom: 0;
  width: 520px;
  background: var(--surface);
  border-left: 1px solid var(--rule-strong);
  box-shadow: -8px 0 24px rgba(28, 38, 32, 0.06);
  padding: 32px 32px 60px;
  overflow-y: auto;
  z-index: 50;
  transform: translateX(100%);
  transition: transform var(--dur-base) var(--ease-out);
}
body.has-panel .side-panel { transform: translateX(0); }
.side-panel .close {
  position: absolute;
  top: 20px; right: 24px;
  font-size: var(--t-metric-sm);
  color: var(--ink-muted);
  cursor: pointer;
  line-height: 1;
}
.side-panel .close:hover { color: var(--ink); }
.side-panel h2 {
  font-size: var(--t-metric-sm);
  margin-bottom: var(--sp-4);
  letter-spacing: -0.01em;
}
.side-panel .subtitle {
  font-size: var(--t-label);
  color: var(--ink-muted);
  letter-spacing: 0.04em;
  margin-bottom: var(--sp-20);
}
.side-panel .panel-section {
  margin-top: var(--sp-20);
  padding-top: var(--sp-16);
  border-top: 1px solid var(--rule);
}
.side-panel .panel-section .label {
  font-size: var(--t-label);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: var(--sp-8);
}
.bridge-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--sp-12);
  padding: var(--sp-6) 0;
  font-size: var(--t-ui);
  border-bottom: 1px solid var(--rule);
}
.bridge-row:last-child { border-bottom: none; }
.bridge-row.total {
  font-family: var(--font-serif);
  font-size: var(--t-head);
  padding-top: var(--sp-10);
  border-top: 2px solid var(--ink);
  border-bottom: none;
  margin-top: var(--sp-4);
}
.bridge-row .v { font-feature-settings: "tnum" 1; }
.bridge-row .v.positive { color: var(--positive-ink); }
.bridge-row .v.negative { color: var(--negative-ink); }
.bridge-row .ctx { color: var(--ink-muted); font-size: var(--t-label); margin-left: var(--sp-6); }
.backdrop {
  position: fixed; inset: 0;
  background: rgba(28, 38, 32, 0.18);
  z-index: 40;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-base) var(--ease-out);
}
body.has-panel .backdrop { opacity: 1; pointer-events: auto; }

/* -- kanban turnover board ----------------------------------- */
.kanban-board {
  background: var(--surface);
  border: 1px solid var(--rule);
  padding: var(--sp-20) var(--sp-20) var(--sp-20);
  margin-bottom: var(--sp-16);
}
.kanban-head-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: var(--sp-6);
  gap: var(--sp-12);
  flex-wrap: wrap;
}
.kanban-head-row h2 {
  font-size: var(--t-metric-sm);
  margin: 0;
}
.kanban-controls {
  display: flex;
  gap: var(--sp-6);
  align-items: center;
}
.kanban-search {
  padding: 5px 10px 5px 26px;
  border: 1px solid var(--rule-strong);
  background: var(--canvas);
  font-size: var(--t-label);
  font-family: var(--font-sans);
  color: var(--ink);
  width: 160px;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%235C6660' stroke-width='1.5' viewBox='0 0 24 24'><circle cx='11' cy='11' r='7'/><line x1='16' y1='16' x2='21' y2='21'/></svg>");
  background-repeat: no-repeat;
  background-position: 8px center;
}
.view-toggle {
  display: inline-flex;
  border: 1px solid var(--rule-strong);
}
.view-toggle button {
  padding: var(--sp-6) var(--sp-12);
  background: transparent;
  border: none;
  font-size: var(--t-label);
  color: var(--ink-muted);
  cursor: pointer;
  font-family: var(--font-sans);
}
.view-toggle button.active {
  background: var(--canvas);
  color: var(--ink);
}
.kanban-new {
  padding: var(--sp-6) var(--sp-12);
  background: var(--ink);
  color: var(--surface);
  border: 1px solid var(--ink);
  font-size: var(--t-label);
  cursor: pointer;
  font-family: var(--font-sans);
  letter-spacing: 0.04em;
}
.kanban-note {
  font-size: var(--t-label);
  color: var(--ink-muted);
  margin-bottom: var(--sp-16);
  font-style: italic;
}
.kanban-note a {
  border-bottom: 1px solid var(--rule-strong);
  padding-bottom: 1px;
  font-style: normal;
}
.kanban-cols {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: var(--sp-10);
}
.kanban-col {
  background: var(--canvas);
  border: 1px solid var(--rule);
  padding: var(--sp-12);
  min-height: 260px;
}
.kanban-col-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: var(--sp-12);
  font-size: var(--t-label);
}
.kanban-col-title {
  color: var(--ink);
  font-weight: 500;
}
.kanban-col-count {
  color: var(--ink-muted);
  font-family: var(--font-serif);
  font-size: var(--t-body);
}
.kanban-card {
  background: var(--surface);
  border: 1px solid var(--rule-strong);
  padding: var(--sp-10) var(--sp-12);
  margin-bottom: var(--sp-8);
  cursor: grab;
  font-size: var(--t-label);
  transition: border-color var(--dur-fast) var(--ease-out), opacity var(--dur-fast) var(--ease-out);
}
.kanban-card:active { cursor: grabbing; }
.kanban-card:hover { border-color: var(--accent); }
.kanban-card:last-child { margin-bottom: 0; }
.card-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: var(--sp-4);
}
.card-unit {
  font-weight: 500;
  color: var(--ink);
  font-size: var(--t-micro);
}
.card-program {
  font-size: var(--t-label);
  letter-spacing: 0.08em;
  color: var(--ink-faint);
  text-transform: uppercase;
}
.card-name {
  font-weight: 500;
  margin-bottom: var(--sp-4);
  color: var(--ink);
  font-size: var(--t-label);
}
.card-note {
  font-size: var(--t-micro);
  color: var(--ink-muted);
  margin-bottom: var(--sp-8);
  line-height: 1.45;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.card-flag {
  font-size: var(--t-label);
  letter-spacing: 0.06em;
  color: var(--accent-ink);
  text-transform: uppercase;
  margin-bottom: var(--sp-4);
}
.card-foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.priority {
  font-size: var(--t-micro);
  letter-spacing: 0.04em;
  padding: 1px 7px;
  border: 1px solid currentColor;
  font-family: var(--font-sans);
}
.priority.p1 { color: var(--negative-ink); }
.priority.p2 { color: var(--watch-ink); }
.priority.p3 { color: var(--positive-ink); }
.priority.p4 { color: var(--accent-ink); }
.priority.p5 { color: var(--ink-muted); }
.card-age { font-size: var(--t-micro); color: var(--ink-faint); font-family: var(--font-sans); }

/* responsive: kanban scrolls horizontally on narrow widths */
@media (max-width: 1300px) {
  .kanban-cols { overflow-x: auto; grid-template-columns: repeat(6, 220px); }
}

/* -- lease expiration heatmap (12-month) --------------------- */
.heatmap-table {
  width: 100%;
  border-collapse: collapse;
  background: var(--surface);
  border: 1px solid var(--rule);
  font-size: var(--t-label);
}
.heatmap-table thead th {
  text-align: center;
  font-weight: 400;
  font-size: var(--t-micro);
  color: var(--ink-muted);
  padding: var(--sp-12) var(--sp-4);
  border-bottom: 1px solid var(--rule-strong);
  font-family: var(--font-sans);
}
.heatmap-table thead th.prop-col {
  text-align: left;
  padding-left: var(--sp-20);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-size: var(--t-label);
}
.heatmap-table thead th .year {
  display: block;
  font-size: var(--t-micro);
  color: var(--ink-faint);
  margin-top: var(--sp-2);
}
.heatmap-table tbody td {
  padding: var(--sp-10) var(--sp-4);
  text-align: center;
  border-bottom: 1px solid var(--rule);
  vertical-align: middle;
}
.heatmap-table tbody td.prop-cell {
  text-align: left;
  padding-left: var(--sp-20);
  font-weight: 500;
  font-size: var(--t-ui);
  color: var(--ink);
}
.heatmap-table tbody td.prop-cell .program {
  font-size: var(--t-label);
  color: var(--ink-faint);
  letter-spacing: 0.08em;
  margin-left: var(--sp-8);
  text-transform: uppercase;
  font-family: var(--font-sans);
  font-weight: 400;
}
.heatmap-cell {
  font-family: var(--font-serif);
  font-size: var(--t-body);
  display: inline-block;
  min-width: 32px;
  padding: var(--sp-6) var(--sp-10);
  border: 1px solid transparent;
  cursor: pointer;
  line-height: 1;
  transition: border-color var(--dur-fast) var(--ease-out);
}
.heatmap-cell.zero {
  font-size: var(--t-micro);
  font-family: var(--font-sans);
  color: var(--ink-faint);
  background: transparent;
}
.heatmap-cell.t1 { background: var(--rule); color: var(--ink-muted); }
.heatmap-cell.t2 { background: var(--rule-strong); color: var(--ink); }
.heatmap-cell.t3 { background: var(--ink-faint); color: var(--surface); }
.heatmap-cell.t4 { background: var(--ink-muted); color: var(--surface); }
.heatmap-cell.selected { border-color: var(--accent); border-width: 2px; padding: var(--sp-6) var(--sp-10); }
.heatmap-cell:hover { border-color: var(--accent); }
.heatmap-table tfoot td {
  background: var(--canvas);
  font-weight: 500;
  border-top: 1px solid var(--rule-strong);
  color: var(--ink);
}
.heatmap-table tfoot td.prop-cell {
  font-size: var(--t-label);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-weight: 500;
}
.heatmap-winter-callout {
  color: var(--watch-ink);
  font-weight: 500;
}

/* -- sync now button (inline w/ sync status) ----------------- */
/* ============================================================
   BUTTONS (2026-08-17)

   Until now there was no shared button in this file: 31 of the 53 buttons in
   app.js carried their own inline style string, which is how the "primary"
   button ended up existing in five sizes and six corner radii, and how
   `color:#fff` on `var(--accent)` survived at 2.48:1 in palette-dark.

   Three variants, one geometry, two sizes. Add a variant here rather than
   styling a button at the call site.

   --btn-primary is INK on CANVAS, deliberately not accent: ink/canvas is
   already the body text pair, so it is contrast-audited in all seven
   palettes by construction (11.4:1 - 13.4:1). Accent-filled surfaces are
   for system chrome (the bug FAB, the impersonation bar) and use
   --on-accent. See the note on --on-accent above.
   ============================================================ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-6);
  font-family: var(--font-sans);
  font-size: var(--t-ui);
  font-weight: 500;
  line-height: 1.2;
  padding: var(--sp-8) var(--sp-16);
  border: 1px solid transparent;
  border-radius: var(--radius);
  cursor: pointer;
  text-decoration: none;
  /* background-COLOR, not the background shorthand: the shorthand also
     animates background-image/position, which nothing here changes. */
  transition: background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn[disabled], .btn.is-busy { opacity: 0.55; pointer-events: none; }

.btn-primary { background: var(--ink); color: var(--canvas); border-color: var(--ink); }
.btn-primary:hover { background: var(--ink-muted); border-color: var(--ink-muted); }

.btn-secondary { background: transparent; color: var(--ink); border-color: var(--rule-strong); }
.btn-secondary:hover { background: var(--surface); border-color: var(--ink-muted); }

/* Text-only. Sits next to a primary in a dialog; never the only button. */
.btn-quiet { background: transparent; color: var(--ink-muted); border-color: transparent; padding: var(--sp-8) var(--sp-6); }
.btn-quiet:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }

/* Destructive. --on-fill, not #fff: this palette's --negative is a light
   tint in dark mode, where white text measured 3.00:1. */
.btn-danger { background: var(--negative); color: var(--on-fill); border-color: var(--negative); }
.btn-danger:hover { filter: brightness(1.12); }

/* In-row actions only — "send to collections" on a rent-roll row. Replaces a
   9px dashed control with 1px of vertical padding, which was a ~13px tap
   target on the one action that moves money. */
.btn-sm { font-size: var(--t-micro); padding: var(--sp-6) var(--sp-12); gap: var(--sp-4); }

.sync-now-btn {
  font-size: var(--t-label);
  color: var(--accent-ink);
  border: 1px solid var(--accent);
  padding: var(--sp-2) var(--sp-8);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  margin-left: var(--sp-6);
  cursor: pointer;
  font-family: var(--font-sans);
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
  display: inline-flex;
  align-items: center;
  gap: var(--sp-4);
}
.sync-now-btn:hover { background: var(--accent); color: var(--surface); }
.sync-now-btn .refresh-icon { display: inline-block; font-weight: 600; font-size: var(--t-micro); line-height: 1; }
.sync-now-btn.syncing { color: var(--ink-muted); border-color: var(--rule-strong); pointer-events: none; }
.sync-now-btn.syncing .refresh-icon { animation: sync-spin 0.9s linear infinite; }
@keyframes sync-spin { to { transform: rotate(360deg); } }

/* -- font switcher (vertical stack, 6 options) --------------- */
.font-switcher {
  position: fixed;
  bottom: 16px; right: 176px;
  background: var(--surface);
  border: 1px solid var(--rule-strong);
  padding: var(--sp-8);
  font-size: var(--t-label);
  z-index: 30;
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  min-width: 200px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
  transition: min-width var(--dur-base) var(--ease-out);
}
.font-switcher.collapsed { min-width: 80px; }
.font-switcher.collapsed a { display: none; }
.font-switcher.collapsed .fs-title { border-bottom: none; margin-bottom: 0; padding-bottom: var(--sp-4); }
.font-switcher .fs-title {
  font-size: var(--t-label);
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  padding: var(--sp-4) var(--sp-10) var(--sp-6);
  border-bottom: 1px solid var(--rule);
  margin-bottom: var(--sp-2);
  font-family: var(--font-sans);
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--sp-8);
  user-select: none;
}
.font-switcher .fs-title:hover, .palette-switcher .ps-title:hover { color: var(--accent-ink); }
.toggle-arrow {
  font-size: var(--t-micro);
  color: var(--ink-faint);
  transition: transform var(--dur-base) var(--ease-out);
  display: inline-block;
}
.font-switcher.collapsed .toggle-arrow,
.palette-switcher.collapsed .toggle-arrow { transform: rotate(-90deg); }
.font-switcher a {
  padding: var(--sp-6) var(--sp-10);
  border: 1px solid transparent;
  color: var(--ink-muted);
  display: block;
  text-decoration: none;
  font-size: var(--t-label);
  font-family: var(--font-sans);
}
.font-switcher a .preview {
  font-family: var(--font-serif);
  font-size: var(--t-ui);
  margin-right: var(--sp-6);
  color: var(--ink);
  letter-spacing: -0.01em;
}
.font-switcher a:hover { background: var(--canvas); color: var(--ink); border-color: var(--rule); }
.font-switcher a.active { background: var(--ink); color: var(--surface); border-color: var(--ink); }
.font-switcher a.active .preview { color: var(--surface); }

/* Per-pairing preview rendering (specific font for each option's preview) */
.font-switcher a[data-font="F1"] .preview { font-family: 'Source Serif 4', Georgia, serif; }
.font-switcher a[data-font="F2"] .preview { font-family: 'DM Serif Display', Georgia, serif; }
.font-switcher a[data-font="F3"] .preview { font-family: 'Crimson Pro', Georgia, serif; }
.font-switcher a[data-font="F4"] .preview { font-family: 'EB Garamond', Georgia, serif; }
.font-switcher a[data-font="F5"] .preview { font-family: 'Playfair Display', Georgia, serif; }
.font-switcher a[data-font="F6"] .preview { font-family: 'Lora', Georgia, serif; }

/* -- palette switcher (vertical stack, 6 options) ------------ */
.palette-switcher {
  position: fixed;
  bottom: 16px; right: 16px;
  background: var(--surface);
  border: 1px solid var(--rule-strong);
  padding: var(--sp-8);
  font-size: var(--t-label);
  z-index: 30;
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  min-width: 140px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
}
.palette-switcher.collapsed { min-width: 80px; }
.palette-switcher.collapsed a { display: none; }
.palette-switcher.collapsed .ps-title { border-bottom: none; margin-bottom: 0; padding-bottom: var(--sp-4); }
.palette-switcher .ps-title {
  font-size: var(--t-label);
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  padding: var(--sp-4) var(--sp-10) var(--sp-6);
  border-bottom: 1px solid var(--rule);
  margin-bottom: var(--sp-2);
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--sp-8);
  user-select: none;
}
.palette-switcher a {
  padding: var(--sp-6) var(--sp-10);
  border: 1px solid transparent;
  color: var(--ink-muted);
  display: block;
  text-decoration: none;
  font-size: var(--t-label);
}
.palette-switcher a:hover { background: var(--canvas); color: var(--ink); border-color: var(--rule); }
.palette-switcher a.active { background: var(--ink); color: var(--surface); border-color: var(--ink); }

/* -- driver / WO row reuse ----------------------------------- */
.driver-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--sp-12);
  align-items: baseline;
  padding: var(--sp-8) 0;
  border-bottom: 1px solid var(--rule);
  font-size: var(--t-micro);
}
.driver-row:last-child { border-bottom: none; }
.driver-row .entity { font-weight: 500; }
.driver-row .detail { color: var(--ink-muted); margin-left: var(--sp-6); }
.driver-row .mini-spark { width: 60px; height: 14px; display: block; }
.driver-row .magnitude { font-size: var(--t-label); color: var(--ink-muted); }

/* responsive */
/* Wide tables get a horizontal-scroll wrapper so columns are never clipped
   off-screen (belt-and-suspenders for tablet widths). */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* Rent roll on phones: the 11-column table can't fit, and the split columns
   (Tenant pays / Subsidy) were running off the right edge — invisible on a
   phone. Below 640px each unit becomes a stacked, labeled card instead, so
   every field (especially the tenant-vs-subsidy split) is visible with no
   sideways scrolling. */
@media (max-width: 640px) {
  .rr-table { min-width: 0; }
  .rr-table thead { display: none; }
  .rr-table, .rr-table tbody, .rr-table tr, .rr-table td { display: block; width: auto; }
  .rr-table tr {
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    padding: var(--sp-8) var(--sp-12);
    margin-bottom: var(--sp-10);
    background: var(--surface);
  }
  .rr-table td {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--sp-16);
    padding: var(--sp-4) 0;
    border: none;
    text-align: right;
  }
  .rr-table td::before {
    content: attr(data-label);
    color: var(--ink-muted);
    font-size: var(--t-label);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    text-align: left;
    white-space: nowrap;
  }
  /* Unit label = the card title (no inline label, full-width). */
  .rr-table td[data-label="Unit"] {
    justify-content: flex-start;
    text-align: left;
    font-weight: 600;
    /* was 15px — the only one of its size in the file. Weight 600 already
       marks it as the card title, so it does not also need its own step. */
    font-size: var(--t-body);
    border-bottom: 1px solid var(--rule);
    padding-bottom: var(--sp-6);
    margin-bottom: var(--sp-4);
  }
  .rr-table td[data-label="Unit"]::before { display: none; }
  /* The whole point on mobile — make the contract rent + split stand out. */
  .rr-table td[data-label="Contract"],
  .rr-table td[data-label="Tenant pays"],
  .rr-table td[data-label="Subsidy"] { font-weight: 500; color: var(--ink); }

  /* ─── Mobile pass (2026-07-30) — every surface usable on a phone ─── */
  /* Side panels — ledgers, work-order detail, tenant history, open-balance —
     were a fixed 520px, wider than the screen. Full-screen now; their inner
     tables scroll within instead of clipping. */
  .side-panel { width: 100%; max-width: 100%; }
  .side-panel .panel-section { overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .side-panel table { max-width: 100%; }
  /* Property sub-tabs (7 of them) scroll sideways rather than wrap/clip. */
  .subtabs { overflow-x: auto; flex-wrap: nowrap; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
  .subtabs::-webkit-scrollbar { display: none; }
  .subtabs button { flex: 0 0 auto; }
  /* Any wide table (auto-wrapped in .table-scroll by JS) scrolls, never clips. */
  .table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
  /* KPI / totals tiles: 2-up (3 is too tight at ~390px). !important beats the
     ≤1100 rule that also matches at this width. */
  .kpi-strip, .totals-strip { grid-auto-flow: row !important; grid-template-columns: repeat(2, 1fr) !important; }
  /* Multi-column content rows collapse to a single column. */
  .row.cols-2, .row.cols-3, .row.cols-2-asym { grid-template-columns: 1fr !important; }
  /* Settings: stack the left nav above the content (inline flex needs !important). */
  .settings-layout { flex-direction: column; }
  .settings-nav { flex: 1 1 auto !important; display: flex; flex-wrap: wrap; gap: var(--sp-4) var(--sp-10); margin-bottom: var(--sp-10); }
  /* Let the page header wrap instead of squeezing. */
  .doc-header { flex-wrap: wrap; }
}

/* Mobile nav hamburger + drawer backdrop. Hidden on desktop; the media query
   below reveals the hamburger and turns the rail into a slide-in drawer.
   z-order: toggle(8) < backdrop(9) < rail(10) — so an open drawer covers the
   hamburger and a tap on the exposed backdrop closes it. */
.nav-toggle {
  display: none;
  position: fixed;
  top: 9px; left: 9px;
  z-index: 8;
  width: 42px; height: 42px;
  align-items: center;
  justify-content: center;
  padding: 0;
  font-size: var(--t-metric-sm);
  line-height: 1;
  cursor: pointer;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.28);
}
.nav-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 9;
  background: rgba(0, 0, 0, 0.45);
}

@media (max-width: 1100px) {
  .totals-strip { grid-template-columns: repeat(3, 1fr); }
  .kpi-strip { grid-auto-flow: row; grid-template-columns: repeat(3, 1fr); }
  /* Leave room at the top for the fixed hamburger so it doesn't cover titles. */
  body.has-rail-wide { padding-left: 0; }
  body.has-rail-wide .shell { padding-top: 54px; }
  /* The sidebar becomes an off-canvas drawer (previously `display:none`, which
     left mobile users with NO navigation — stuck on whatever page they opened).
     The hamburger toggles `body.nav-open`, sliding it in over a tap-to-close
     backdrop. */
  .rail-wide {
    transform: translateX(-100%);
    transition: transform var(--dur-base) var(--ease-out);
    box-shadow: 3px 0 22px rgba(0, 0, 0, 0.4);
    width: 82vw;
    max-width: 300px;
  }
  body.nav-open .rail-wide { transform: translateX(0); }
  .nav-toggle { display: flex; }
  body.nav-open .nav-backdrop { display: block; }
}

/* ============================================================
   v10 additions — exec dashboard property cards, panel KPI rows,
   property sub-tabs. Tokens unchanged (palette E + locked fonts).
   ============================================================ */

/* -- property card (dashboard, one per property) -------------- */
.prop-card {
  background: var(--surface);
  border: 1px solid var(--rule);
  margin-bottom: var(--sp-12);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out);
}
.prop-card:hover { border-color: var(--rule-strong); background: var(--surface); transform: translateY(-1px); }
.prop-card:hover .prop-card-head .open-hint { color: var(--accent-ink); }
.prop-card-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: var(--sp-10) var(--sp-20) var(--sp-8);
  border-bottom: 1px solid var(--rule);
}
.prop-card-head .name {
  font-family: var(--font-serif);
  font-size: var(--t-head);
  letter-spacing: -0.01em;
}
.prop-card-head .name a { border-bottom: 1px solid transparent; }
.prop-card-head .name a:hover { border-bottom-color: var(--accent); }
.prop-card-head .meta {
  font-size: var(--t-label);
  color: var(--ink-muted);
  letter-spacing: 0.04em;
}
.prop-card-head .open-hint {
  font-size: var(--t-label);
  color: var(--ink-faint);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.prop-card-minis {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
}
.prop-card-minis .mini {
  padding: var(--sp-10) var(--sp-20) var(--sp-10);
  border-right: 1px solid var(--rule);
}
.prop-card-minis .mini:last-child { border-right: none; }
.prop-card-minis .label {
  font-size: var(--t-label);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-faint);
  margin-bottom: var(--sp-4);
}
.prop-card-minis .v {
  font-family: var(--font-serif);
  font-size: var(--t-head);
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
.prop-card-minis .v.warn { color: var(--watch-ink); }
.prop-card-minis .v.alarm { color: var(--negative-ink); }

/* -- KPI tile "needs attention" tint (design pass, 2026-07-12) -----------
   Stoplight tiers already colored the region/property-card numbers above;
   extending the same tiers to the top-level KPI strips (dashboard + property
   page) so the one off-threshold metric in a 6-tile row actually stands out
   at a glance, instead of every tile reading with equal visual weight. */
.kpi-strip .kpi.warn, .totals-strip .total-cell.warn {
  border-left-color: var(--watch);
  background: var(--row-watch);
}
.kpi-strip .kpi.alarm, .totals-strip .total-cell.alarm {
  border-left-color: var(--negative);
  background: var(--row-trouble);
}
/* The VALUE stays --ink. Severity is carried by the 4px stripe and the tint,
   not by recolouring the number — `$58,850` in --negative on --row-trouble
   measured 2.18:1 in palette A, i.e. the most urgent figure on the dashboard
   was the hardest one to read. A stripe is a graphic, judged at 3:1, and it
   does the same signalling job without fighting the text.
   Sub-lines may still carry colour, but via the *-ink tokens. */
.kpi-strip .kpi.warn, .kpi-strip .kpi.alarm,
.totals-strip .total-cell.warn, .totals-strip .total-cell.alarm { border-left-width: 4px; }
.prop-card-minis .delta { font-size: var(--t-micro); margin-left: var(--sp-4); }
.prop-card-minis .delta.up { color: var(--positive-ink); }
.prop-card-minis .delta.dn { color: var(--negative-ink); }
.prop-card-minis .sub {
  font-size: var(--t-micro);
  color: var(--ink-muted);
  margin-top: var(--sp-4);
}
.prop-card-minis .sub .alarm { color: var(--negative-ink); font-weight: 500; }
.prop-card-minis .sub .warn { color: var(--watch-ink); font-weight: 500; }

/* -- region card expand/collapse (dashboard) -------------------- */
.region-chev {
  transition: transform var(--dur-base) var(--ease-out);
  transform-origin: 45% center;
}
.region-chev.open { transform: rotate(90deg); }
.region-props.open { animation: fade-in var(--dur-base) var(--ease-out) both; }

/* -- side-panel KPI rows (property quick view) ----------------- */
.panel-kpi {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--sp-12);
  padding: var(--sp-10) 0;
  border-bottom: 1px solid var(--rule);
  align-items: baseline;
  font-size: var(--t-ui);
}
.panel-kpi:last-child { border-bottom: none; }
.panel-kpi .k-label { color: var(--ink-muted); }
.panel-kpi .k-sub { display: block; font-size: var(--t-label); color: var(--ink-faint); margin-top: 1px; }
.panel-kpi .k-value {
  font-family: var(--font-serif);
  font-size: var(--t-head);
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.panel-kpi .k-value .delta { font-family: var(--font-sans); font-size: var(--t-label); margin-left: var(--sp-6); }
.panel-kpi .k-value .delta.up { color: var(--positive-ink); }
.panel-kpi .k-value .delta.dn { color: var(--negative-ink); }
.panel-kpi .k-value.warn { color: var(--watch-ink); }
.panel-kpi .k-value.alarm { color: var(--negative-ink); }
.panel-kpi .k-sub .warn, .panel-kpi .k-sub.warn { color: var(--watch-ink); font-weight: 500; }
.panel-kpi .k-sub .alarm, .panel-kpi .k-sub.alarm { color: var(--negative-ink); font-weight: 500; }
.panel-kpi .k-value .warn { color: var(--watch-ink); }
.panel-kpi .k-value .alarm { color: var(--negative-ink); }
.panel-flag-row {
  padding: var(--sp-8) 0;
  border-bottom: 1px solid var(--rule);
  font-size: var(--t-micro);
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--sp-12);
  align-items: baseline;
}
.panel-flag-row:last-child { border-bottom: none; }
.panel-flag-row .f-val { font-weight: 500; }
.panel-flag-row .f-val.alarm { color: var(--negative-ink); }
.panel-flag-row .f-val.warn { color: var(--watch-ink); }
.panel-flag-row .f-none { color: var(--ink-faint); font-style: italic; }

/* -- property page sub-tabs (Amit's hierarchy, 2026-06-08) ----- */
.subtabs {
  display: flex;
  gap: 0;
  border-bottom: 1px solid var(--rule-strong);
  margin: var(--sp-24) 0 var(--sp-16);
}
.subtabs button {
  padding: var(--sp-10) var(--sp-20);
  background: transparent;
  border: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  letter-spacing: 0.04em;
  color: var(--ink-muted);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out), border-bottom-color var(--dur-base) var(--ease-out);
}
.subtabs button:hover { color: var(--ink); background: var(--hover-tint); }
.subtabs button.active {
  color: var(--ink);
  border-bottom-color: var(--accent);
  font-weight: 500;
}

/* -- Trends page filter rows — reuse .subtabs button styling (flat, square,
   bottom-border active state) so property/metric/period pickers match the
   rest of the site instead of standing alone as rounded pills. ----------- */
.trends-filter-row { display: flex; align-items: center; gap: var(--sp-10); margin-bottom: var(--sp-8); }
/* The property row holds wrapping chips, so its label lines up with the FIRST
   row of them rather than floating in the middle of a two-row block. */
.trends-filter-row.is-multi { align-items: flex-start; }
.trends-filter-row.is-multi .trends-filter-label { padding-top: var(--sp-8); }
.trends-filter-label {
  font-size: var(--t-label);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-muted);
  /* min-width, not width: this used to be a hard 60px, which fit "PROPERTIES"
     at 9px and clipped it to "PROPERTIE" once the 11px floor landed. The column
     still aligns (every label reaches the same minimum) but a longer word is
     now allowed to take the room it needs. */
  min-width: 60px;
  white-space: nowrap;
  flex: 0 0 auto;
}
.trends-tabs { border-bottom: none; margin: 0; flex: 1; flex-wrap: wrap; }
.trends-tabs button:disabled { opacity: 0.45; cursor: not-allowed; }
.trends-caption { font-size: var(--t-label); color: var(--ink-muted); margin-top: var(--sp-8); }
.subtab-pane { display: none; }
.subtab-pane.active { display: block; animation: fade-in var(--dur-base) var(--ease-out) both; }
@keyframes fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* -- flag tiers on table cells (insights rollups) -------------- */
.data-table td.warn { color: var(--watch-ink); font-weight: 500; }
.data-table td.alarm { color: var(--negative-ink); font-weight: 500; }

/* -- movement over the selected window (E3, by-manager rollup) --
   Rendered from an ATTRIBUTE via ::after rather than as a child node, on
   purpose: applyBindings writes el.textContent on these exact cells, which
   would wipe any element placed inside them. An attribute survives that, so
   the delta can be painted at any point in the boot sequence without having
   to be ordered against the binder. (_countUp does not reach these cells —
   COUNT_UP_SELECTOR covers .total-value and .kpi-strip .v, not table cells —
   so it is applyBindings alone that makes this necessary.) */
.data-table td.num[data-delta]::after {
  content: attr(data-delta);
  display: block;
  margin-top: var(--sp-2);
  font-size: var(--t-micro);
  font-weight: 400;
  letter-spacing: 0.02em;
  color: var(--ink-faint);
}
.data-table td.num[data-delta-dir="good"]::after { color: var(--positive-ink); }
.data-table td.num[data-delta-dir="bad"]::after  { color: var(--negative-ink); }

/* -- "Trend" sparkline column, This Week's four-week table (2026-08-17) ---
   Sized in px, not a token: this is a drawing box, not type or rhythm. The
   column is fixed-width so every row's shape shares a baseline and the eye can
   compare them down the column — a shape that rescales per row is a shape you
   cannot read against its neighbours. */
.tw-spark-col {
  width: 96px;
  text-align: left;
  white-space: nowrap;
}
.tw-spark {
  display: block;
  width: 84px;
  height: 20px;
  overflow: visible;
}
@media (max-width: 900px) {
  /* The numbers are the payload; on a phone the shape is the first thing to go. */
  .tw-spark-col { display: none; }
}

/* Trends shows its filters outright — see the note in trends.html. Only the
   bottom rule of the old fold is kept, so the controls still read as a band
   above the chart rather than floating loose. */
.trends-filters-open {
  padding-bottom: var(--sp-16);
  margin-bottom: var(--sp-16);
  border-bottom: 1px solid var(--rule);
}

/* -- folded filter block (2026-08-17) -------------------------------------
   The controls were costing ~230px above the chart on Trends. Folded, the page
   opens on its answer and states the question in one line. */
.filter-fold { margin-bottom: var(--sp-16); }
.filter-fold > summary {
  display: flex;
  align-items: baseline;
  gap: var(--sp-12);
  cursor: pointer;
  padding: var(--sp-6) 0;
  border-bottom: 1px solid var(--rule);
  list-style: none;
}
.filter-fold > summary::-webkit-details-marker { display: none; }
.filter-fold > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.filter-fold-line {
  font-size: var(--t-ui);
  color: var(--ink);
  flex: 1;
  min-width: 0;
}
/* The cue is the affordance, so it must not be the faintest thing in the row. */
.filter-fold-cue {
  font-size: var(--t-label);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent-ink);
  white-space: nowrap;
}
.filter-fold > summary:hover .filter-fold-cue { text-decoration: underline; text-underline-offset: 3px; }
.filter-fold[open] > summary .filter-fold-cue::after { content: " \2014 hide"; }
.filter-fold-body { padding-top: var(--sp-16); }

/* -- delinquency carried/this-month bar (2026-08-17) ----------------------
   Two segments inside the Delinquency tile, above the sentence that names them.
   Carried arrears is the part somebody has to chase, so it takes the severity
   colour; this month's billed-not-yet-paid rent is mostly not late and reads as
   the quieter half. The segments are FILLS, so they use the vivid tokens rather
   than the -ink text variants. */
.delinq-bar {
  display: flex;
  height: 6px;
  margin: var(--sp-6) 0 var(--sp-4);
  background: var(--rule);
  border-radius: var(--radius);
  overflow: hidden;
}
.delinq-bar i { display: block; height: 100%; }
.dq-carried { background: var(--negative); }
/* Deliberately not a second alarm colour: this half is the billing cycle, not a
   problem, and colouring it red would double-count the urgency. */
.dq-mtd { background: var(--rule-strong); }

/* -- risk strip: one row per property, worst first (2026-08-17) -----------
   Tints reuse --row-watch / --row-trouble, so a cell here means exactly what the
   same colour means on a tile. The severity is in the cell FILL rather than the
   figure's colour, for the reason the tiles now do the same: recolouring a number
   costs contrast on the number you most need to read. */
/* The wrapper carries .table-scroll (the stylesheet's existing utility) so the
   table scrolls INSIDE this box. Without it the 690px-wide table pushed the whole
   page wider than a 393px phone viewport, which is what test_mobile_layout's
   horizontal-overflow check caught. */
.panel-flush { border: 1px solid var(--rule); background: var(--surface); max-width: 100%; }
.risk-strip { width: 100%; min-width: 640px; border-collapse: collapse; font-size: var(--t-ui); }
.risk-strip th {
  font-size: var(--t-label);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
  text-align: left;
  padding: var(--sp-10) var(--sp-12);
  border-bottom: 1px solid var(--rule-strong);
  white-space: nowrap;
}
.risk-strip td {
  padding: var(--sp-10) var(--sp-12);
  border-bottom: 1px solid var(--rule);
  font-variant-numeric: tabular-nums;
}
.risk-strip tbody tr:last-child td { border-bottom: 0; }
.risk-strip td.num, .risk-strip th.num { text-align: right; white-space: nowrap; }
.risk-strip td.nm { white-space: nowrap; }
.risk-strip td.nm a { color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--rule-strong); }
.risk-strip td.nm a:hover { border-bottom-color: var(--accent); }
.risk-strip td.warn  { background: var(--row-watch); }
.risk-strip td.alarm { background: var(--row-trouble); font-weight: 500; }
.risk-strip tbody tr:hover td { background: var(--hover-tint); }
.risk-strip tbody tr:hover td.warn  { background: var(--row-watch); }
.risk-strip tbody tr:hover td.alarm { background: var(--row-trouble); }

/* -- sample-data tag (honest demo labeling) -------------------- */
.demo-tag {
  font-size: var(--t-label);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
  border: 1px solid var(--rule-strong);
  padding: 1px 6px;
  font-style: normal;
}

/* Work-order filter chips are the standalone layout of the one filter control
   below — see .period-btn. Only the state-word alias lives here. */
.wo-filter-n { opacity: 0.6; }

/* ================================================================
   PREVIEW BRANCH — animation/transition/graphics brainstorm
   (2026-07-12, mockup-preview/ copy only — not the live site).
   ================================================================ */

/* -- tier-change pulse — brief flash on a KPI tile whose stoplight
   tier got WORSE since the last time this browser loaded the page
   (see applyLiveFlags' localStorage snapshot in app.js). Distinct
   from the persistent .warn/.alarm tint: this says "this just
   changed," not just "this is currently bad." -------------------- */
@keyframes tier-pulse-warn  { 0%, 100% { background: var(--row-watch); }   50% { background: var(--watch); } }
@keyframes tier-pulse-alarm { 0%, 100% { background: var(--row-trouble); } 50% { background: var(--negative); } }
.kpi-strip .kpi.warn.tier-pulse, .totals-strip .total-cell.warn.tier-pulse {
  animation: tier-pulse-warn 1s var(--ease-out) 1;
}
.kpi-strip .kpi.alarm.tier-pulse, .totals-strip .total-cell.alarm.tier-pulse {
  animation: tier-pulse-alarm 1s var(--ease-out) 1;
}

/* -- trend chart draw-in — the curve reveals left-to-right on mount
   (stroke-dasharray/dashoffset driven from JS, timed to the path's REAL
   measured length via getTotalLength() — see _mountTrendChart in app.js,
   not a fixed CSS guess). Dots fade+scale in after, staggered per-point.
   The area fill fades in over the same window as the line draws. -------- */
.trend-dot {
  opacity: 0;
  transform-box: fill-box;
  transform-origin: center;
  animation: trend-dot-in var(--dur-base) var(--ease-out) forwards;
  transition: transform var(--dur-fast) var(--ease-out);
}
@keyframes trend-dot-in {
  from { opacity: 0; transform: scale(0.3); }
  to   { opacity: 1; transform: scale(1); }
}
.trend-dot.hover { transform: scale(1.3); }

/* -- F1 work-order bars — grow from the baseline, staggered per BAND from
   JS (see _mountTrendChart) so an adjacent Opened/Closed pair rises
   together and reads as one comparison. The global prefers-reduced-motion
   block at the top of this file collapses the duration; `forwards` still
   lands the end state, so the bars are never left invisible. ---------- */
.trend-bar {
  opacity: 0;
  transform-box: fill-box;
  transform-origin: bottom;
  animation: trend-bar-in var(--dur-base) var(--ease-out) forwards;
}
@keyframes trend-bar-in {
  from { opacity: 0; transform: scaleY(0.02); }
  to   { opacity: 1; transform: scaleY(1); }
}
/* The hover target is a full-height band per period. It carries
   fill-opacity="0" as a presentation attribute, which any CSS rule
   outranks — so this lights the whole period under the pointer, telling
   you which pair the tooltip is describing. */
.trend-hit:hover { fill-opacity: 0.06; }

/* -- trend chart swap transition — fade+shrink the OLD chart out before
   the new one paints, when switching property/metric on an already-drawn
   chart (see _mountTrendChart). Not applied on first mount. ------------- */
.trend-fade-container {
  transition: opacity 170ms var(--ease-out), transform 170ms var(--ease-out);
}
.trend-fade-container.trend-swap-out {
  opacity: 0;
  transform: scale(0.98);
}

/* -- region accordion — JS-measured max-height transition (see
   app.js) replacing the instant display:none/block snap. The
   fade-in on .open (already in the base stylesheet) still applies;
   this adds the actual height motion under it. ------------------- */
.region-props { overflow: hidden; transition: max-height var(--dur-base) var(--ease-out); }

/* -- sliding tab-underline indicator — a single absolutely-positioned
   span per tab strip that slides between the active tab's bounds
   instead of the underline jumping (see mountSlidingIndicator() in
   app.js). Tab strips need position:relative for this to anchor. --- */
.subtabs, .settings-nav { position: relative; }
.subtabs .tab-indicator {
  position: absolute;
  bottom: 0;
  height: 2px;
  background: var(--accent);
  transition: left var(--dur-base) var(--ease-out), width var(--dur-base) var(--ease-out);
  pointer-events: none;
}
.settings-nav .tab-indicator {
  position: absolute;
  left: 0;
  width: 2px;
  background: var(--accent);
  transition: top var(--dur-base) var(--ease-out), height var(--dur-base) var(--ease-out);
  pointer-events: none;
}

/* -- search dropdown entrance — fade + slight rise instead of a
   flat display toggle (see app.js — .open still gates visibility,
   this just animates the transition into that state). ------------ */
.rail-search-results {
  opacity: 0;
  transform: translateY(-4px);
  visibility: hidden;
  transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out), visibility 0s linear var(--dur-fast);
}
.rail-search-results.open {
  opacity: 1;
  transform: translateY(0);
  visibility: visible;
  transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out);
}

/* -- save/delete micro-feedback — a small inline checkmark that
   fades in then out next to a "Saved." message (see flashSuccess()
   in app.js). ------------------------------------------------------ */
.flash-check {
  display: inline-block;
  color: var(--positive-ink);
  opacity: 0;
  animation: flash-check-in 1.6s var(--ease-out) 1;
}
@keyframes flash-check-in {
  0%   { opacity: 0; transform: scale(0.6); }
  15%  { opacity: 1; transform: scale(1); }
  75%  { opacity: 1; }
  100% { opacity: 0; }
}

/* -- empty-state glyph — small flat-line SVG icon shown above an
   empty-state message (no open WOs, no notes, no search results).
   Zero rounded corners / single-weight stroke to match the app's
   existing line language. ------------------------------------------ */
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-10);
  padding: var(--sp-32) 0;
  color: var(--ink-faint);
  font-size: var(--t-micro);
  font-style: italic;
  text-align: center;
}
.empty-state svg { width: 40px; height: 40px; opacity: 0.55; }
.rail-search-results .empty-state { padding: var(--sp-16) 0; }
.rail-search-results .empty-state svg { width: 26px; height: 26px; }

/* -- brand mark — a small geometric glyph next to the "Action
   Tracker" wordmark (rail + login page). Same flat rectilinear
   language as the rest of the UI, no rounded corners. --------------- */
.brand-mark {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-8);
}
.brand-mark svg { width: 20px; height: 20px; flex: none; }
.login-card .brand-mark svg { width: 28px; height: 28px; }
.login-card .brand-mark { display: flex; justify-content: center; margin-bottom: var(--sp-4); }

/* Cross-site brand dropdown. The SVG mark is untouched; only the product
   name becomes a trigger, and only once TanzaTerra's public menu returns
   at least one other origin. Closed: the same serif wordmark. Open: a
   panel of sister sites, position:fixed so the rail's overflow does not
   clip it (same reason the reminders panel is fixed). */
.site-menu { position: relative; display: inline-flex; min-width: 0; }
.site-menu-trigger {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  cursor: pointer;
  text-align: left;
}
.site-menu-trigger:hover .site-menu-label,
.site-menu-trigger:focus-visible .site-menu-label { color: var(--accent-ink); }
.site-menu-trigger:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 3px;
}
.site-menu-chevron {
  width: 0;
  height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid var(--accent);
  flex: none;
  transition: transform var(--dur-fast) var(--ease-out);
}
.site-menu-trigger[aria-expanded="true"] .site-menu-chevron {
  transform: rotate(180deg);
}
.site-menu-panel {
  position: fixed;
  z-index: 70;
  min-width: 220px;
  max-width: min(280px, calc(100vw - 24px));
  padding: 6px 0 8px;
  background: var(--surface);
  border: 1px solid var(--rule-strong);
  border-radius: 10px;
  box-shadow: 0 8px 30px rgba(28, 42, 32, 0.18);
}
.site-menu-heading {
  margin: 0;
  padding: 8px 12px 4px;
  font-family: var(--font-sans);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.site-menu-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  font-family: var(--font-sans);
  font-size: var(--t-ui);
  color: var(--ink);
  text-decoration: none;
}
.site-menu-item:hover { background: var(--canvas); color: var(--ink); }
.site-menu-item-name { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.site-menu-ext {
  margin-left: auto;
  font-size: 11px;
  color: var(--ink-faint);
}

/* ================================================================
   PREVIEW BRANCH, round 2 — pushing the brainstorm further: choreographed
   load-in, scroll reveal, a nav fade-veil, milestone glows, theme crossfade.
   ================================================================ */

/* -- the value that MOVED (2026-08-17) ------------------------------------
   This block replaced three things: the staggered KPI entrance, the
   scroll-reveal on region cards, and the navigation fade-veil. All three
   animated arrival — content that was already loaded, held back so it could be
   revealed. The entrance one was actively harmful: it left the top strip blank
   for the first ~500ms of a page, which is the half-second somebody is looking
   hardest at it.

   What is left marks CHANGE. A tile whose headline figure differs from the one
   this browser last saw on this page gets a brief sweep and settles to a quiet
   rule underneath it, so a glance answers "what is different since I was last
   here" without reading six numbers. Paired with the count-up in app.js, which
   now fires only on these.

   The mark is a border, not a fill or a colour change: fills are how severity
   is expressed on these tiles already, and a moved value is not a severity.
   No layout properties animate — a tile that shifts while being read is worse
   than one that does not announce itself. --------------------------------- */
@keyframes value-moved-sweep {
  from { background-position: 140% 0; }
  to   { background-position: -40% 0; }
}
.value-moved {
  position: relative;
  border-bottom: 2px solid var(--accent);
}
.value-moved::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(100deg,
    transparent 30%, var(--hover-tint) 50%, transparent 70%);
  background-size: 220% 100%;
  animation: value-moved-sweep 900ms var(--ease-out) 1 both;
}
@media (prefers-reduced-motion: reduce) {
  /* The rule still says which tile moved; only the sweep goes. The information
     survives without the motion, which is the point of the preference. */
  .value-moved::after { display: none; }
}

/* -- skeletons ------------------------------------------------------------
   Reserve the shape of pending content so its arrival is a fill, not a shove.
   The shimmer is the one place an indeterminate animation is right: there is
   genuinely nothing to show yet, so it is not animating the arrival of data
   that already exists. Collapses to a static bar under reduced-motion. ---- */
@keyframes skel-shimmer {
  from { background-position: 180% 0; }
  to   { background-position: -80% 0; }
}
.skel-bar {
  display: block;
  height: 9px;
  border-radius: var(--radius);
  background: linear-gradient(90deg, var(--rule) 25%, var(--rule-strong) 50%, var(--rule) 75%);
  background-size: 260% 100%;
  animation: skel-shimmer 1.4s linear infinite;
}
.skel-block { display: flex; flex-direction: column; gap: var(--sp-12); padding: var(--sp-16) 0; }
.skel-row td { padding: var(--sp-12); border-bottom: 1px solid var(--rule); }
.skel-row td .skel-bar { width: 70%; }
.skel-row:nth-child(even) td .skel-bar { width: 52%; }
@media (prefers-reduced-motion: reduce) {
  .skel-bar { animation: none; background: var(--rule); }
}

/* -- milestone glow — a one-time soft radiating glow (not confetti; this
   is an ops ledger, not a game) when a KPI tile crosses into a genuine
   milestone for the first time (100% occupancy, zero open work orders).
   Uses a wide, opaque-color blur rather than rgba() so it reads correctly
   against every palette without needing a companion RGB-triplet token. --- */
@keyframes milestone-glow {
  0%   { box-shadow: 0 0 0 0 var(--positive); }
  50%  { box-shadow: 0 0 18px 6px var(--positive); }
  100% { box-shadow: 0 0 0 0 var(--positive); }
}
.milestone-glow { animation: milestone-glow 1.4s var(--ease-out) 1; }

/* -- theme/palette switch crossfade — briefly promotes background/color/
   border transitions to --dur-slow (and to `!important`, since per-element
   hover transitions would otherwise win) so flipping the palette in
   Settings reads as a smooth reskin instead of an instant flicker. Toggled
   on/off around the swap in app.js — not a permanent rule. --------------- */
body.theme-switching, body.theme-switching * {
  transition: background-color var(--dur-slow) var(--ease-out),
              color var(--dur-slow) var(--ease-out),
              border-color var(--dur-slow) var(--ease-out) !important;
}

/* Segmented period selector — Insights "closed per person" (E3). Sits inside a
   1px bordered group; buttons share internal rules so the set reads as one
   control rather than four separate buttons. */
/* ONE filter control (2026-08-17).

   There were three ways to render "which slice am I looking at": .period-btn,
   .wo-filter-chip, and — worst — per-call-site inline styles where a ternary
   also carried the active/inactive state. That third kind is why the leasing
   period buttons went unreadable: the active branch said background:var(--ink)
   and the inactive branch said background:transparent, so any class applying a
   fixed text colour was wrong for one of the two states.

   State now lives in ONE place, the .is-active class, so it cannot disagree
   with the paint. Two layouts:
     default        cells that share borders inside a wrapper (segmented)
     .is-standalone separate chips with their own border and radius
   .active is accepted alongside .is-active because two work-order call sites
   already used that word. */
.period-btn, .wo-filter-chip {
  appearance: none;
  border: 0;
  border-left: 1px solid var(--rule-strong);
  background: var(--canvas);
  color: var(--ink-muted);
  font: inherit;
  font-size: var(--t-micro);
  letter-spacing: 0.02em;
  padding: var(--sp-6) var(--sp-12);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.period-btn:first-child, .wo-filter-chip:first-child { border-left: 0; }
.period-btn:hover, .wo-filter-chip:hover { color: var(--ink); background: var(--surface); }
.period-btn.is-active, .wo-filter-chip.is-active,
.period-btn.active, .wo-filter-chip.active {
  background: var(--ink);
  color: var(--surface);
  font-weight: 500;
}
/* Standalone chips: not in a shared-border wrapper, so they carry their own.
   This block must NOT set `background` unconditionally — it sits after the
   .is-active rule at equal specificity, so a bare `background: transparent`
   here silently wins and leaves the active chip's inverted text colour on a
   see-through ground (measured 1.12:1). The :not() keeps it order-independent
   instead of relying on where the rule happens to sit in the file. */
.period-btn.is-standalone, .wo-filter-chip {
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
}
.period-btn.is-standalone:not(.is-active):not(.active),
.wo-filter-chip:not(.is-active):not(.active) {
  background: transparent;
}
.period-btn.is-standalone.is-active, .period-btn.is-standalone.active,
.wo-filter-chip.is-active, .wo-filter-chip.active {
  border-color: var(--ink);
}
/* Keyboard focus must stay visible against the inverted active state. */
.period-btn:focus-visible, .wo-filter-chip:focus-visible {
  outline: 2px solid var(--accent); outline-offset: -2px;
}
.period-btn[disabled], .wo-filter-chip[disabled] { opacity: 0.5; cursor: progress; }

/* Renewal decision — Undecided / Renewing / Vacating (A1, Amit 2026-08-19).
   Replaces a single cycling chip that took two clicks to reach Vacating and
   committed 'renewing' to the server on the way past it. Pranay's design,
   00:48:39: "three boxes that are side by side just a little bit smaller…
   you still have the confirmation for each. Once you click each, it's not
   like a cycle."

   Same segmented family as .period-btn above, and it obeys that block's rule
   the hard way: state lives ONLY in .is-active, never in a call-site ternary.
   app.js used to paint this control with exactly the inline
   `background: on ? X : transparent` pattern the .period-btn note calls the
   worst of the three — the one that made the leasing period buttons
   unreadable. The per-state colour is a modifier class here, so the colour
   and the state cannot disagree.

   Colours use the --*-ink text variants, not the fills LEASE_INTENT_META
   carries for borders: the two diverge in palettes 3-7 (e.g. --positive
   #4A6F4F vs --positive-ink #436447) and these labels are text.

   "A little bit smaller": tighter than .period-btn's var(--sp-6) var(--sp-12)
   so that three cells land near the width of the one chip they replace,
   ~18px tall against the old chip's ~19px. That is deliberately at the small
   end — but still well clear of the ~13px target the .btn-sm note above flags
   as the real failure, and unlike the old chip a mis-click here costs one
   declined confirmation instead of a wrong write. */
.lease-intent-group {
  display: inline-flex;
  flex: 0 0 auto;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  overflow: hidden;  /* clip the children's square corners to the group radius */
}
.lease-intent {
  appearance: none;
  border: 0;
  border-left: 1px solid var(--rule-strong);
  background: transparent;
  color: var(--ink-faint);
  font: inherit;
  font-size: var(--t-micro);
  line-height: 1.15;
  padding: var(--sp-2) var(--sp-6);
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.lease-intent:first-child { border-left: 0; }
.lease-intent:hover { color: var(--ink); background: var(--surface); }
/* The selected cell keeps the state's own colour — green really does mean
   renewing here — so this is a tint + weight change, not the ink/canvas
   inversion .period-btn uses for a neutral period filter. */
.lease-intent.is-active { background: var(--surface); font-weight: 600; }
.lease-intent.is-active.is-undecided { color: var(--ink); }
.lease-intent.is-active.is-renewing  { color: var(--positive-ink); }
.lease-intent.is-active.is-vacating  { color: var(--negative-ink); }
/* Already-selected cell: clicking it is a no-op in _setLeaseIntent (prev ===
   next returns early), so it should not offer a pointer. */
.lease-intent.is-active { cursor: default; }
.lease-intent:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Editable unit notes. The affordance only appears on hover — these cells are
   read far more often than they are edited, and a permanent underline on every
   note turns the column into a wall of dotted lines. */
.unit-note-body:hover { border-bottom-color: var(--rule-strong) !important; }


/* ============================================================
   Phone pass, 2026-08-17 — reported from an iPhone by Pranay:
   "items not formatted correctly or cutting off".

   Everything here was found by rendering at 393x852 and 852x393 rather than by
   reading the CSS; scripts/mobile_screenshots.py captures both and fails loudly
   on page-level horizontal overflow.

   Landscape is its own case and not a smaller desktop: a phone on its side is
   852px WIDE and 393px TALL, so every rule keyed on max-width alone skips
   straight past the phone rules into the desktop ones. The region rollup below
   is exactly that bug.
   ============================================================ */

/* The six-column region rollup never had a mobile rule at ANY width. At 393px
   each column got ~65px, of which 36px was padding — so "Open WOs / 15 opened
   · 24 closed this wk · oldest 257d" wrapped to five lines and "0 units under"
   clipped to "der". Three across when a phone is on its side, two when it is
   upright. */
@media (max-width: 1100px) {
  .prop-card-minis { grid-template-columns: repeat(3, 1fr); }
  .prop-card-minis .mini {
    padding: var(--sp-10) var(--sp-12) var(--sp-10);
    border-bottom: 1px solid var(--rule);
  }
  /* The last cell of each ROW must lose its divider, not merely the last cell
     of the grid — otherwise a wrapped layout draws a rule against the card
     edge. */
  .prop-card-minis .mini:nth-child(3n) { border-right: none; }
  .prop-card-minis .mini:nth-last-child(-n+3) { border-bottom: none; }
}

@media (max-width: 640px) {
  .prop-card-minis { grid-template-columns: repeat(2, 1fr); }
  .prop-card-minis .mini:nth-child(3n) { border-right: 1px solid var(--rule); }
  .prop-card-minis .mini:nth-child(2n) { border-right: none; }
  .prop-card-minis .mini:nth-last-child(-n+3) { border-bottom: 1px solid var(--rule); }
  .prop-card-minis .mini:nth-last-child(-n+2) { border-bottom: none; }
  .prop-card-minis .v { font-size: var(--t-body); }

  /* The head is three flex children with no wrap, so a serif 17px region name
     sat ON TOP of "26 units · 2 properties · Kent + Ravenna". */
  .prop-card-head {
    flex-wrap: wrap;
    align-items: flex-start;
    gap: var(--sp-2) var(--sp-10);
    padding: var(--sp-10) var(--sp-12) var(--sp-8);
  }
  .prop-card-head .name { flex: 1 0 100%; }

  /* Label in a fixed 60px column + wrapping options = a ragged two-column
     block with the label floating beside the middle of it. Stack instead: the
     options get the full width and read as one row of choices. */
  .trends-filter-row,
  .trends-filter-row.is-multi { flex-direction: column; align-items: stretch; gap: var(--sp-4); }
  .trends-filter-label,
  .trends-filter-row.is-multi .trends-filter-label { width: auto; padding-top: 0; }
  .trends-tabs { gap: 2px 4px; }
  .trends-tabs button { padding-left: 9px; padding-right: 9px; }
}

/* Three flex columns of 10px legend text on a 393px screen gave three ~100px
   ribbons of broken words ("balance vs / monthly billed / red >25%"). It is
   footnote text — it should read as prose, one item per line. */
@media (max-width: 640px) {
  .foot { flex-direction: column; gap: var(--sp-8); line-height: 1.6; }
}

/* The floating "Report a bug" button sits over the last few lines of every
   page — the trends caption and the dashboard footnotes both disappeared under
   it. Give every page room to scroll past it, plus the home-indicator inset. */
@media (max-width: 1100px) {
  .shell { padding-bottom: calc(76px + env(safe-area-inset-bottom, 0px)); }
}

/* iOS draws the status bar and the notch OVER the page once viewport-fit=cover
   is set, and it is set now so the palette reaches the edges instead of leaving
   white bars. Without these insets the sync line and the sub-tab bar sat under
   the clock — visible in both of Pranay's portrait screenshots. */
@media (max-width: 1100px) {
  body.has-rail-wide .shell { padding-top: calc(54px + env(safe-area-inset-top, 0px)); }
  .nav-toggle { top: calc(9px + env(safe-area-inset-top, 0px)); }
  .rail-wide { padding-top: env(safe-area-inset-top, 0px); }
  .shell { padding-left: max(var(--gutter, 16px), env(safe-area-inset-left, 0px));
           padding-right: max(var(--gutter, 16px), env(safe-area-inset-right, 0px)); }
}

/* A13 — the sticky tenant chip (Pranay 2026-08-19, 00:48:39: "Desmond Taylor on
   all tabs would be sticky"). Injected into the rail by app.js rather than added
   to markup, because the rail lives in all 17 page files.

   Sits directly under .brand, above the first .nav-group, and borrows the
   brand's border-bottom rhythm so it reads as part of the masthead — i.e. as
   CONTEXT for what follows, not as another destination. That distinction is the
   whole reason it is not styled as a .nav-item.

   Two children, deliberately: a wide button that opens the panel and a narrow
   one that clears. Opening is the common act and gets the whole width; clearing
   is rarer and destructive-ish, so it is separated rather than being a second
   meaning for the same click — the mistake the lease-intent chip made. */
.tenant-ctx {
  display: flex;
  align-items: stretch;
  border-bottom: 1px solid var(--rule);
  background: var(--canvas);
}
/* One class for three tag names, because the control changes shape with the
   page: a <button> where #panel-tenant exists (property.html only), an <a> to
   that page from anywhere else, and an inert <span> when there is no property
   recorded to link to. They must be indistinguishable — the user is clicking
   the same thing either way. Hence the explicit display/text-decoration resets,
   which a button does not need but an anchor does. */
.tenant-ctx-open {
  flex: 1;
  min-width: 0;                 /* or a long tenant name pushes × out of the rail */
  display: block;
  text-align: left;
  text-decoration: none;
  padding: var(--sp-8) var(--sp-10) var(--sp-8) var(--sp-20);
  background: none;
  border: none;
  border-left: 2px solid var(--accent);
  font-family: var(--font-sans);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out);
}
.tenant-ctx-open:hover { background: var(--surface); }
.tenant-ctx-name {
  display: block;
  font-size: var(--t-ui);
  color: var(--ink);
  font-weight: 500;
  /* Names are user data and can be long; the rail is 220px and does not grow. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The property is always named here, so a tenant from somewhere other than the
   page you are on reads as legible rather than as a bug. That is also why the
   chip does NOT clear itself when the property changes. */
.tenant-ctx-where {
  display: block;
  font-size: var(--t-micro);
  color: var(--ink-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tenant-ctx-clear {
  flex: 0 0 auto;
  padding: 0 var(--sp-12);
  background: none;
  border: none;
  border-left: 1px solid var(--rule);
  color: var(--ink-faint);
  font-size: var(--t-body);
  line-height: 1;
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out);
}
.tenant-ctx-clear:hover { color: var(--negative-ink); background: var(--surface); }

/* A12 — the "Evicting" marker on a pre-vacant row. Says why the row is listed
   when the reason is neither an RM move-out date nor a PM Vacating flag. Muted
   rather than alarming: an eviction in progress is information the PM already
   knows and asked to SEE, not a new problem being reported at them. */
.pv-evicting {
  display: inline-block;
  padding: 0 var(--sp-4);
  border: 1px solid var(--negative);
  color: var(--negative-ink);
  font-size: var(--t-micro);
  white-space: nowrap;
  vertical-align: middle;
}
/* The fourth state in the shared .lease-intent group. Colour comes from the
   same negative token as Vacating — both mean "this unit is going to empty" —
   but the icon differs so the two are not read as one thing at a glance. */
.lease-intent.is-evicting.is-active {
  border-color: var(--negative);
  color: var(--negative-ink);
  font-weight: 500;
}

/* A19 — property grouping on the standup. Amit, 00:16:38: "I would just do it by
   properties… and then I just click on Ravenna, because I now have to find out
   which one is mine." The heading is a signpost inside a list he scans top to
   bottom, so it is quiet: no box, no fill, just a rule and a name. Anything
   heavier competes with the stalled-row colour that is the actual signal. */
.su-group { margin-bottom: var(--sp-20); }
.su-group-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-10);
  padding: 0 0 var(--sp-6);
  margin-bottom: var(--sp-8);
  border-bottom: 1px solid var(--rule-strong);
  font-family: var(--font-serif);
  font-size: var(--t-head);
  color: var(--ink);
}
.su-group-meta {
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  color: var(--ink-faint);
  white-space: nowrap;
}

/* C2 — the ledger window selector, sitting inline in the section label. Reuses
   .period-btn so it behaves and reads exactly like the This Week and Trends
   window controls; only the layout is local. */
.ledger-window { display: inline-flex; gap: 4px; flex: 0 0 auto; }
.ledger-window .period-btn { padding: 1px 6px; font-size: var(--t-micro); }
/* -- B5 · collapsible Insights sections (2026-08-20) -----------------------
   John, via Pranay: "you click on the 'by manager' and then all the information
   comes … the table is quite busy."

   The section NAME is the control, because that is literally what he described
   clicking. So the button has to disappear into the heading it replaced: no
   chrome, no button font, no button colour — it inherits the h2 (serif,
   --t-head, weight 400) and adds only a caret and a hover/focus state.

   Not a <details>/<summary>: see the note above wireInsightsFolds in app.js —
   <summary> must be the first child, which would drag every .section-head
   inside the fold and break how retireSampleChip finds a section's sample
   chip. The button carries aria-expanded/aria-controls instead, so screen
   readers get the same state and Enter/Space work for free. */
.fold-toggle {
  /* `font: inherit` alone would also inherit the UA button's own line-height
     quirk on some engines; the three properties that matter are set outright. */
  font-family: inherit;
  font-size: inherit;
  font-weight: inherit;
  line-height: inherit;
  color: var(--ink);
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-8);
  cursor: pointer;
  text-align: left;
  transition: color var(--dur-fast) var(--ease-out);
}
.fold-toggle:hover { color: var(--accent-ink); }
/* :focus-visible only — a mouse click on a heading should not leave a ring
   parked on it, but a Tab user has to be able to see where they are. */
.fold-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}
/* A border triangle rather than a glyph: no dependency on a font shipping the
   character, and it stays crisp at 6px where a ▸ does not. Points right when
   shut, down when open — the direction convention the rest of the app's
   disclosure affordances use. */
.fold-caret {
  flex: 0 0 auto;
  width: 0;
  height: 0;
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
  border-left: 7px solid currentColor;
  /* Optical centring against a serif cap-height, not the line box. */
  margin-top: 1px;
  transform-origin: 30% 50%;
  transition: transform var(--dur-base) var(--ease-out);
}
.fold-toggle[aria-expanded="true"] .fold-caret { transform: rotate(90deg); }
/* The fold body. `hidden` is only the UA stylesheet's display:none, which any
   author display rule beats — and one of these three bodies carries an inline
   style — so state it here where it cannot be lost. */
.insights-fold-body[hidden] { display: none; }
/* Reads as belonging to the heading above it once the caret has been clicked,
   rather than as a table that happens to follow. */
.insights-fold-body { padding-top: var(--sp-4); }

/* B2 — the "photos in Rent Manager" link on a work-order panel. Styled as a
   clear outbound link rather than a primary button: it leaves the app, and
   overselling it would imply we have the photos when RM's API does not expose
   them at all. */
.wo-photos-link {
  display: inline-block;
  font-size: var(--t-ui);
  color: var(--accent-ink);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.wo-photos-link:hover { color: var(--ink); }
