/* ══════════════════════════════════════════════════════════════════════
   STUDENT LEASE-UP PLAN  (.fr-leaseup-*)
   ----------------------------------------------------------------------
   Frontier reskin of the student Lease-Up Plan page (Renewals → Pre-Lease
   Tiers → GPR Forecast). Per-unit-type cards + editable tables reuse the
   fr-renewal-* vocabulary; this block adds the plan-level chrome: the
   sticky forecast band, the renewals scoreboard, the tier cells, and the
   GPR forecast table. Scoped under .fr-leaseup-page where it touches
   shared form primitives.
   ══════════════════════════════════════════════════════════════════════ */

.fr-leaseup-page {
    font-family: var(--fr-font-sans);
    color: var(--fr-ink);
    /* Breathing room at the page foot so the floating comment bubble
       (bottom-right) sits clear of the final GPR forecast total row.
       !important is required to beat Bootstrap's `.p-3` (padding: 1rem
       !important) on the same element. */
    padding-bottom: 140px !important;
}
/* Provenance, two lines (§5.4). Line 1 is freshness and aligns with the pull
   buttons beside it; line 2 states the bed universe. It replaced one
   ~200-character mono run-on that mixed the two topics. */
.fr-leaseup-snapshot-info {
    font-family: var(--fr-font-mono);
    font-size: 11px;
    letter-spacing: 0.02em;
    color: var(--fr-ink-3);
    margin: 0 2px 14px;
    min-width: 0;
}
.fr-leaseup-prov-line { line-height: 1.65; }
.fr-leaseup-prov-line--universe { color: var(--fr-ink-2); }

/* Snapshot line + in-app Entrata pull buttons share one header row */
.fr-leaseup-refresh-row {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
}
.fr-leaseup-refresh-actions {
    display: flex;
    gap: 8px;
    flex-shrink: 0;
}
.fr-leaseup-refresh-actions .fr-leaseup-ghost-btn:disabled {
    opacity: 0.55;
    cursor: wait;
}

/* "What changed" disclosure banner (pull-over-pull diff + orphaned overrides) */
.fr-leaseup-diff-banner {
    background: #FFFBEB;
    border: 1px solid #FDE68A;
    border-radius: var(--fr-r-md);
    color: var(--fr-ink-2);
    font-family: var(--fr-font-mono);
    font-size: 11.5px;
    letter-spacing: 0.01em;
    line-height: 1.7;
    padding: 10px 36px 10px 14px;
    margin: 0 0 14px;
}
.fr-leaseup-diff-banner .btn-close {
    padding: 12px;
    font-size: 10px;
}
.fr-leaseup-diff-lead {
    color: #92400E;
    font-weight: 600;
}
.fr-leaseup-diff-lead--warn {
    color: #B91C1C;
}
.fr-leaseup-diff-note {
    color: var(--fr-ink-3);
    font-style: italic;
}

/* Shared form primitives (inputs, addons) */
.fr-leaseup-page .form-control {
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 13px;
    color: var(--fr-ink);
    background: var(--fr-bg);
    border: 1px solid var(--fr-hairline-strong);
    border-radius: var(--fr-r-sm);
    box-shadow: none;
}
.fr-leaseup-page .form-control:focus {
    border-color: var(--fr-accent);
    background: var(--fr-surface);
    box-shadow: 0 0 0 3px rgba(180, 83, 9, 0.10);
}
.fr-leaseup-page .form-control::placeholder { color: var(--fr-ink-3); }
.fr-leaseup-page .input-group-text {
    background: var(--fr-bg);
    border: 1px solid var(--fr-hairline-strong);
    color: var(--fr-ink-3);
    font-family: var(--fr-font-sans);
    font-size: 12px;
    font-weight: 500;
}

/* Eyebrow (uppercase tracked label) */
.fr-leaseup-eyebrow {
    font-family: var(--fr-font-sans);
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--fr-ink-3);
}
.fr-leaseup-eyebrow--accent { color: var(--fr-accent); display: block; margin-bottom: 8px; }

/* ══════════════════════════════════════════════════════════════════════
   STICKY PLAN-CHAIN STRIP  (visual-design §5)
   ----------------------------------------------------------------------
   Replaces ~170px of permanently sticky chrome — a hero band, a three-stat
   breakdown, and a JUMP TO link row — with ~100px that carries strictly
   more. The old band's fatal flaw was saying the same thing on every
   screen, which turns it into wallpaper; the chain instead states how its
   numbers relate (renewals + pre-lease + amenity = forecast) and marks
   which step you are currently in.

   No shrink-on-scroll state (§5.5): at this height a progressive collapse
   would add motion complexity and a second layout to maintain for no
   legibility gain.
   ══════════════════════════════════════════════════════════════════════ */
.fr-leaseup-sticky {
    position: sticky;
    top: 0;
    z-index: 20;
    background: var(--fr-surface);
    border: 1px solid var(--fr-hairline);
    border-radius: var(--fr-r-lg);
    box-shadow: 0 1px 2px rgba(20, 24, 29, 0.05);
    padding: 9px 20px 8px;
    margin-bottom: 20px;
}

/* Row 1 — plan year · forecast hero · growth · commit action, on one line.
   `center`, not `baseline`: the button is the tallest thing in the row, and
   baseline-aligning a 38px control against a 27px numeral leaves the line
   visibly crooked. The three text elements share a baseline inside
   .fr-leaseup-strip-figures instead. */
.fr-leaseup-strip-head {
    display: flex;
    align-items: center;
    gap: 18px;
    flex-wrap: wrap;
}
.fr-leaseup-strip-figures {
    display: flex;
    align-items: baseline;
    gap: 16px;
    flex-wrap: wrap;
    min-width: 0;
}
/* Shorter than the global 38px action button: this strip is permanent chrome
   above every screen, so the tallest element in it sets the page's fixed cost. */
.fr-leaseup-strip-head .fr-action-btn { height: 32px; }
.fr-leaseup-strip-plan {
    font-family: var(--fr-font-sans);
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--fr-ink-3);
    white-space: nowrap;
}
.fr-leaseup-strip-hero { display: flex; align-items: baseline; gap: 5px; }
/* Display serif is deliberate — the forecast total is one of the editorial
   single figures §10.1 assigns to the serif, not the mono. Tabular figures
   still matter: this number rewrites itself on every edit, and proportional
   digits make it change WIDTH as it does, which reads as the layout twitching. */
.fr-leaseup-hero-value {
    font-family: var(--fr-font-serif);
    font-variant-numeric: tabular-nums;
    font-size: 27px;
    font-weight: 400;
    line-height: 1.1;
    letter-spacing: -0.01em;
    color: var(--fr-ink);
}
.fr-leaseup-hero-unit {
    font-family: var(--fr-font-sans);
    font-size: 11.5px;
    color: var(--fr-ink-3);
}
/* Sans, not mono — this is a sentence ("+2.7% vs $509,163 today"), not a column
   entry. Tabular figures keep the two numbers inside it from shifting the
   sentence around as they update. */
.fr-leaseup-hero-growth {
    font-family: var(--fr-font-sans);
    font-variant-numeric: tabular-nums;
    font-size: 13px;
    font-weight: 600;
}
.fr-leaseup-hero-growth.is-up   { color: var(--fr-positive); }
.fr-leaseup-hero-growth.is-down { color: var(--fr-negative); }
.fr-leaseup-hero-growth.is-flat { color: var(--fr-ink-3); }

/* Save status sits INLINE to the left of the button, not stacked under it —
   the head row has to stay one line tall. */
.fr-leaseup-actions {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 12px;
}
.fr-leaseup-save-status { text-align: right; }
.fr-leaseup-save-msg { font-family: var(--fr-font-mono); font-size: 11px; }
.fr-leaseup-save-msg.is-ok     { color: var(--fr-positive-deep); }
.fr-leaseup-save-msg.is-warn   { color: var(--fr-warning-deep); }
.fr-leaseup-save-msg.is-danger { color: var(--fr-negative-deep); }
/* Unsaved changes (§9). A stated fact sitting beside the button that resolves
   it — not an alarm: on a page where the whole job is editing, this is the
   NORMAL mid-session state and a red badge would be lit for most of an
   afternoon. The dot is what makes it register at a glance; the amber is
   §10.2's "deliberate human state", the same weight the override dot carries. */
.fr-leaseup-save-msg.is-dirty {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-family: var(--fr-font-sans);
    font-size: 11px;
    font-weight: 600;
    color: var(--fr-warning-deep);
}
.fr-leaseup-save-msg.is-dirty::before {
    content: "";
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--fr-warning-deep);
}
/* The class `assets/student_leaseup_dirty.js` reads to decide whether to
   challenge a navigation. It lives on the STATUS WRAPPER, not the message
   inside it, so the JS has one stable selector regardless of what is being
   said. Renaming either half silently disables warn-on-navigate — a test pins
   the pairing. */
.fr-leaseup-save-status.is-dirty { text-align: right; }

/* Row 2 — the chain. Also the page's anchor nav: every step is clickable and
   no step blocks access to another (§5.1). */
.fr-leaseup-chain {
    display: flex;
    align-items: stretch;
    gap: 12px;
    margin-top: 7px;
    padding-top: 7px;
    border-top: 1px solid var(--fr-hairline);
}
.fr-leaseup-chain:empty { display: none; }

/* ① and ② share ONE flexible region, and only they are proportionally sized.
   The amenity term and step ③ take the width they need first; this region
   absorbs whatever is left. Letting all four blocks compete on share pushed
   step ③ — the number the page exists to produce — into ~110px and squeezed the
   amenity term into a stub. The ①:② ratio, which is where the proportional read
   earns anything, is still exact. */
.fr-leaseup-chain-main {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: stretch;
    gap: 8px;
}

.fr-leaseup-chain-step {
    /* flex-grow is set inline from the term's share of forecast GPR; flex-basis 0
       makes grow the sole size driver. */
    flex: 0 1 0;
    min-width: 96px;
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 2px 7px 3px;
    border-radius: var(--fr-r-sm);
    text-decoration: none;
    color: inherit;
}
/* The active/hover fills live with the hues further down, since each step's
   resting fill is its own tint and the active state has to deepen THAT rather
   than replace it with a neutral. Weight and fill only, never motion — the
   active state is identical under prefers-reduced-motion; only the fade between
   states is suppressed. */
@media (prefers-reduced-motion: no-preference) {
    .fr-leaseup-chain-step { transition: background 140ms ease; }
}
.fr-leaseup-chain-step:focus-visible {
    outline: 2px solid var(--fr-accent);
    outline-offset: 1px;
}

/* Step ③ is a fixed-width terminal block: it is the sum, not a share (§5.1).
   Given a real reserved width rather than min-width:0 — as the sum of everything
   left of it, it should read as the chain's destination, and at whatever width
   the proportional blocks happened to leave over it read as an afterthought. */
.fr-leaseup-chain-step.is-forecast {
    flex: 0 0 auto;
    min-width: 178px;
    justify-content: flex-start;
    padding-left: 15px;
    border-left: 1px solid var(--fr-hairline);
    border-radius: 0 var(--fr-r-sm) var(--fr-r-sm) 0;
}

/* `center`, not `baseline`: the step marker is a box (a drawn circle), and a box
   has no text baseline to sit on. */
.fr-leaseup-chain-head {
    display: flex;
    align-items: center;
    gap: 7px;
    min-width: 0;
    line-height: 1.3;
    /* Matches the marker's height, so the money line lands at the same offset in
       every block — including the amenity term, which has no marker. Without
       this its figures sit ~5px above the rest of the chain. */
    min-height: 18px;
}

/* The step marker: a CSS-drawn ring around a REAL digit.
   ----------------------------------------------------------------------
   This was `①` / `②` / `③` as Unicode glyphs and the user could not read them
   without zooming to 150% (2026-07-28). Two compounding reasons: the enclosed-
   alphanumeric glyph spends ~85% of its em box on the ring, so a 12px `①` puts
   a ~6px digit on screen; and JetBrains Mono has no such glyph, so it fell back
   to whatever system font did, at whatever metrics that font chose.

   Drawing the ring in CSS puts the digit at its full 11.5px, roughly doubling
   the legible size, and makes it crisp at any zoom. §5.2's numbered markers earn
   their place on this page, so the number stays — it is only the ring that was
   never load-bearing. */
.fr-leaseup-chain-mark {
    flex: 0 0 auto;
    box-sizing: border-box;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    border: 1.5px solid var(--fr-ink-3);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 11.5px;
    font-weight: 700;
    line-height: 1;
    color: var(--fr-ink-2);
}
.fr-leaseup-chain-name {
    font-family: var(--fr-font-sans);
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.13em;
    text-transform: uppercase;
    color: var(--fr-ink-2);
    white-space: nowrap;
}
.fr-leaseup-chain-step.is-active .fr-leaseup-chain-name { color: var(--fr-ink); }

/* The marker carries the step's own hue, so it doubles as the block's identity
   colour — and filling it on the active step gives §5.1's "weight and fill"
   highlight a second, much stronger cue than a background tint alone. */
.fr-leaseup-chain-step.is-renewals .fr-leaseup-chain-mark {
    border-color: var(--fr-renewals);
    color: var(--fr-renewals);
}
.fr-leaseup-chain-step.is-prelease .fr-leaseup-chain-mark {
    border-color: var(--fr-prelease);
    color: var(--fr-prelease);
}
.fr-leaseup-chain-step.is-renewals.is-active .fr-leaseup-chain-mark {
    background: var(--fr-renewals);
    color: var(--fr-surface);
}
.fr-leaseup-chain-step.is-prelease.is-active .fr-leaseup-chain-mark {
    background: var(--fr-prelease);
    color: var(--fr-surface);
}
/* Step ③ has no hue — it is the sum of the others, not one of them — so its
   marker takes the ink, which still reads as the chain's strongest term. */
.fr-leaseup-chain-step.is-forecast.is-active .fr-leaseup-chain-mark {
    background: var(--fr-ink-2);
    border-color: var(--fr-ink-2);
    color: var(--fr-surface);
}

/* Per-step completeness (§5.1). The under-allocated case is the page's one
   silent-and-downward failure, so it gets the warning colour reserved by
   §10.2 — an unallocated bed contributes $0 and nothing else says so. */
.fr-leaseup-chain-note {
    font-family: var(--fr-font-mono);
    font-size: 10px;
    letter-spacing: 0.01em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.fr-leaseup-chain-note.is-ok   { color: var(--fr-ink-3); }
.fr-leaseup-chain-note.is-warn { color: var(--fr-negative); font-weight: 600; }
/* Step ①'s `2 of 3 types reviewed` mid-walkthrough. Not a warning — an unfinished
   annual pass is the expected state for most of the season — but not done either,
   so it sits between `is-ok`'s quiet and `is-warn`'s red. */
.fr-leaseup-chain-note.is-pending { color: var(--fr-ink-2); font-weight: 600; }

.fr-leaseup-chain-figs {
    display: flex;
    align-items: baseline;
    gap: 6px;
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    line-height: 1.25;
}
.fr-leaseup-chain-beds { font-size: 12px; color: var(--fr-ink-2); }
.fr-leaseup-chain-sep  { color: var(--fr-hairline-strong); }
.fr-leaseup-chain-gpr  { font-size: 13.5px; font-weight: 600; color: var(--fr-ink); }
.fr-leaseup-chain-gpr.is-total { font-size: 15px; }

/* Bar and share on ONE line, as §5's ASCII draws it (`◄── 39.7% ──►`). The bar
   flexes to fill whatever the block's share-driven width leaves, so the
   segments still read as one bar running across the strip — and the strip pays
   for three lines instead of four on every screen of the page. */
.fr-leaseup-chain-rule {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-top: 1px;
}
.fr-leaseup-chain-bar {
    flex: 1 1 auto;
    min-width: 8px;
    height: 5px;
    border-radius: 999px;
}
.fr-leaseup-chain-step.is-renewals .fr-leaseup-chain-bar { background: var(--fr-renewals); }
.fr-leaseup-chain-step.is-prelease .fr-leaseup-chain-bar { background: var(--fr-prelease); }

/* At rest the BAR carries the hue and the block carries no fill. Tinting every
   block at rest would leave the active state nothing to say — and this strip is
   permanent chrome, so two full-width washes above the first input is exactly
   the visual weight §1 set out to remove. Fill is the active affordance; each
   step deepens into its own hue rather than a neutral, so the highlight still
   reads as part of §10.2's one visual language. */
.fr-leaseup-chain-step.is-renewals.is-active,
.fr-leaseup-chain-step.is-renewals:hover { background: var(--fr-renewals-tint); }
.fr-leaseup-chain-step.is-prelease.is-active,
.fr-leaseup-chain-step.is-prelease:hover { background: var(--fr-prelease-tint); }
.fr-leaseup-chain-step.is-amenity:hover  { background: var(--fr-amenity-tint); }
/* Step ③ has no hue — it is the sum of the others, not one of them. */
.fr-leaseup-chain-step.is-forecast.is-active,
.fr-leaseup-chain-step.is-forecast:hover { background: var(--fr-bg); }

/* GPR share, not bed share — it labels what the bar width encodes (§5.1) */
.fr-leaseup-chain-share {
    flex: 0 0 auto;
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 9.5px;
    color: var(--fr-ink-3);
    letter-spacing: 0.02em;
    line-height: 1;
}

/* The amenity segment is unnumbered and carries NO BAR (§5.6). A bar is a
   proportion claim, and there is no width that is both honest and legible for a
   2–3% term: wide enough to hold "$12,660" overstates it ~4x, honest enough to
   encode 2.4% cannot hold its own label. §5.1a and §10.2 already place amenity
   outside the bar grammar, so the printed share carries it and the hue moves to
   the `+` operator. At $0 the segment is not rendered at all, so there is no
   empty state to style. */
.fr-leaseup-chain-step.is-amenity {
    flex: 0 0 auto;
    min-width: 0;
}
.fr-leaseup-chain-plus {
    font-family: var(--fr-font-mono);
    font-size: 13px;
    font-weight: 700;
    line-height: 1;
    color: var(--fr-amenity);
}
.fr-leaseup-chain-step.is-amenity .fr-leaseup-chain-name { color: var(--fr-ink-3); }
.fr-leaseup-chain-step.is-amenity .fr-leaseup-chain-gpr { font-size: 12.5px; font-weight: 500; }
/* The share reads as part of the figures line here, not as a bar caption. */
.fr-leaseup-chain-step.is-amenity .fr-leaseup-chain-share { font-size: 11px; }

/* The arrows carry the dependency: step 2 cannot be done before step 1 (§5.2).
   Aligned to the step-name line, as drawn. */
.fr-leaseup-chain-arrow {
    flex: 0 0 auto;
    align-self: flex-start;
    padding-top: 3px;
    font-size: 12px;
    line-height: 1.3;
    color: var(--fr-hairline-strong);
}

/* Tablet: the chain is the first thing to stack, since a 4-block flex row of
   96px minimums stops being proportional long before it stops fitting. */
@media (max-width: 1100px) {
    .fr-leaseup-chain { flex-wrap: wrap; row-gap: 6px; }
    .fr-leaseup-chain-main { flex-basis: 100%; }
    .fr-leaseup-chain-arrow { display: none; }
    .fr-leaseup-chain-step.is-forecast {
        border-left: 0;
        padding-left: 7px;
        min-width: 140px;
    }
}

/* Section header (eyebrow + serif title) */
.fr-leaseup-section-header { margin-top: 32px; }
.fr-leaseup-section-header .fr-section-title { margin-bottom: 4px; }

/* Controls bar actions + ghost button */
.fr-leaseup-controls-actions { margin-left: auto; display: flex; align-items: center; gap: 10px; }
.fr-leaseup-ghost-btn {
    height: 34px;
    padding: 0 16px;
    background: var(--fr-surface);
    border: 1px solid var(--fr-hairline-strong);
    border-radius: var(--fr-r-md);
    font-family: var(--fr-font-sans);
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--fr-ink-2);
    cursor: pointer;
    transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
}
.fr-leaseup-ghost-btn:hover { background: #FBF9F4; color: var(--fr-ink); border-color: #C9C5B8; }

/* Tertiary control — a text action, not a button. For destructive-but-rare
   things that were sitting at peer weight beside a constructive primary (§6.3):
   "Reset to pulled prices" throws away the user's website prices, and a
   button-shaped control invites the click it should discourage. */
.fr-leaseup-text-btn {
    background: none;
    border: 0;
    padding: 0 2px;
    font-family: var(--fr-font-sans);
    font-size: 12px;
    color: var(--fr-ink-3);
    text-decoration: underline;
    text-decoration-color: var(--fr-hairline-strong);
    text-underline-offset: 3px;
    cursor: pointer;
}
.fr-leaseup-text-btn:hover { color: var(--fr-negative); text-decoration-color: var(--fr-negative); }
.fr-leaseup-text-btn:focus-visible { outline: 2px solid var(--fr-accent); outline-offset: 2px; }

/* §6.3's sentence: the highest-value copy on the page. It sits under the
   strategy toggle it corrects, and it is the one piece of prose on this page
   that survived invariant 3 — because no column header can say "the control you
   are looking at cannot move these beds". */
.fr-leaseup-strategy-note:empty { display: none; }
.fr-leaseup-strategy-note {
    margin-top: 10px;
    padding-top: 9px;
    border-top: 1px solid var(--fr-hairline);
}
.fr-leaseup-strategy-note-text {
    font-family: var(--fr-font-sans);
    font-variant-numeric: tabular-nums;
    font-size: 12.5px;
    line-height: 1.5;
}
/* Loud once the toggle is doing almost nothing, quiet while it still drives
   most of the property. Not red — this is the normal state of most properties
   (§11.8), not an error. */
.fr-leaseup-strategy-note-text.is-dominant { color: var(--fr-warning-deep); font-weight: 500; }
.fr-leaseup-strategy-note-text.is-minor    { color: var(--fr-ink-3); }
.fr-leaseup-ghost-btn:focus, .fr-leaseup-ghost-btn:active {
    outline: none;
    box-shadow: 0 0 0 3px rgba(180, 83, 9, 0.12);
}

/* Renewals portfolio scoreboard */
.fr-leaseup-scoreboard {
    display: flex;
    flex-wrap: wrap;
    background: var(--fr-surface);
    border: 1px solid var(--fr-hairline);
    border-radius: var(--fr-r-lg);
    overflow: hidden;
    margin-bottom: 18px;
    box-shadow: 0 1px 0 rgba(20, 24, 29, 0.02);
}
.fr-leaseup-score {
    flex: 1 1 180px;
    padding: 18px 22px 16px;
    border-right: 1px solid var(--fr-hairline);
}
.fr-leaseup-score:last-child { border-right: 0; }
.fr-leaseup-score-label {
    font-family: var(--fr-font-sans);
    font-size: 9.5px;
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--fr-ink-3);
    margin-bottom: 8px;
}
/* These four tiles become the §6.7 equation, which §10.1 also assigns to the
   serif — so serif here is not an inconsistency with the mono rule. Tabular
   figures for the same reason as the hero: they all rewrite live. */
.fr-leaseup-score-value {
    font-family: var(--fr-font-serif);
    font-variant-numeric: tabular-nums;
    font-size: 30px;
    font-weight: 400;
    line-height: 1;
    letter-spacing: -0.01em;
    color: var(--fr-ink);
    font-variant-numeric: tabular-nums;
}
.fr-leaseup-score-value--pos  { color: var(--fr-positive); }
.fr-leaseup-score-value--neg  { color: var(--fr-negative); }
.fr-leaseup-score-value--gold { color: var(--fr-gold); }

/* ══════════════════════════════════════════════════════════════════════
   THE CHECKLIST  (.fr-leaseup-index* / .fr-leaseup-card* / .fr-leaseup-check)
   ----------------------------------------------------------------------
   visual-design §6.1 (collapsed card) / §6.6 (walkthrough support)

   The renewals section is a checklist, not a table: land with everything
   collapsed, open one type, price every row, mark it reviewed, collapse, open
   the next. Collapsing is only defensible BECAUSE of the mark-reviewed step
   beside it — hiding rows with no record of what has been eyeballed would hide
   unreviewed work, which an annual deliberate pass cannot tolerate.

   The card HEAD is the collapsed card. One piece of markup serves both states
   and collapsing only hides the body, which is what keeps every row input
   mounted: re-rendering to collapse would destroy focus mid-edit (§6.8).
   ══════════════════════════════════════════════════════════════════════ */

/* The card index + Expand/Collapse all, directly above the stack. */
.fr-leaseup-index-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 12px;
    margin: 4px 0 12px;
}
.fr-leaseup-index { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
.fr-leaseup-index-actions { display: flex; align-items: center; gap: 16px; }

/* A jump chip per unit type, each picking up a check as it is finished (§6.6). */
.fr-leaseup-chip {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 3px 10px;
    border: 1px solid var(--fr-hairline-strong);
    border-radius: 999px;
    background: var(--fr-surface);
    font-family: var(--fr-font-mono);
    font-size: 11.5px;
    font-weight: 600;
    color: var(--fr-ink-2);
    text-decoration: none;
    transition: border-color 120ms ease, color 120ms ease, background 120ms ease;
}
.fr-leaseup-chip:hover { border-color: var(--fr-accent); color: var(--fr-ink); text-decoration: none; }
.fr-leaseup-chip:focus-visible { outline: 2px solid var(--fr-accent); outline-offset: 2px; }
/* The tick is CSS, keyed off the same class the card is painted with, so a chip
   cannot report a different review state from the card it points at. */
.fr-leaseup-chip.is-reviewed::before { content: "✓"; color: var(--fr-positive); font-weight: 700; }
.fr-leaseup-chip.is-reviewed { color: var(--fr-ink-3); }
.fr-leaseup-chip.is-open { border-color: var(--fr-accent); background: var(--fr-accent-soft); color: var(--fr-ink); }

/* Neutral sibling of .fr-leaseup-text-btn, which reddens on hover because it is
   the destructive `Reset to pulled prices`. Expand/Collapse all destroy nothing. */
.fr-leaseup-link-btn {
    border: 0;
    background: none;
    padding: 0;
    font-family: var(--fr-font-sans);
    font-size: 11.5px;
    font-weight: 600;
    color: var(--fr-ink-3);
    text-decoration: underline;
    text-decoration-color: var(--fr-hairline-strong);
    text-underline-offset: 3px;
    cursor: pointer;
}
.fr-leaseup-link-btn:hover { color: var(--fr-ink); text-decoration-color: var(--fr-accent); }
.fr-leaseup-link-btn:focus-visible { outline: 2px solid var(--fr-accent); outline-offset: 2px; }

/* Jump target. `scroll-margin-top` clears the sticky strip, or a chip would land
   the card head underneath the thing the reader is orienting by. */
.fr-leaseup-anchor { display: block; height: 0; scroll-margin-top: 140px; }

/* Sticky head, per §6.6: thirty rows into 4x2 the reviewer must still see which
   type they are in AND its website price, since that price is the ceiling driving
   every number on screen. `overflow: hidden` on the base card would kill sticky,
   so it is traded for a clipped radius on the head itself. */
.fr-leaseup-card { overflow: visible; }
.fr-leaseup-card-head {
    position: sticky;
    /* Clears .fr-leaseup-sticky, which pins at top:0 and measures ~120px (§5.6).
       If the strip's height changes, this and STRIP_CLEARANCE in
       assets/student_leaseup_chain.js both follow it. */
    top: 128px;
    z-index: 3;
    display: block !important;
    padding: 9px 16px 8px !important;
    border-top-right-radius: var(--fr-r-md);
    cursor: default;
}
.fr-leaseup-card-head:hover { background: var(--fr-bg); }

/* CLICK ANYWHERE ON THE HEADER TO EXPAND — matching the conventional New Leases
   rows (user, 2026-07-29). A transparent overlay, not `n_clicks` on the header,
   because the header contains the website-price input: a click on that box
   bubbles to any ancestor carrying n_clicks, so the card would collapse the
   moment the user reached for the one control §6.1 put up here.
   The overlay sits ABOVE the text and BELOW the controls, so clicks on text and
   empty space toggle while clicks on the tick / price box / caret reach those —
   the overlay is not their ancestor, so there is nothing to bubble. No JS, and no
   dependence on how React delegates events.
   `position: sticky` on the head already makes it the containing block, so no
   `position: relative` is needed (and adding one would break the sticky). */
.fr-leaseup-headhit {
    position: absolute;
    inset: 0;
    z-index: 1;
    width: 100%;
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
}
.fr-leaseup-headhit:focus { outline: none; }
/* The three controls that mean something other than "toggle me". */
.fr-leaseup-check,
.fr-leaseup-price-group,
.fr-leaseup-caret {
    position: relative;
    z-index: 2;
}
/* Collapsed is the LANDING state (§6.1), and hiding is all it does — the inputs
   below stay mounted so focus and every row callback survive. */
.fr-leaseup-card.is-collapsed > .fr-renewal-card-body { display: none; }
/* A finished type recedes: the accent rail that marks "work here" goes quiet. */
.fr-leaseup-card.is-reviewed { border-top-color: var(--fr-hairline-strong); border-left-color: var(--fr-positive); }
.fr-leaseup-card.is-reviewed .fr-leaseup-cardhead-type { color: var(--fr-ink-2); }

/* Row 1 — the scan line, as LANES.
   ----------------------------------------------------------------------
   A flex row was the first build and it ragged badly (user, 2026-07-29): every
   item's x-position depended on the width of whatever sat to its left, so a card
   reading `A1 (1x1) · 8 beds` put its website price ~90px left of one reading
   `D1 (4x4) · 280 beds`, and the eye had to travel right-to-left on every row.

   A grid fixes it, but NOT with `max-content` on the variable tracks: each card
   is its own element, so `max-content` resolves per card and reproduces exactly
   the ragged layout. The variable lanes therefore arrive as custom properties
   measured server-side over every card at once (`_head_lane_style`), and only the
   genuinely constant tracks stay `max-content` — the `WEBSITE` and `VS AVG`
   labels are fixed strings and the price input is a fixed-width control, so those
   are identical on every card by construction.

   The row sets the MONO face itself so every `ch` below is exact; the type label
   and the small-caps tags override the font on themselves. */
.fr-leaseup-cardhead-row {
    display: grid;
    grid-template-columns:
        20px                        /* tick box                        */
        var(--fr-lane-type, 120px)  /* unit type — measured, sans/bold  */
        var(--fr-lane-beds, 3ch)    /* bed numeral, right-aligned       */
        max-content                 /* the word `beds`                  */
        max-content                 /* `WEBSITE` label                  */
        max-content                 /* price input                      */
        max-content                 /* `VS AVG` label                   */
        6ch                         /* variance, right-aligned (-10.5%) */
        var(--fr-lane-hist, 54px)   /* histogram                        */
        1fr                         /* slack                            */
        var(--fr-lane-lift, 11ch)   /* lift, right-aligned              */
        26px;                       /* caret                            */
    align-items: center;
    gap: 12px;
    font-family: var(--fr-font-mono);
    font-size: 12px;
}
/* Placed from the END, so the two trailing lanes stay put if a track is ever
   added in the middle. Everything before them auto-places in DOM order, which
   leaves the `1fr` slack track empty — that is what pushes these right. */
.fr-leaseup-cardhead-lift { grid-column: -3 / -2; }
.fr-leaseup-caret { grid-column: -2 / -1; }

/* Below tablet width the twelve fixed lanes stop fitting, so the row falls back
   to wrapping. Alignment is a scanning aid for a wide screen; at 900px the
   priority is simply not breaking (§10.4). */
@media (max-width: 1100px) {
    .fr-leaseup-cardhead-row {
        display: flex;
        flex-wrap: wrap;
    }
    .fr-leaseup-cardhead-lift { margin-left: auto; }
}

/* Row 2 — the regime summary and the provenance tail. Indented to the type lane
   (the tick box plus one gap), so the summary starts under the unit type it
   describes. */
.fr-leaseup-cardhead-sub {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 12px;
    margin-top: 5px;
    padding-left: 32px;
}

/* The tick box, and the primary Mark reviewed control (§6.6). */
.fr-leaseup-check {
    flex: 0 0 auto;
    width: 20px;
    height: 20px;
    padding: 0;
    border: 1.5px solid var(--fr-hairline-strong);
    border-radius: 4px;
    background: var(--fr-surface);
    font-size: 13px;
    font-weight: 700;
    line-height: 1;
    color: transparent;
    cursor: pointer;
    transition: border-color 120ms ease, background 120ms ease;
}
.fr-leaseup-check:hover { border-color: var(--fr-positive); }
.fr-leaseup-check:focus-visible { outline: 2px solid var(--fr-accent); outline-offset: 2px; }
.fr-leaseup-check.is-reviewed { border-color: var(--fr-positive); background: var(--fr-positive); }
.fr-leaseup-check.is-reviewed::after { content: "✓"; color: #FFFFFF; }

/* The primary scan target. A plain span: the header overlay carries the click, so
   the label does not need to be a control to be a toggle target. */
.fr-leaseup-cardhead-type {
    font-family: var(--fr-font-sans);
    font-size: 15px;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--fr-ink);
    text-align: left;
}
/* The whole header is the hit area, so it is the whole header that responds. */
.fr-leaseup-card-head:hover .fr-leaseup-cardhead-type { color: var(--fr-accent-strong); }
.fr-leaseup-headhit:focus-visible ~ .fr-leaseup-cardhead-row .fr-leaseup-cardhead-type {
    outline: 2px solid var(--fr-accent);
    outline-offset: 2px;
}
/* The lane is measured from the longest label on the page, and the ellipsis is
   the safety net if that estimate ever comes in short — a clipped name is
   recoverable, a broken column is not. */
.fr-leaseup-cardhead-type {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* Right-aligned so `8` and `280` share a right edge and the digits stack. This is
   the ONE thing that makes the counts scannable down the page; the word `beds`
   gets its own left-aligned lane so it stacks too. Invariant 7's centring rule
   governs the editable per-unit-type grids (the renewal rows, the tier matrix) —
   this is a header scan line, and centring here would un-stack the digits. */
.fr-leaseup-cardhead-beds {
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 12px;
    color: var(--fr-ink-2);
    text-align: right;
}
.fr-leaseup-cardhead-bedsunit {
    font-family: var(--fr-font-mono);
    font-size: 12px;
    color: var(--fr-ink-3);
}
.fr-leaseup-cardhead-tag {
    font-family: var(--fr-font-sans);
    font-size: 9.5px;
    font-weight: 600;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--fr-ink-3);
    white-space: nowrap;
}
/* Right-aligned in its lane for the same reason as the bed counts: `+3.0%` and
   `-1.5%` line up on the decimal, and `+10.2%` cannot shove the histogram. */
.fr-leaseup-variance { text-align: right; }
/* This type's share of the renewal lift. At the right end of the scan line — it is
   the outcome, and the outcome belongs last. Right-aligned in its own measured
   lane, so `+$140/mo` and `+$2,018/mo` stack on the `/mo`. */
.fr-leaseup-cardhead-lift {
    text-align: right;
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 13.5px;
    font-weight: 600;
    color: var(--fr-positive);
    white-space: nowrap;
}
/* At most properties the LARGEST type earns nothing, every bed already at or
   above its website price. §6.2 wants that legible, not hidden. */
.fr-leaseup-cardhead-lift.is-zero { color: var(--fr-ink-3); font-weight: 400; }

.fr-leaseup-caret {
    flex: 0 0 auto;
    width: 26px;
    height: 26px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: 4px;
    background: none;
    font-size: 11px;
    line-height: 1;
    color: var(--fr-ink-2);
    cursor: pointer;
}
.fr-leaseup-caret:hover { border-color: var(--fr-hairline-strong); color: var(--fr-ink); }
.fr-leaseup-caret:focus-visible { outline: 2px solid var(--fr-accent); outline-offset: 2px; }
/* The glyph is CSS off the CARD's class, not a written child, so it cannot
   disagree with the class that decides whether the body is visible. */
.fr-leaseup-caret::after { content: "▴"; }
.fr-leaseup-card.is-collapsed .fr-leaseup-caret::after { content: "▾"; }

/* The three-regime summary: what tells the reviewer, before clicking, whether
   this type is a real pricing decision or is mostly ceiling-bound (§6.1). */
.fr-leaseup-cardhead-regimes {
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 11.5px;
    color: var(--fr-ink-2);
}
/* Loud once the website price is deciding most of the type — the same signal
   §6.3's sentence carries property-wide, restated per card. */
.fr-leaseup-cardhead-regimes.is-bound { color: var(--fr-warning-deep); font-weight: 600; }

/* §6.1's micro-histogram: the rent spread without expanding anything. Normalised
   per card, so it compares within a unit type and not between them. */
.fr-leaseup-hist {
    display: flex;
    align-items: flex-end;
    /* Bars start at the lane's left edge, so a 3-bin type and an 8-bin type share
       a baseline origin instead of being centred against each other. The lane is
       sized from the widest histogram on the page (`_head_lane_style`). */
    justify-content: flex-start;
    gap: 2px;
    height: 18px;
    min-width: 0;
}
.fr-leaseup-hist-bar {
    width: 5px;
    min-height: 2px;
    background: var(--fr-renewals);
    border-radius: 1px 1px 0 0;
    opacity: 0.75;
}
/* An empty bin keeps its WIDTH and loses its height, so a gap in the
   distribution reads as a gap rather than as the histogram simply ending. */
.fr-leaseup-hist-bar.is-empty { background: var(--fr-hairline-strong); min-height: 1px; opacity: 1; }

/* The walkthrough's terminal action for one card, at the place the walkthrough
   actually ends: below the last row. Price every row → mark reviewed → collapse
   → open the next, in one click. */
.fr-leaseup-card-foot {
    display: flex;
    justify-content: flex-end;
    padding: 12px 2px 2px;
}
.fr-leaseup-done-btn {
    border: 1px solid var(--fr-positive);
    border-radius: var(--fr-r-sm, 4px);
    background: var(--fr-surface);
    padding: 6px 14px;
    font-family: var(--fr-font-sans);
    font-size: 11.5px;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--fr-positive);
    cursor: pointer;
    transition: background 120ms ease, color 120ms ease;
}
.fr-leaseup-done-btn:hover { background: var(--fr-positive); color: #FFFFFF; }
.fr-leaseup-done-btn:focus-visible { outline: 2px solid var(--fr-accent); outline-offset: 2px; }

/* Renewals card body: the table now has the full width. The website-price rail
   that used to sit on the left is gone — the input moved into the head, where
   §6.1 wants it (editable without opening the card) and where it doubles as
   §6.6's sticky website price for free. */
.fr-leaseup-renewal-body { display: block; }
.fr-leaseup-price-group { flex-wrap: nowrap; width: auto; flex: 0 0 auto; }
.fr-leaseup-variance {
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 13px;
    font-weight: 600;
    line-height: 1;
}
.fr-leaseup-variance.is-up   { color: var(--fr-positive); }
.fr-leaseup-variance.is-down { color: var(--fr-negative); }
/* No max-width: the table spreads across the card, which is the look the user
   prefers (2026-07-29). Capping it at 1200px was tried and left the table floating
   in the card. The lanes are shares summing to 100 and the gaps carry operators, so
   the spread reads as arithmetic rather than as distance. */
.fr-leaseup-table-wrap { min-width: 0; }

/* ══════════════════════════════════════════════════════════════════════
   RENEWAL ROWS  (.fr-leaseup-rr-*)   visual-design §6.2 / §6.4 / §6.4a / §6.5
   ----------------------------------------------------------------------
   An HTML table of inputs, replacing the Dash DataTable. Group header rows,
   per-row count bars, `at website` text inside a money column, the
   composition sub-line and two editable synced cells per row are all outside
   what the DataTable could express (§13.4) — which is why the DataTable-
   specific rules that used to live here are gone.
   ══════════════════════════════════════════════════════════════════════ */
/* EIGHT LANES, DECLARED AS SHARES — and the spread carries operators.
   ----------------------------------------------------------------------
   Two failed attempts are worth recording, because the second looked like the fix
   for the first and was not.

   1. `width: 100%` + `table-layout: auto` handed the surplus to whichever columns
      had the widest content, so on an 1800px card the rent column took ~580px and
      each money column ~450px, and every value sat centred in a band far wider
      than itself: *"the Increase % and Increase $ live on their own island, the
      2027–28 Offer lives on an island, and the Renewer Saves lives on its own
      island"* (user, 2026-07-29).
   2. Tightening to declared pixel widths removed the emptiness by removing the
      width — which cost the spread the user actually wanted and left a ~700px
      table floating in an 1800px card. Verdict: *"I would probably prefer the more
      spread out look to this."*

   The gaps were never too WIDE, they were EMPTY. So the lanes are shares of the
   card again, and the two operator lanes print the `+` and the `=` that were
   implicitly there all along — invariant 4, the page's stated visual signature,
   and the same device that carries §5's chain strip and §6.7's equation. The
   shares sum to 100, so there is no leftover to park: the annotation lane is a
   declared share like everything else, not a remainder. */
.fr-leaseup-rr {
    width: 100%;
    table-layout: fixed;
    border-collapse: collapse;
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
}
/* Rent + count + bar share ONE lane: `$1,679 ×2` reads as one unit and the bar
   takes what is left of the lane (user, 2026-07-29). A separate BEDS lane was
   tried and stranded the count — rent pinned left, a gap, then `×2`, then a short
   bar. */
.fr-leaseup-rr-th--rent  { width: 20%; }
.fr-leaseup-rr-th--op    { width: 3%;  }
/* Holds both increase boxes side by side: 2 x (54px field + ~26px addon) + gap,
   plus the override dot ≈ 195px. At 17% that clears its content down to a
   ~1150px card, below which the media query hands over. */
.fr-leaseup-rr-th--edit  { width: 17%; }
.fr-leaseup-rr-th--offer { width: 12%; }
.fr-leaseup-rr-th--saves { width: 14%; }
.fr-leaseup-rr-th--note  { width: 31%; }

/* Below ~1300px the editable lane's 17% stops clearing its ~195px of controls, so
   the annotation lane gives up share to it. The note truncates with an ellipsis
   rather than wrapping, which would break the one-row-one-height property that
   moving it inline bought. */
@media (max-width: 1300px) {
    .fr-leaseup-rr-th--rent  { width: 24%; }
    .fr-leaseup-rr-th--edit  { width: 22%; }
    .fr-leaseup-rr-th--offer { width: 13%; }
    .fr-leaseup-rr-th--saves { width: 15%; }
    .fr-leaseup-rr-th--note  { width: 20%; }
}

/* The two arithmetic operators. Quiet on purpose: they repeat on every row — 119
   times in Coug Housing's largest unit type — so they read as connective tissue
   rather than as data. They are what makes the width mean something. */
.fr-leaseup-rr-op {
    text-align: center;
    font-size: 13px;
    color: var(--fr-ink-3);
    user-select: none;
}

/* The editable lane is bounded on both sides, header included, so the `Increase`
   heading visibly owns the two boxes under it. A hairline rather than the amber
   column wash step 2 removed for being the loudest thing in the section. The
   group band interrupts these rules, which is correct — a band is a section
   break. */
.fr-leaseup-rr-th--edit,
.fr-leaseup-rr-editcell {
    border-left: 1px solid var(--fr-hairline);
    border-right: 1px solid var(--fr-hairline);
}
.fr-leaseup-rr-th--edit { border-color: rgba(255, 255, 255, 0.22); }
.fr-leaseup-rr-th {
    padding: 7px 10px;
    background: var(--fr-ink);
    color: #FFFFFF;
    font-family: var(--fr-font-sans);
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.13em;
    text-transform: uppercase;
    text-align: center;
    white-space: nowrap;
}
.fr-leaseup-rr-th:first-child { text-align: left; }
/* The two editable columns are flagged in the header too, so the pair reads as
   a pair before the user reaches a single row. */
.fr-leaseup-rr-th--edit { background: #22282F; }

/* Group headers carry a bed count and a dollar contribution, so the headers
   ALONE describe the shape of the decision before a single row is read (§6.2).
   Grouping here is labelling, not hiding — groups never collapse.

   A BAND spanning every column, not a row of the data grid. Built inside the grid
   first (label across three columns, lift in the fourth) and both halves went
   wrong the same way — a group header is not column-shaped. The label ran across
   the increase inputs so it read as their heading, and the lift landed in the
   `Renewer Saves` lane so it read as that column's label instead of as the group's
   total (user, 2026-07-29). */
.fr-leaseup-rr-group td {
    padding: 12px 10px 5px;
    border-bottom: 1px solid var(--fr-hairline-strong);
    vertical-align: bottom;
    white-space: nowrap;
}
.fr-leaseup-rr-group-band {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 16px;
}
.fr-leaseup-rr-group-label { display: flex; align-items: baseline; }
.fr-leaseup-rr-group-name {
    font-family: var(--fr-font-sans);
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.13em;
    text-transform: uppercase;
    color: var(--fr-ink-2);
}
.fr-leaseup-rr-group-detail {
    margin-left: 8px;
    font-family: var(--fr-font-mono);
    font-size: 11px;
    color: var(--fr-ink-3);
}
/* Rides with the label, not in a column of its own — a bed count describing the
   group had been landing under the `2027–28 Offer` heading, stranded mid-band. */
.fr-leaseup-rr-group-sep { margin: 0 7px; color: var(--fr-hairline-strong); }
.fr-leaseup-rr-group-beds {
    font-family: var(--fr-font-mono);
    font-size: 11.5px;
    font-weight: 600;
    color: var(--fr-ink-2);
}
/* `2 set by hand`. The header says what the generator RECOMMENDS; this says how
   much of the group has since been decided some other way, so the two together
   are a complete account without opening a row. Amber to match the override dot
   it counts — same fact, two grains — and quieter than the bed count beside it,
   because it is an annotation on that number rather than a peer of it. */
.fr-leaseup-rr-group-hand {
    font-family: var(--fr-font-sans);
    font-size: 10.5px;
    font-weight: 600;
    color: var(--fr-accent);
}
.fr-leaseup-rr-group-lift {
    font-size: 11.5px;
    font-weight: 600;
    text-align: right;
    color: var(--fr-positive);
    white-space: nowrap;
}
/* The zero-lift group is the LARGEST at most properties and earns nothing —
   §6.2 wants that legible, not hidden, so the $0 is muted rather than absent. */
.fr-leaseup-rr-group-lift.is-zero { color: var(--fr-ink-3); font-weight: 400; }
.fr-leaseup-rr-group.is-at_website .fr-leaseup-rr-group-name { color: var(--fr-ink-3); }

.fr-leaseup-rr-row td {
    padding: 4px 10px;
    border-bottom: 1px solid var(--fr-hairline);
    text-align: center;
    vertical-align: middle;
}
.fr-leaseup-rr-row:hover td { background: #FBFAF6; }
/* A bucket that is ENTIRELY assumed backfill is an assumption, not a fact, so
   it takes the lighter treatment. A MIXED bucket deliberately does not — most
   of it is real leases (§6.5). */
.fr-leaseup-rr-row.is-assumed .fr-leaseup-rr-rent,
.fr-leaseup-rr-row.is-assumed .fr-leaseup-rr-offer { color: var(--fr-ink-2); font-style: italic; }

.fr-leaseup-rr-rentcell { text-align: left !important; }
/* `$1,679 ×2` then the bar, on one line. Flex on this inner div, never on the <td>:
   `display: flex` on a table-cell drops it out of the table layout algorithm (the
   browser wraps it in an anonymous cell), which silently breaks the column
   alignment. */
.fr-leaseup-rr-rent-wrap { display: flex; align-items: center; gap: 10px; }
/* FIXED-WIDTH as a group, so every row's bar starts at the same x. Sized to
   content, a `×194` would push its bar further right than a `×2`'s and the bars
   would stop being comparable — the one thing a bar is for. 11ch holds
   `$1,679 ×194`; the rent itself is right-aligned inside it so the digits stack. */
.fr-leaseup-rr-rent-line {
    flex: 0 0 11ch;
    display: flex;
    align-items: baseline;
    gap: 6px;
}
.fr-leaseup-rr-rent { font-size: 13px; font-weight: 600; color: var(--fr-ink); }
.fr-leaseup-rr-count { font-size: 11px; color: var(--fr-ink-3); }
.fr-leaseup-rr-new {
    margin-left: 7px;
    font-family: var(--fr-font-sans);
    font-size: 8.5px;
    font-weight: 700;
    letter-spacing: 0.1em;
    color: var(--fr-warning-deep);
    background: var(--fr-warning-tint);
    padding: 1px 4px;
    border-radius: 3px;
}
/* Distribution at a glance, which the bare integer could not give.
   ----------------------------------------------------------------------
   HISTORY, so the width is not re-litigated from scratch: uncapped it filled the
   rent column's ~490px of slack and read as an accidental underline on the rent
   above it; at 92px a unit type whose largest bucket is x10 rendered a single bed
   as 9px — legible as a mark, useless as a measurement, and the user's verdict was
   "unintentionally small"; 230px was where it settled on 2026-07-28 after seeing
   92px and 190px in place.

   THE RETUNING IS OVER, because the cause is gone. Every one of those four passes
   was forced by a change to the column the bar happened to live in, and a bar that
   stops short of its container always reads as truncated no matter what number you
   pick. It now takes ALL the space its lane has left after the fixed-width
   `$1,679 ×2` group, so 100% always reaches the lane's edge and looks like 100%.
   Do not reintroduce a fixed cap. */
.fr-leaseup-rr-bar {
    flex: 1 1 auto;
    min-width: 0;
    height: 4px;
    background: var(--fr-hairline);
    border-radius: 999px;
}
.fr-leaseup-rr-bar-fill {
    /* The smallest bucket must still register as a bar rather than as nothing:
       at a 190px track a single bed in a 60-bed type is 3px. */
    min-width: 3px;
    height: 100%;
    background: var(--fr-renewals);
    opacity: 0.75;
    border-radius: 999px;
}

/* The editable pair (§6.4a). Matched width, matched chrome, no hierarchy
   implying the % is primary — neither is the "real" one to the user. Persistent
   input chrome, deliberately pitched between the old full-column amber wash
   (the loudest thing in the section) and step 2's hover-only border, which the
   user judged "*really* subtle" and could not read as editable. A reviewer
   scanning thirty rows should not have to hover to find the boxes. */
/* The two increase boxes, side by side in ONE cell (user, 2026-07-29). §6.4a's
   ratified pairing is intact — both editable, matched width, dollars canonical,
   no hierarchy implying the % is primary. Only the column boundary between them
   is gone, which is what made them read as two islands. */
/* A GRID, not a flex row, and that is the whole fix for the shape change.
   Flex sized each box against its own content, so the moment the dollar side
   gained a dot the two boxes redistributed — Bootstrap's `.input-group` carries
   `width: 100%`, and a wrapper that stopped stretching handed its slack to the
   percent box (user, 2026-07-29: "the $ column gets squished and the % column
   gets expanded"). Two `1fr` tracks are equal by construction, whatever sits
   inside them. */
.fr-leaseup-rr-pair {
    display: grid;
    grid-template-columns: 1fr 1fr;
    align-items: center;
    gap: 8px;
}
/* `minmax(0, 1fr)` and not `1fr`: a grid track's default minimum is its
   content's min-content width, so an input group would refuse to shrink below
   its addon + field and push the dot's slot off the end. */
.fr-leaseup-rr-input-wrap {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 6px;
    align-items: center;
    gap: 5px;
}
.fr-leaseup-rr-input-group { flex-wrap: nowrap; }
.fr-leaseup-rr-input-group .input-group-text {
    padding: 0 6px;
    background: var(--fr-accent-soft);
    border-color: #E3D6AE;
    color: var(--fr-accent-strong);
    font-size: 11px;
}
.fr-leaseup-page .fr-leaseup-rr-input.form-control {
    width: 54px;
    min-width: 0;
    padding: 2px 5px;
    text-align: center;
    background: #FFFDF6;
    border-color: #E3D6AE;
    font-size: 12.5px;
}
.fr-leaseup-page .fr-leaseup-rr-input.form-control:hover { background: #FFFBEA; }

/* "Did I set this, or did the generator?" — unanswerable an hour into a session
   without it (§10.3). Sits on the DOLLAR box: dollars are the stored quantity,
   and which box the user typed in is deliberately not persisted.

   The SLOT is always there and always the same size, on both boxes; only the
   fill is conditional. That is what lets the dot appear and disappear mid-edit
   without the pair changing shape underneath the cursor — and it is why the
   percent box carries an empty one it will never fill. */
.fr-leaseup-rr-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: transparent;
}
.fr-leaseup-rr-dot.is-on { background: var(--fr-accent); }

.fr-leaseup-rr-offer { font-size: 13px; font-weight: 600; color: var(--fr-ink); }
.fr-leaseup-rr-saves { font-size: 12.5px; color: var(--fr-ink-2); }
.fr-leaseup-rr-muted { color: var(--fr-ink-3); font-size: 11.5px; }
/* The ONE warning colour left on this column. A negative saving means the
   renewer would pay MORE than a brand-new resident, which only arises from a
   hand-typed override above the website price — a genuine anomaly, not a
   direction on a gradient. Amber, because it is a deliberate human choice
   rather than an error (§12). */
.fr-leaseup-rr-over { color: var(--fr-warning-deep); font-weight: 600; }

/* §6.5's composition note, in the trailing lane rather than on a sub-row of its
   own (user, 2026-07-29). Two things come with the move: the lane is where the
   table's surplus width went, so the note costs no layout; and every row is one
   height, so a mixed bucket no longer makes the table taller than a clean one. */
.fr-leaseup-rr-notecell {
    text-align: left !important;
    font-family: var(--fr-font-sans);
    font-size: 11px;
    font-style: italic;
    color: var(--fr-ink-3);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ── Numeric input primitive ─────────────────────────────────────────────
   Every editable number on this page: mono with tabular figures, CENTRED,
   sized to its content.

   Centred is the user's call (2026-07-28): a unit type's rents are almost
   always all-3-digit or all-4-digit, so a shared right edge buys little here.
   What DID matter was the box sprawl — values centred inside a ~360px input
   holding four characters end up far apart no matter how they are aligned.
   Width in ch (not pixels) so it tracks the font. */
.fr-leaseup-num.form-control {
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    text-align: center;
}
/* 4-5 digits with a thousands separator: 1,659 */
.fr-leaseup-num--price.form-control {
    width: calc(6ch + 1.25rem);
    flex: 0 0 auto;
}
/* 1-3 digits: bed quotas, percentages */
.fr-leaseup-num--count.form-control {
    width: calc(4ch + 1.25rem);
    flex: 0 0 auto;
}

/* Tier controls row */
.fr-leaseup-tier-controls { align-items: flex-end; }
.fr-leaseup-field { display: flex; flex-direction: column; gap: 6px; }
.fr-leaseup-tier-pct-row { display: flex; gap: 6px; }
.fr-leaseup-tier-pct-input.form-control {
    padding-left: 6px;
    padding-right: 6px;
}

/* ══════════════════════════════════════════════════════════════════════
   PRE-LEASE TIER SECTION — generator row, derivation equation, matrix
   docs/STUDENT_LEASEUP_PLAN_VISUAL_DESIGN.md §7.1, §7.3, §7.5
   ══════════════════════════════════════════════════════════════════════ */

/* ── Generator row ─────────────────────────────────────────────────── */
.fr-leaseup-generator-lead {
    font-family: var(--fr-font-sans);
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--fr-ink-3);
    align-self: flex-end;
    padding-bottom: 7px;
}
.fr-leaseup-split-sum {
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 11.5px;
    font-weight: 600;
    white-space: nowrap;
    align-self: center;
    margin-left: 8px;
}
.fr-leaseup-split-sum.is-ok   { color: var(--fr-positive); }
.fr-leaseup-split-sum.is-warn { color: var(--fr-warning-deep); }

/* R4: the generator's inputs do nothing until Regenerate runs, so the state
   between "edited" and "applied" has to be visible. */
.fr-leaseup-pending { display: flex; align-items: center; gap: 8px; }
.fr-leaseup-pending-chip {
    font-family: var(--fr-font-sans);
    font-size: 9.5px;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--fr-warning-deep);
    background: var(--fr-warning-tint-2);
    border-radius: 999px;
    padding: 3px 9px;
    white-space: nowrap;
}
.fr-leaseup-pending-note {
    font-family: var(--fr-font-serif);
    font-style: italic;
    font-size: 12px;
    color: var(--fr-ink-3);
}

/* ── Derivation equation (§7.1) ────────────────────────────────────────
   Display-serif numerals with mono captions and REAL operators. Per
   invariant 4 the operators are the content: three KPI cards would say
   "here are three facts", the equation says "this number causes that one".

   Laid out HORIZONTALLY — numeral beside its caption on a shared baseline,
   the whole band left-aligned inside the matrix card. Stacking the caption
   under each numeral and centring the row cost ~150px of mostly-empty grey
   with content only in the middle third of the screen. */
.fr-leaseup-eq {
    display: flex;
    align-items: center;
    gap: 20px;
    flex-wrap: wrap;
    padding: 15px 18px 16px;
    border-bottom: 1px solid var(--fr-hairline-strong);
}
.fr-leaseup-eq-term { display: flex; align-items: baseline; gap: 11px; }
.fr-leaseup-eq-num {
    font-family: var(--fr-font-serif);
    font-variant-numeric: tabular-nums;
    font-size: 34px;
    line-height: 1;
    color: var(--fr-ink);
    letter-spacing: -0.01em;
}
.fr-leaseup-eq-label {
    font-family: var(--fr-font-sans);
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--fr-accent);
}
.fr-leaseup-eq-sub {
    font-family: var(--fr-font-mono);
    font-size: 10.5px;
    line-height: 1.45;
    color: var(--fr-ink-3);
    margin-top: 2px;
}
.fr-leaseup-eq-op {
    font-family: var(--fr-font-serif);
    font-size: 24px;
    line-height: 1;
    color: var(--fr-ink-3);
}

/* ── The matrix (§7.3) ─────────────────────────────────────────────── */
/* Card chrome on the outer element so the equation band shares it; only the
   table scrolls. */
.fr-leaseup-mx-card {
    background: var(--fr-surface);
    border: 1px solid var(--fr-hairline);
    border-radius: var(--fr-r-lg);
    overflow: hidden;
    box-shadow: 0 1px 0 rgba(20, 24, 29, 0.02);
}
.fr-leaseup-mx-wrap { overflow-x: auto; }
.fr-leaseup-mx {
    width: 100%;
    border-collapse: collapse;
    font-family: var(--fr-font-sans);
    color: var(--fr-ink);
}
.fr-leaseup-mx thead th {
    background: #FBFAF6;
    color: var(--fr-ink-3);
    font-size: 10px;
    font-weight: 500;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    padding: 11px 12px;
    border-bottom: 1px solid var(--fr-hairline-strong);
    white-space: nowrap;
    text-align: center;
    vertical-align: bottom;
}
.fr-leaseup-mx-th-label { min-width: 116px; text-align: left !important; }
.fr-leaseup-mx-th-micro { width: 42px; }
.fr-leaseup-mx-th-hint {
    font-family: var(--fr-font-mono);
    font-size: 9px;
    letter-spacing: 0.02em;
    text-transform: none;
    color: var(--fr-accent);
    margin-top: 3px;
}
/* Headers align with the values beneath them. Quota and GPR are read-only
   summary columns whose values genuinely span magnitudes ($10,674 next to
   $394,624), so they stay right-aligned like the GPR Forecast table — and the
   header has to follow, or it floats over a column edge it doesn't share. */
.fr-leaseup-mx-th-quota,
.fr-leaseup-mx-th-gpr { min-width: 96px; text-align: right !important; }
/* Wide enough that the §7.6 state markers land in the empty space beside the
   input group (~114px) rather than on top of it. On a wide screen the table's
   `width: 100%` already gives the tier columns far more than this; the floor
   only matters once the wrapper starts scrolling, where an overlapping marker
   would be the failure. */
.fr-leaseup-mx-th-tier { min-width: 158px; }

/* Pinned label column, so the unit type stays readable while the tier
   columns scroll — the same pattern the GPR Forecast table uses. */
.fr-leaseup-mx-label,
.fr-leaseup-mx-th-label {
    position: sticky;
    left: 0;
    z-index: 2;
    background: var(--fr-surface);
}
.fr-leaseup-mx-th-label { background: #FBFAF6; }

.fr-leaseup-mx-label {
    padding: 10px 14px;
    border-bottom: 1px solid var(--fr-hairline-strong);
    vertical-align: middle;
}
.fr-leaseup-mx-ut {
    font-size: 14px;
    font-weight: 600;
    letter-spacing: -0.005em;
}
.fr-leaseup-mx-ut-sub {
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 10.5px;
    color: var(--fr-ink-3);
    margin-top: 2px;
}
/* The two micro-labels are what remove all ambiguity about which box is
   which, with no tooltip (§7.3). */
.fr-leaseup-mx-micro {
    font-family: var(--fr-font-sans);
    font-size: 9px;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--fr-ink-3);
    text-align: right;
    padding: 0 8px 0 4px;
    white-space: nowrap;
}
.fr-leaseup-mx-cell { padding: 3px 6px; }
.fr-leaseup-mx-row-rate .fr-leaseup-mx-cell { padding-top: 10px; }
.fr-leaseup-mx-row-beds .fr-leaseup-mx-cell { padding-bottom: 8px; }

/* Faint tint darkening Tier 1 → Tier 4, no borders. The tint encodes the
   ascending price ladder, and the quota bar reuses the same ramp
   positions — which is what lets the bar go unlabelled (§10.2). */
.fr-leaseup-mx-cell.is-t1, .fr-leaseup-mx-th-tier.is-t1 { background: var(--fr-tier-1-tint); }
.fr-leaseup-mx-cell.is-t2, .fr-leaseup-mx-th-tier.is-t2 { background: var(--fr-tier-2-tint); }
.fr-leaseup-mx-cell.is-t3, .fr-leaseup-mx-th-tier.is-t3 { background: var(--fr-tier-3-tint); }
.fr-leaseup-mx-cell.is-t4, .fr-leaseup-mx-th-tier.is-t4 { background: var(--fr-tier-4-tint); }

/* ── Per-cell validation states (§7.6) ─────────────────────────────── */
/* Both lines of a tier carry the same state classes and the CSS decides which
   line shows what, so the rate cell and the beds cell under it can never
   disagree. Markers are pseudo-elements for the same reason the §6.9 tick is:
   a cell here holds an input, and rebuilding its children to show a state would
   pull the field out from under the cursor mid-edit (§6.8). */
.fr-leaseup-mx-cell { position: relative; }
.fr-leaseup-mx-cell::after {
    position: absolute;
    right: 9px;
    top: 50%;
    transform: translateY(-50%);
    font-family: var(--fr-font-sans);
    font-size: 9.5px;
    font-weight: 600;
    letter-spacing: 0.06em;
    white-space: nowrap;
    /* The input underneath stays clickable through the marker. */
    pointer-events: none;
}

/* A live price on a tier with no beds — §7.6's headline case, and the one that
   has been on screen since step 3: a priced tier that can never sell. Dimming
   the pair is what stops it reading as part of the plan; the `no beds` marker
   sits on the beds line, directly beside the zero it explains. */
.fr-leaseup-mx-cell.is-dead .fr-leaseup-mx-input { opacity: 0.42; }
.fr-leaseup-mx-row-beds .fr-leaseup-mx-cell.is-dead::after {
    content: "no beds";
    color: var(--fr-ink-3);
    text-transform: uppercase;
}

/* Priced at or below the tier before it. Legal but suspicious, so it is a mark,
   never a block — the sentence naming which tier lives in the quota cell. */
.fr-leaseup-mx-row-rate .fr-leaseup-mx-cell.is-ladder::after {
    content: "▾";
    color: var(--fr-warning-deep);
    font-size: 12px;
}

/* Beds allocated at an explicit $0 — the mirror of `is-dead`, and the one that
   really does put nothing into the forecast. Declared AFTER the ladder rule on
   purpose: a $0 tier following a priced one is both, and this is the more
   serious of the two. */
.fr-leaseup-mx-row-rate .fr-leaseup-mx-cell.is-unpriced::after {
    content: "$0";
    color: var(--fr-warning-deep);
    font-family: var(--fr-font-mono);
    font-size: 11px;
}
.fr-leaseup-mx-cell.is-unpriced { box-shadow: inset 0 0 0 1px var(--fr-warning-deep); }

/* ── The step-1 ↔ step-2 seam (§7.7) ───────────────────────────────── */
/* Always in the DOM, hidden until it has something to say: the sync control is
   part of a pattern-matched ALL input, and a button that appears fires that
   callback as a phantom click (§6.9). Permanent, so it cannot appear. */
.fr-leaseup-mx-seam { display: none; }
.fr-leaseup-mx-seam.is-live {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-top: 5px;
    padding-left: 2px;
}
.fr-leaseup-mx-seam-msg {
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 10px;
    font-weight: 600;
    color: var(--fr-warning-deep);
    white-space: nowrap;
}
/* A quiet control, not a call to action. Syncing is one legitimate answer to
   the divergence; leaving Tier 1 where the user put it is the other, and a
   primary-weight button beside a warning would read as an instruction. */
.fr-leaseup-mx-seam-btn {
    font-family: var(--fr-font-sans);
    font-size: 9.5px;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--fr-warning-deep);
    background: transparent;
    border: 1px solid rgba(140, 86, 24, 0.35);
    border-radius: var(--fr-r-sm);
    padding: 1px 6px;
    white-space: nowrap;
    cursor: pointer;
}
.fr-leaseup-mx-seam-btn:hover {
    background: rgba(140, 86, 24, 0.08);
    border-color: var(--fr-warning-deep);
}

/* The section-header rollup. The marks live at one Tier 1 cell inside a table
   that scrolls; this is the line that cannot be scrolled past. */
.fr-leaseup-seam-rollup { display: none; }
.fr-leaseup-seam-rollup.is-live {
    display: block;
    font-family: var(--fr-font-sans);
    font-size: 11.5px;
    font-weight: 600;
    color: var(--fr-warning-deep);
    background: rgba(140, 86, 24, 0.07);
    border-left: 2px solid var(--fr-warning-deep);
    border-radius: 0 var(--fr-r-sm) var(--fr-r-sm) 0;
    padding: 6px 12px;
    margin: 0 0 12px;
}

/* Matched addon width AND matched field width, so the rate line and the beds
   line form ONE grid. Sized to their own content they disagreed visibly — a
   sliver of a `$` addon over a wide `beds` addon, and a wide rate field over a
   narrow beds field — which read as an accident rather than a decision. */
.fr-leaseup-mx-input { flex-wrap: nowrap; }
.fr-leaseup-mx-input .input-group-text {
    min-width: 44px;
    justify-content: center;
    font-size: 10px;
    padding: 0 6px;
    color: var(--fr-ink-3);
    background: transparent;
    border-color: var(--fr-hairline-strong);
}
.fr-leaseup-mx-input .fr-leaseup-num.form-control {
    width: calc(6ch + 1.25rem);
    flex: 0 0 auto;
    background: var(--fr-surface);
}

.fr-leaseup-mx-quota,
.fr-leaseup-mx-gpr {
    text-align: right;
    padding: 10px 14px;
    border-bottom: 1px solid var(--fr-hairline-strong);
    border-left: 1px solid var(--fr-hairline);
    vertical-align: middle;
    white-space: nowrap;
}
/* The pair and its marker stay on ONE line — a lone ✓ on its own row read as
   an orphan rather than as a confirmation of the number above it. */
.fr-leaseup-mx-quota-num {
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 12.5px;
    font-weight: 600;
}
.fr-leaseup-mx-quota-ok {
    font-size: 11px;
    color: var(--fr-positive);
    margin-left: 6px;
}
/* The named explanations (§7.6). They WRAP where the pair above them does not:
   the cells can only dim, so this is the one place a state gets a sentence, and
   forcing `1 bed unallocated · forecast as renewals` onto the pair's line would
   widen the column past the table. A note only ever renders on a unit type that
   has something wrong with it, so the extra height is the exception state
   paying for itself. */
.fr-leaseup-mx-quota-pair { white-space: nowrap; }
.fr-leaseup-mx-quota-note {
    font-family: var(--fr-font-sans);
    font-size: 9.5px;
    font-weight: 600;
    line-height: 1.35;
    letter-spacing: 0.01em;
    margin-top: 3px;
    max-width: 150px;
    margin-left: auto;
    white-space: normal;
}
/* Beds that quietly leave the pre-lease side. */
.fr-leaseup-mx-quota-note.is-warn { color: var(--fr-warning-deep); }
/* More pre-leases than the generator planned: a deliberate hand edit trading
   renewal beds for pre-lease beds, so it is named and left alone (§10.2 —
   reserve colour for genuine warning states). */
.fr-leaseup-mx-quota-note.is-note { color: var(--fr-ink-3); }
/* More pre-leases than beds exist. The only impossible state in the section. */
.fr-leaseup-mx-quota-note.is-danger { color: var(--fr-negative); }
.fr-leaseup-mx-gpr {
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 13px;
    font-weight: 600;
}

/* Thin, unlabelled: the grid carries the labels, the bar carries only the
   proportion the grid cannot express. First thing to cut if this section
   ever feels busy (§7.3). */
.fr-leaseup-mx-barcell { padding: 0 6px 9px; }
.fr-leaseup-mx-bar {
    display: flex;
    height: 7px;
    border-radius: 999px;
    overflow: hidden;
    background: var(--fr-hairline);
}
.fr-leaseup-mx-bar.is-empty { opacity: 0.4; }
.fr-leaseup-mx-bar-seg.is-t1 { background: var(--fr-tier-1); }
.fr-leaseup-mx-bar-seg.is-t2 { background: var(--fr-tier-2); }
.fr-leaseup-mx-bar-seg.is-t3 { background: var(--fr-tier-3); }
.fr-leaseup-mx-bar-seg.is-t4 { background: var(--fr-tier-4); }

.fr-leaseup-mx-total td {
    background: var(--fr-accent-soft);
    color: var(--fr-accent-strong);
    font-weight: 700;
    border-top: 1px solid rgba(180, 83, 9, 0.30);
    border-bottom: 0;
    padding: 11px 14px;
}
.fr-leaseup-mx-total-label {
    text-align: right;
    font-size: 10px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
}
.fr-leaseup-mx-total .fr-leaseup-mx-quota,
.fr-leaseup-mx-total .fr-leaseup-mx-gpr {
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 13px;
    color: var(--fr-accent-strong);
}

@media (max-width: 900px) {
    .fr-leaseup-eq { gap: 12px; padding: 12px 12px 13px; }
    .fr-leaseup-eq-num { font-size: 27px; }
    .fr-leaseup-eq-op { font-size: 20px; }
}

/* Amenity card */
.fr-leaseup-amenity-field { max-width: 320px; }
.fr-leaseup-field-hint {
    font-family: var(--fr-font-serif);
    font-style: italic;
    font-size: 13px;
    color: var(--fr-ink-3);
    margin-top: 2px;
}

/* GPR forecast table */
.fr-leaseup-fc-wrap {
    background: var(--fr-surface);
    border: 1px solid var(--fr-hairline);
    border-radius: var(--fr-r-lg);
    overflow: hidden;
    box-shadow: 0 1px 0 rgba(20, 24, 29, 0.02);
}
.fr-leaseup-fc-table {
    width: 100%;
    border-collapse: collapse;
    font-family: var(--fr-font-sans);
    font-size: 13px;
    color: var(--fr-ink);
}
.fr-leaseup-fc-table thead th {
    background: #FBFAF6;
    color: var(--fr-ink-3);
    font-size: 10.5px;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.14em;
    padding: 13px 18px;
    border-bottom: 1px solid var(--fr-hairline-strong);
    white-space: nowrap;
}
.fr-leaseup-fc-table tbody td {
    padding: 11px 18px;
    border-bottom: 1px solid var(--fr-hairline);
    vertical-align: middle;
}
.fr-leaseup-fc-table .is-left  { text-align: left; }
.fr-leaseup-fc-table .is-right { text-align: right; }
.fr-leaseup-fc-table .fr-cell-money,
.fr-leaseup-fc-table .fr-cell-num {
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
}
.fr-leaseup-fc-name { font-weight: 500; }
.fr-leaseup-fc-strong { font-weight: 600; }
.fr-leaseup-fc-table tbody tr:nth-of-type(even):not(.fr-leaseup-fc-subtotal):not(.fr-leaseup-fc-amenity):not(.fr-leaseup-fc-total) td {
    background: #FCFBF7;
}
.fr-leaseup-fc-subtotal td {
    background: #FBFAF6;
    font-weight: 600;
    border-top: 1px solid var(--fr-hairline-strong);
}
.fr-leaseup-fc-amenity td { color: var(--fr-ink-2); }
/* Inline qualifier on the amenity row's label. Carries what the old
   "(static)" was trying to say — property-wide, not derived per unit type —
   without putting a parenthetical in the row's name. */
.fr-leaseup-fc-hint {
    font-size: 10.5px;
    color: var(--fr-ink-3);
    font-weight: 400;
    letter-spacing: 0.01em;
}
.fr-leaseup-fc-total td {
    background: var(--fr-accent-soft);
    color: var(--fr-accent-strong);
    font-weight: 700;
    font-size: 14px;
    border-top: 1px solid rgba(180, 83, 9, 0.30);
    border-bottom: 0;
}
.fr-leaseup-fc-total .is-left {
    text-transform: uppercase;
    letter-spacing: 0.10em;
    font-size: 11px;
}

/* ── Share of forecast (§8.1) ──────────────────────────────────────── */
/* Fills the gulf between the pinned unit-type column and the money columns.
   The first draft put a renewal/pre-lease MIX bar here, which fails the
   section's own honesty test — renewal % is one property-wide input, so every
   row's mix is identical by construction and the column would print `40/60`
   three times. Share of forecast varies 7× across rows and answers a question
   the numbers alone do not: which unit types carry this property. */
.fr-leaseup-fc-th-share { padding-left: 18px !important; }
.fr-leaseup-fc-sharecell { padding: 11px 18px; width: 26%; }
.fr-leaseup-fc-share {
    display: flex;
    align-items: center;
    gap: 10px;
}
/* The TRACK is the full lane; this element's width is the row's share, so the
   row-to-row comparison is the thing the eye actually measures. Percent, not
   pixels, so it survives the table being resized. */
.fr-leaseup-fc-bar {
    height: 9px;
    border-radius: 999px;
    overflow: hidden;
    /* Pre-lease is the remainder inside each bar, so the bar's own ground IS
       the pre-lease hue and only the renewal share needs drawing. */
    background: var(--fr-prelease);
    min-width: 3px;
    flex: 0 0 auto;
}
.fr-leaseup-fc-bar-fill {
    height: 100%;
    background: var(--fr-renewals);
}
/* Fixed width so the percentages form a column edge rather than tracking the
   end of each bar — a number that moves with the bar reads as part of the bar
   and stops being scannable down the page. */
.fr-leaseup-fc-share-num {
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 11px;
    color: var(--fr-ink-3);
    width: 42px;
    text-align: right;
    flex: 0 0 auto;
}
/* The summary rows have no share of anything — an empty cell, not a zero bar. */
.fr-leaseup-fc-subtotal .fr-leaseup-fc-sharecell,
.fr-leaseup-fc-amenity .fr-leaseup-fc-sharecell,
.fr-leaseup-fc-total .fr-leaseup-fc-sharecell { padding: 0; }

/* ── The closing block (§9) ────────────────────────────────────────── */
/* The page used to scroll to the forecast total and stop. For a once-a-year
   commit there has to be a terminal moment, so "I'm done" has a location —
   without a bottom, a long careful session ends in ambiguity about whether it
   is finished. Deliberately the widest, quietest surface on the page: nothing
   here is an input, and it should read as a full stop. */
.fr-leaseup-closing {
    margin: 26px 0 40px;
    padding: 22px 26px 24px;
    background: var(--fr-surface);
    border: 1px solid var(--fr-hairline);
    border-top: 2px solid var(--fr-accent);
    border-radius: var(--fr-r-lg);
}
.fr-leaseup-closing:empty { display: none; }
.fr-leaseup-closing-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 24px;
    flex-wrap: wrap;
}
.fr-leaseup-closing-lead .fr-section-title { margin: 2px 0 0; }
.fr-leaseup-closing-action {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 6px;
}
/* Stated as a presence rather than left as an absence: the Rate Manager DOES
   push to Entrata, and a manager who works in both surfaces will otherwise
   assume this one does too (§9). */
.fr-leaseup-closing-scope {
    font-size: 11px;
    color: var(--fr-ink-3);
    max-width: 34ch;
    text-align: right;
    line-height: 1.45;
}

/* ── Publish gate + preview (Phase C step 5) ────────────────────────────── */

/* Save and the offers action sit on one row, in the order they happen: a saved
   plan is a precondition of previewing offers. */
.fr-leaseup-closing-buttons {
    display: flex;
    align-items: center;
    gap: 10px;
}

/* Why the offers button is unavailable. Sits under both buttons and wraps to the
   same measure as the scope line so the block reads as one column of prose. */
.fr-leaseup-publish-note {
    max-width: 46ch;
    text-align: right;
}
.fr-leaseup-publish-reasons {
    display: flex;
    flex-direction: column;
    gap: 3px;
}
.fr-leaseup-publish-reason {
    font-size: 11px;
    line-height: 1.45;
}
/* A blocker is a thing to act on; a warning is a fact about Entrata's calendar
   that no amount of clicking changes. Different colours, and neither is red —
   §10.2 reserves red for genuine error states and "the lease term does not exist
   yet" is the expected condition for months. */
.fr-leaseup-publish-reasons.is-blocked .fr-leaseup-publish-reason {
    color: var(--fr-warning-deep);
}
.fr-leaseup-publish-reasons.is-warning .fr-leaseup-publish-reason {
    color: var(--fr-ink-3);
}

.fr-leaseup-publish-ready {
    font-size: 13px;
    line-height: 1.5;
    padding: 10px 12px;
    border-radius: 4px;
    border-left: 3px solid transparent;
    margin-bottom: 14px;
}
.fr-leaseup-publish-ready.is-ok {
    border-left-color: var(--fr-positive);
    background: var(--fr-positive-wash, rgba(16, 122, 87, 0.06));
    color: var(--fr-positive-deep);
}
.fr-leaseup-publish-ready.is-warning {
    border-left-color: var(--fr-warning);
    background: var(--fr-warning-wash, rgba(180, 83, 9, 0.06));
    color: var(--fr-warning-deep);
}

/* The headline figures. Mono + tabular via .fr-leaseup-num on the value. */
.fr-leaseup-publish-stats {
    display: flex;
    flex-wrap: wrap;
    gap: 24px;
    padding: 12px 0 14px;
    border-bottom: 1px solid var(--fr-hairline);
}
.fr-leaseup-publish-stat-value {
    font-size: 22px;
    font-weight: 600;
    line-height: 1.15;
}
.fr-leaseup-publish-stat.is-lift .fr-leaseup-publish-stat-value {
    color: var(--fr-positive-deep);
}
.fr-leaseup-publish-stat-label {
    font-family: var(--fr-font-mono);
    font-size: 9.5px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--fr-ink-3);
    margin-top: 3px;
}

.fr-leaseup-publish-prov,
.fr-leaseup-publish-cap {
    font-family: var(--fr-font-mono);
    font-size: 10.5px;
    color: var(--fr-ink-3);
    margin: 10px 0 0;
}

.fr-leaseup-publish-subhead {
    font-family: var(--fr-font-serif);
    font-size: 15px;
    margin: 18px 0 6px;
}
.fr-leaseup-publish-lead,
.fr-leaseup-publish-sublead {
    font-size: 12.5px;
    color: var(--fr-ink-2);
    margin: 8px 0 4px;
}
.fr-leaseup-publish-note-inline {
    font-size: 12px;
    color: var(--fr-ink-3);
    margin: 8px 0 0;
    line-height: 1.5;
}
/* Decision 2's disclosure. Counts are mono so they line up down the left edge —
   the reader is comparing magnitudes, not reading sentences. */
.fr-leaseup-publish-reasonlist,
.fr-leaseup-publish-blockers {
    margin: 4px 0 0;
    padding-left: 18px;
    font-size: 12.5px;
    line-height: 1.7;
    color: var(--fr-ink-2);
}
.fr-leaseup-publish-reasonlist .fr-leaseup-num {
    display: inline-block;
    min-width: 4.5ch;
    font-weight: 600;
}

/* Wide content scrolls inside its own box; the modal body never scrolls
   sideways. */
.fr-leaseup-publish-tablewrap {
    overflow-x: auto;
}
.fr-leaseup-publish-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 12.5px;
}
.fr-leaseup-publish-table th {
    font-family: var(--fr-font-mono);
    font-size: 9.5px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--fr-ink-3);
    text-align: left;
    font-weight: 500;
    padding: 6px 10px 6px 0;
    border-bottom: 1px solid var(--fr-hairline-strong);
    white-space: nowrap;
}
.fr-leaseup-publish-table td {
    padding: 5px 10px 5px 0;
    border-bottom: 1px solid var(--fr-hairline);
    white-space: nowrap;
}
/* A combined multi-bedspace offer is worth spotting: it is the row most likely
   to look wrong at a glance, because one resident carries several beds' rent. */
.fr-leaseup-publish-table td.is-multi {
    font-family: var(--fr-font-mono);
    font-size: 11px;
    color: var(--fr-accent);
}
.fr-leaseup-publish-empty {
    font-family: var(--fr-font-serif);
    font-style: italic;
    color: var(--fr-ink-3);
}
.fr-leaseup-publish-error {
    font-family: var(--fr-font-mono);
    font-size: 11px;
    white-space: pre-wrap;
    background: var(--fr-surface-2, rgba(0, 0, 0, 0.03));
    padding: 8px 10px;
    border-radius: 3px;
    overflow-x: auto;
}
/* The one-line ledger summary, in the footer beside the buttons. */
.fr-leaseup-publish-footnote {
    flex: 1;
    font-family: var(--fr-font-mono);
    font-size: 10.5px;
    color: var(--fr-ink-3);
    text-align: left;
    line-height: 1.5;
    padding-right: 12px;
}

/* The chain restated as one line of plain type. NOT the strip's component: the
   strip is sticky orientation with step markers and anchor nav, this is a
   closing statement read once. Same `chain` dict, so they cannot disagree. */
.fr-leaseup-closing-chain {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 14px;
    margin-top: 18px;
    padding-top: 16px;
    border-top: 1px solid var(--fr-hairline);
}
.fr-leaseup-closing-chain:empty { display: none; }
.fr-leaseup-closing-term {
    display: flex;
    align-items: baseline;
    gap: 7px;
}
.fr-leaseup-closing-num {
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 16px;
    font-weight: 600;
    color: var(--fr-ink);
}
.fr-leaseup-closing-num.is-total {
    font-size: 21px;
    color: var(--fr-accent-strong);
}
.fr-leaseup-closing-cap {
    font-size: 10px;
    letter-spacing: 0.10em;
    text-transform: uppercase;
    color: var(--fr-ink-3);
}
.fr-leaseup-closing-op {
    font-family: var(--fr-font-mono);
    font-size: 14px;
    color: var(--fr-ink-3);
}

/* Still open. Every line is a link to the step that fixes it — a list of
   problems at the bottom of a page this long is only useful if it is also the
   way back to them. */
.fr-leaseup-closing-open { margin-top: 16px; }
.fr-leaseup-closing-open:empty { display: none; }
.fr-leaseup-closing-open-label {
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--fr-ink-3);
    margin-bottom: 7px;
}
.fr-leaseup-closing-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 5px;
}
.fr-leaseup-closing-item {
    position: relative;
    padding-left: 15px;
    font-size: 12.5px;
    line-height: 1.5;
}
.fr-leaseup-closing-item::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0.55em;
    width: 6px;
    height: 6px;
    border-radius: 50%;
}
.fr-leaseup-closing-item.is-warn::before   { background: var(--fr-warning-deep); }
.fr-leaseup-closing-item.is-danger::before { background: var(--fr-negative); }
.fr-leaseup-closing-link {
    color: var(--fr-ink-2);
    text-decoration: none;
    border-bottom: 1px solid var(--fr-hairline-strong);
}
.fr-leaseup-closing-link:hover {
    color: var(--fr-ink);
    border-bottom-color: var(--fr-ink-3);
}

/* The all-clear is STATED, not implied by an absence — it is the sentence
   someone is actually looking for at the end of a once-a-year pass, and an
   empty space does not say it. */
.fr-leaseup-closing-clear {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--fr-ink-2);
}
.fr-leaseup-closing-tick {
    color: var(--fr-positive);
    font-weight: 700;
}

/* Empty / info notes */
.fr-leaseup-note {
    font-family: var(--fr-font-serif);
    font-style: italic;
    font-size: 15px;
    padding: 14px 4px;
}
.fr-leaseup-note--muted  { color: var(--fr-ink-3); }
.fr-leaseup-note--warn   { color: var(--fr-warning-deep); }
.fr-leaseup-note--danger { color: var(--fr-negative-deep); }


/* ══════════════════════════════════════════════════════════════════════
   STUDENT LEASE-UP TRACKER  (.fr-tracker-*)
   ----------------------------------------------------------------------
   Read-only live-pace dashboard. Reuses the Lease-Up Plan vocabulary
   (.fr-leaseup-page, .fr-section-*, .fr-renewal-controls, .fr-renewal-seg)
   and adds tracker-specific chrome: editorial header, a pace-summary chip
   row, a chart card, and signed-vs-plan tier-progress cells.
   ══════════════════════════════════════════════════════════════════════ */

.fr-tracker-page { font-family: var(--fr-font-sans); }

/* ── Editorial header (eyebrow + serif title) ──────────────────────── */
.fr-tracker-head {
    display: flex;
    flex-direction: column;
    margin: 6px 2px 6px;
}
.fr-tracker-title { margin: 4px 0 0; }

/* ── Filter bar tweaks ─────────────────────────────────────────────── */
.fr-tracker-controls-row { align-items: flex-end; gap: 22px; }
.fr-tracker-field { min-width: 150px; }
/* Wide enough for the full cycle label, e.g. "2026 Cycle (Oct 2025 – Sep 2026)". */
.fr-tracker-field:first-child  { min-width: 300px; flex: 0 1 320px; }
.fr-tracker-field:nth-child(2) { min-width: 240px; flex: 1 1 240px; }
.fr-tracker-field:last-child   { min-width: 200px; flex: 1 1 200px; }
/* The metric segmented control sits a touch above the dropdown baseline;
   nudge it down so its pill aligns with the dropdown controls. */
.fr-tracker-metric-seg { margin-bottom: 1px; }

/* Cycle / benchmark dropdown menu options on the lease-up pages: keep each
   option on a single line so long cycle labels (e.g. "2026 Cycle (Oct 2025
   – Sep 2026)") don't wrap and collide. Paired with the wider Cycle fields. */
.fr-leaseup-page .fr-renewal-ut-dropdown .Select-menu-outer .Select-option {
    white-space: nowrap;
}

/* ── Pace summary chips (this cycle vs prior, same cycle-week) ──────── */
.fr-tracker-pace-summary {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin: 2px 2px 14px;
    min-height: 0;
}
.fr-tracker-pace-summary:empty { margin: 0; }
.fr-tracker-pace-chip {
    display: inline-flex;
    align-items: baseline;
    gap: 8px;
    background: var(--fr-surface);
    border: 1px solid var(--fr-hairline);
    border-radius: 100px;
    padding: 7px 15px;
    box-shadow: 0 1px 0 rgba(20, 24, 29, 0.02);
}
.fr-tracker-pace-chip-label {
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--fr-ink-3);
}
.fr-tracker-pace-chip-value {
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 15px;
    font-weight: 600;
    color: var(--fr-ink);
}
.fr-tracker-pace-chip-delta {
    font-size: 12px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
.fr-tracker-pace-chip-delta.is-up   { color: var(--fr-positive); }
.fr-tracker-pace-chip-delta.is-down { color: var(--fr-negative); }
.fr-tracker-pace-chip-delta.is-flat { color: var(--fr-ink-3); }

/* ── Chart card ────────────────────────────────────────────────────── */
.fr-tracker-chart-card {
    background: var(--fr-surface);
    border: 1px solid var(--fr-hairline);
    border-radius: var(--fr-r-lg);
    padding: 14px 16px 8px;
    box-shadow: 0 1px 0 rgba(20, 24, 29, 0.02);
}

/* Lease-Up Tracker charts (§1 YoY Pace + §3 Lead Demand) — undo the global
   ".js-plotly-plot { height: 500px !important }" force (PLOTLY CHART STYLES,
   ~line 1764) for THESE charts only. That global rule pins every Plotly element
   to 500px and gives it its own border/shadow, so each chart drew its content at
   the figure's real height inside a 500px box (the blank band / slight overflow)
   and stacked a second card inside fr-tracker-chart-card. Scoped to these two
   chart ids so every other page stays untouched. Each graph fills its styled
   height (responsive=True), and the card grows to contain it — one clean box,
   no squish, no overflow. */
#student-tracker-yoy-chart .js-plotly-plot,
#student-tracker-yoy-chart .js-plotly-plot .plotly,
#student-tracker-yoy-chart .js-plotly-plot .svg-container,
#student-tracker-yoy-chart .js-plotly-plot .main-svg,
#student-tracker-demand-chart .js-plotly-plot,
#student-tracker-demand-chart .js-plotly-plot .plotly,
#student-tracker-demand-chart .js-plotly-plot .svg-container,
#student-tracker-demand-chart .js-plotly-plot .main-svg {
    height: 100% !important;
    min-height: 0 !important;
    max-height: none !important;
}
#student-tracker-yoy-chart .js-plotly-plot,
#student-tracker-demand-chart .js-plotly-plot {
    border: 0 !important;
    box-shadow: none !important;
    border-radius: 0 !important;
    background: transparent !important;
    overflow: visible !important;
}

/* ── Tier-progress cells (signed vs plan) ──────────────────────────── */
.fr-tracker-progress-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 14px;
}
.fr-tracker-progress-cell {
    background: var(--fr-surface);
    border: 1px solid var(--fr-hairline);
    border-radius: var(--fr-r-lg);
    padding: 18px 20px 16px;
    box-shadow: 0 1px 0 rgba(20, 24, 29, 0.02);
}
.fr-tracker-cell-group {
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--fr-ink-3);
    margin-bottom: 10px;
}
.fr-tracker-cell-count { display: flex; align-items: baseline; gap: 7px; margin-bottom: 12px; }
.fr-tracker-cell-signed {
    font-family: var(--fr-font-serif);
    font-size: 34px;
    font-weight: 400;
    line-height: 1;
    letter-spacing: -0.01em;
    color: var(--fr-ink);
    font-variant-numeric: tabular-nums;
}
.fr-tracker-cell-target {
    font-family: var(--fr-font-mono);
    font-size: 14px;
    color: var(--fr-ink-3);
    font-variant-numeric: tabular-nums;
}
.fr-tracker-cell-bar {
    height: 7px;
    border-radius: 100px;
    background: var(--fr-hairline);
    overflow: hidden;
    margin-bottom: 8px;
}
.fr-tracker-cell-bar-fill {
    height: 100%;
    border-radius: 100px;
    background: var(--fr-accent);
    transition: width 240ms ease;
}
.fr-tracker-cell-bar-fill.is-ahead  { background: var(--fr-positive); }
.fr-tracker-cell-bar-fill.is-on     { background: var(--fr-accent); }
.fr-tracker-cell-bar-fill.is-behind { background: var(--fr-warning); }
.fr-tracker-cell-bar-fill.is-risk   { background: var(--fr-negative); }
.fr-tracker-cell-bar-fill.is-none   { background: var(--fr-hairline-strong); width: 0 !important; }
.fr-tracker-cell-status {
    font-family: var(--fr-font-sans);
    font-size: 11px;
    font-weight: 500;
    color: var(--fr-ink-3);
}
.fr-tracker-cell-status.is-ahead  { color: var(--fr-positive-deep); }
.fr-tracker-cell-status.is-behind { color: var(--fr-warning-deep); }
.fr-tracker-cell-status.is-risk   { color: var(--fr-negative-deep); }

/* What the count is measuring. Deliberately quieter than the status line and
   never colour-banded: it is a permanent fact about the metric, not a signal. */
.fr-tracker-cell-basis {
    font-family: var(--fr-font-mono);
    font-size: 9.5px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--fr-ink-4, var(--fr-ink-3));
    margin-top: 2px;
}

/* Empty / loading note */
.fr-tracker-empty {
    font-family: var(--fr-font-serif);
    font-style: italic;
    font-size: 15px;
    color: var(--fr-ink-3);
    padding: 14px 4px;
}


/* ══════════════════════════════════════════════════════════════════════
   STUDENT RATE MANAGER  (.fr-rate-mgr-* / .fr-rm-*)
   ----------------------------------------------------------------------
   In-season pricing cockpit. Shares the editorial header / controls /
   section headers / scoreboard with the Tracker + Plan pages, and adds a
   data-table vocabulary (.fr-rm-table) for the live rate board, the modal
   change preview, and the audit log — mirroring the .fr-leaseup-fc-table
   look so all the lease-up surfaces feel like one ledger.
   ══════════════════════════════════════════════════════════════════════ */

.fr-rate-mgr-page { font-family: var(--fr-font-sans); }

/* ── Filter bar ────────────────────────────────────────────────────── */
/* Top-align so the Cycle label/input lines up with the Benchmark
   label/input even though Benchmark carries a hint line beneath it. */
.fr-rate-mgr-controls-row { align-items: flex-start; gap: 22px; }
/* Wide enough for the full cycle label, e.g. "2026 Cycle (Oct 2025 – Sep 2026)". */
.fr-rate-mgr-field:first-child { min-width: 300px; flex: 0 1 320px; }
.fr-rate-mgr-field:nth-child(2) { min-width: 280px; flex: 1 1 320px; }

/* ── Summary scoreboard wrap + per-score sub-line ──────────────────── */
.fr-rate-mgr-summary-wrap { margin: 2px 0 4px; }
.fr-rm-score-sub {
    font-family: var(--fr-font-sans);
    font-size: 11px;
    color: var(--fr-ink-3);
    margin-top: 7px;
}

/* ── Lead Demand pill (property-level, summary band) ───────────────── */
.fr-rm-demand-pill {
    font-family: var(--fr-font-sans);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.02em;
}
.fr-rm-demand-pill.is-hot    { color: var(--fr-positive); }
.fr-rm-demand-pill.is-normal { color: var(--fr-ink-2); }
.fr-rm-demand-pill.is-cold   { color: var(--fr-negative); }
.fr-rm-demand-pill.is-none   { color: var(--fr-ink-3); }
.fr-rm-demand-detail { margin-top: 4px; }

/* ── Detail pop-out modals: expand affordance + modal body ─────────── */
/* Summary-score header holds the title + (optional) expand icon inline. */
.fr-rm-score-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}
/* A context-zone figure + its expand icon (board group-header cells). The
   figure grows to fill the cell so the icon sits right beside it (top-right),
   not stranded at the far edge. */
.fr-rm-zone-cell {
    display: flex;
    align-items: flex-start;
    gap: 8px;
}
.fr-rm-zone-cell > :first-child {
    flex: 1 1 auto;
    min-width: 0;
}
/* The expand icon — a small bordered button beside the figure. Quiet by
   default, accent on hover, so the board stays calm but the figure clearly
   signals "there's more behind this." */
.fr-rm-expand-btn {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    border: 1px solid var(--fr-hairline);
    background: var(--fr-paper, #FFFFFF);
    color: var(--fr-ink-2);
    font-size: 14px;
    line-height: 1;
    border-radius: 6px;
    cursor: pointer;
    opacity: 0.85;
    transition: opacity 0.12s ease, color 0.12s ease, background 0.12s ease, border-color 0.12s ease;
}
.fr-rm-expand-btn:hover {
    opacity: 1;
    color: var(--fr-accent, #B45309);
    background: var(--fr-accent-soft, #FEF3C7);
    border-color: var(--fr-accent, #B45309);
}
.fr-rm-expand-btn:focus-visible {
    outline: 2px solid var(--fr-accent, #B45309);
    outline-offset: 1px;
    opacity: 1;
}

/* Detail modal body: intro/footnote text + the one-line numeric read. */
.fr-rm-detail-modal .modal-body { padding-top: 8px; }
.fr-rm-modal-intro {
    font-family: var(--fr-font-sans);
    font-size: 12.5px;
    color: var(--fr-ink-2);
    margin: 0 0 10px;
    max-width: 70ch;
}
.fr-rm-modal-foot {
    font-family: var(--fr-font-sans);
    font-size: 11px;
    color: var(--fr-ink-3);
    margin: 8px 0 0;
    max-width: 80ch;
}
.fr-rm-modal-read {
    display: flex;
    align-items: baseline;
    gap: 8px;
    flex-wrap: wrap;
    margin: 0 0 8px;
}
.fr-rm-modal-read-text {
    font-family: var(--fr-font-sans);
    font-size: 13px;
    color: var(--fr-ink);
    font-weight: 500;
}

/* ── Benchmark save/reset controls (pace comparison set) ───────────── */
.fr-rate-mgr-benchmark-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 8px;
}
.fr-rate-mgr-benchmark-btn {
    font-size: 11px;
    padding: 4px 10px;
}
.fr-rate-mgr-benchmark-status {
    font-family: var(--fr-font-sans);
    font-size: 11px;
    margin-left: 2px;
}

/* ── Push controls ─────────────────────────────────────────────────── */
.fr-rate-mgr-push-bar { margin-top: 16px; }
.fr-rate-mgr-modal-note {
    font-family: var(--fr-font-sans);
    font-size: 11.5px;
    color: var(--fr-ink-3);
    margin-top: 10px;
    line-height: 1.5;
}

/* ── Data table (rate board / modal preview / audit log) ───────────── */
.fr-rm-table-wrap {
    background: var(--fr-surface);
    border: 1px solid var(--fr-hairline);
    border-radius: var(--fr-r-lg);
    overflow-x: auto;
    box-shadow: 0 1px 0 rgba(20, 24, 29, 0.02);
}
.fr-rm-table {
    width: 100%;
    border-collapse: collapse;
    font-family: var(--fr-font-sans);
    font-size: 13px;
    color: var(--fr-ink);
}
.fr-rm-table thead th {
    background: #FBFAF6;
    color: var(--fr-ink-3);
    font-size: 10.5px;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.14em;
    text-align: left;
    padding: 13px 16px;
    border-bottom: 1px solid var(--fr-hairline-strong);
    white-space: nowrap;
}
.fr-rm-table tbody td {
    padding: 11px 16px;
    border-bottom: 1px solid var(--fr-hairline);
    vertical-align: middle;
}
.fr-rm-table--compact thead th { padding: 10px 14px; }
.fr-rm-table--compact tbody td { padding: 9px 14px; }
.fr-rm-table .is-right { text-align: right; }
.fr-rm-table .fr-cell-money {
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
}

/* Option B column model: the pricing zone (name + rate cluster) is pinned to
   fixed widths so the rates hug the floor-plan name; the context zone
   (Pre-Lease Pace · Renewal Pace · Comp) absorbs the remaining width, leaving
   room for the planned pace/comp modals. */
.fr-rm-table th.fr-rm-col-name { width: 220px; }
.fr-rm-table th.fr-rm-col-rate { width: 250px; }

/* Hairline divider between the pricing zone and the context zone. Lives on the
   first context column (Pre-Lease Pace) in both head and body so it runs as one
   continuous rule down the table. */
.fr-rm-table th.fr-rm-zone,
.fr-rm-table td.fr-rm-zone { border-left: 1px solid var(--fr-hairline); }

/* Unit-type group header row — warm tint + accent top rule delineate groups */
.fr-rm-group-row td {
    background: #FBF6EC;
    border-top: 2px solid rgba(180, 83, 9, 0.28);
    border-bottom: 1px solid var(--fr-hairline-strong);
}
.fr-rm-group-row:first-child td { border-top: 0; }
.fr-rm-group-name { font-weight: 600; color: var(--fr-ink); }
.fr-rm-strong { font-weight: 600; color: var(--fr-ink); }

/* Space-option sub-row cell */
.fr-rm-so-cell { display: flex; align-items: center; padding-left: 14px; }
.fr-rm-so-label { color: var(--fr-ink-2); }
.fr-rm-check { display: inline-block; margin-right: 9px; vertical-align: middle; }

/* Editable new-rate input (inherits .fr-leaseup-page .form-control base) */
.fr-rm-input.form-control {
    width: 104px;
    height: 32px;
    padding: 4px 10px;
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
}

/* Tight pricing cluster: current → [input] [inline Δ], hugging the name. */
.fr-rm-rate-cell {
    display: flex;
    align-items: center;
    gap: 9px;
    white-space: nowrap;
}
.fr-rm-cur {
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    color: var(--fr-ink-2);
}
.fr-rm-arrow { color: var(--fr-ink-3); }
.fr-rm-delta-inline {
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 11.5px;
    font-weight: 600;
}

/* Context row: "rates as of …" info + the demoted Refresh link, inline. */
.fr-rate-mgr-context-row {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 6px;
}
/* Demoted "Refresh from Entrata" — quiet accent text-link, not a button. */
.fr-rm-refresh-link {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    background: none;
    border: 0;
    cursor: pointer;
    padding: 2px 6px;
    font-family: var(--fr-font-sans);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.04em;
    color: var(--fr-accent);
    border-radius: var(--fr-r-sm);
    transition: background 0.12s ease, color 0.12s ease;
}
.fr-rm-refresh-link:hover { background: var(--fr-accent-soft); color: var(--fr-accent-strong); }
.fr-rm-refresh-link:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(180, 83, 9, 0.12); }
.fr-rm-refresh-ico { font-size: 13px; line-height: 1; }

/* ── Inline status / delta / comp colorings ────────────────────────── */
.fr-rm-subtle {
    font-family: var(--fr-font-sans);
    font-size: 11.5px;
    color: var(--fr-ink-3);
}
/* ── Pace cell (Variant D): leased % headline + surfaced gap + mini bar ─────
   leased % is the headline (labeled "leased"); the gap ("▼ 16% behind") is
   colored and sits beside it; a mini progress bar fills to leased % with a dark
   tick at the ideal — "how far behind" reads as a visible distance. Mirrors the
   New Leases percentile-bar idiom. */
.fr-rm-pace { line-height: 1.3; min-width: 132px; }
.fr-rm-pace-top {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
}
.fr-rm-pace-leased {
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    font-size: 16px;
    color: var(--fr-ink);
    white-space: nowrap;
}
.fr-rm-pace-lab {
    font-family: var(--fr-font-sans);
    font-weight: 500;
    font-size: 11px;
    color: var(--fr-ink-3);
    margin-left: 3px;
}
.fr-rm-pace-gap {
    font-family: var(--fr-font-sans);
    font-weight: 600;
    font-size: 12px;
    white-space: nowrap;
}
.fr-rm-pace-gap.dir-ahead  { color: var(--fr-positive); }
.fr-rm-pace-gap.dir-behind { color: var(--fr-negative); }
.fr-rm-pace-gap.dir-on     { color: var(--fr-ink-2); }
.fr-rm-pace-gap.dir-none   { color: var(--fr-ink-3); font-weight: 500; }
.fr-rm-pacebar {
    position: relative;
    height: 8px;
    border-radius: 5px;
    background: var(--fr-hairline);
    margin-top: 7px;
}
.fr-rm-pacebar-fill {
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    border-radius: 5px;
    transition: width 0.25s ease-out;
}
.fr-rm-pacebar-fill.is-ahead  { background: var(--fr-positive); }
.fr-rm-pacebar-fill.is-behind { background: var(--fr-negative); }
.fr-rm-pacebar-fill.is-on     { background: var(--fr-accent); }
.fr-rm-pacebar-fill.is-none   { background: var(--fr-hairline-strong); }
/* Ideal tick — a dark vertical line that pokes just beyond the track. */
.fr-rm-pacebar-mark {
    position: absolute;
    top: -3px;
    bottom: -3px;
    width: 2px;
    background: var(--fr-ink);
    border-radius: 1px;
}
.fr-rm-pace-ideal {
    font-size: 11px;
    color: var(--fr-ink-3);
    margin-top: 5px;
}
/* Count-fallback primary (capacity unknown — no bar). */
.fr-rm-pace-primary {
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    margin-right: 8px;
    color: var(--fr-ink);
}
.fr-rm-status { font-weight: 600; font-size: 12px; }
.fr-rm-status.is-ahead  { color: var(--fr-positive); }
.fr-rm-status.is-on     { color: var(--fr-ink-2); }
.fr-rm-status.is-behind { color: var(--fr-negative); }
.fr-rm-status.is-none   { color: var(--fr-ink-3); }
.fr-rm-delta {
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    font-weight: 600;
}
.fr-rm-delta.is-up   { color: var(--fr-positive); }
.fr-rm-delta.is-down { color: var(--fr-negative); }
.fr-rm-delta.is-flat { color: var(--fr-ink-3); }
.fr-rm-comp { font-weight: 600; font-variant-numeric: tabular-nums; }
.fr-rm-comp.is-strong { color: var(--fr-positive); }
.fr-rm-comp.is-mid    { color: var(--fr-warning); }
.fr-rm-comp.is-weak   { color: var(--fr-negative); }
.fr-rm-comp.is-none   { color: var(--fr-ink-3); }

/* ── Comp cell: market-position dot-line (ported from the New Leases card) ───
   A line spanning the market best-price min→max, a hollow dot per competitor,
   and a filled strength-colored "You" dot (with a caret) marking our rate. */
.fr-rm-comp-cell { min-width: 150px; }
.fr-rm-comp-head {
    display: flex;
    align-items: baseline;
    gap: 8px;
    font-size: 12.5px;
}
.fr-rm-comp-you-rate {
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    color: var(--fr-ink-2);
}
.fr-rm-comp-n { color: var(--fr-ink-3); font-size: 11px; }
.fr-rm-comp-line {
    position: relative;
    height: 6px;
    border-radius: 100px;
    background: var(--fr-hairline);
    box-shadow: inset 0 0 0 1px var(--fr-hairline-strong);
    margin: 16px 0 6px;   /* top room for the You caret */
}
.fr-rm-comp-track { position: absolute; inset: 0; border-radius: 100px; }
.fr-rm-comp-dot {
    position: absolute;
    top: 50%;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--fr-paper, #FFFFFF);
    border: 1px solid rgba(20, 24, 29, 0.35);
    transform: translate(-50%, -50%);
    z-index: 3;
}
.fr-rm-comp-you {
    position: absolute;
    top: 50%;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: var(--fr-ink);
    border: 2px solid var(--fr-paper, #FFFFFF);
    box-shadow: 0 0 0 1px var(--fr-ink);
    transform: translate(-50%, -50%);
    z-index: 5;
}
.fr-rm-comp-you.is-strong { background: var(--fr-positive); box-shadow: 0 0 0 1px var(--fr-positive); }
.fr-rm-comp-you.is-mid    { background: var(--fr-warning);  box-shadow: 0 0 0 1px var(--fr-warning); }
.fr-rm-comp-you.is-weak   { background: var(--fr-negative); box-shadow: 0 0 0 1px var(--fr-negative); }
.fr-rm-comp-you-caret {
    position: absolute;
    bottom: 100%;
    width: 0;
    height: 0;
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    border-top: 5px solid var(--fr-ink);
    transform: translate(-50%, -3px);
    z-index: 6;
}
.fr-rm-comp-you-caret.is-strong { border-top-color: var(--fr-positive); }
.fr-rm-comp-you-caret.is-mid    { border-top-color: var(--fr-warning); }
.fr-rm-comp-you-caret.is-weak   { border-top-color: var(--fr-negative); }
.fr-rm-comp-ends {
    display: flex;
    justify-content: space-between;
    font-family: var(--fr-font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 11px;
    color: var(--fr-ink-3);
}

/* Audit-log specifics */
.fr-rm-notes { color: var(--fr-ink-2); font-size: 12px; }
.fr-rm-flag {
    font-family: var(--fr-font-sans);
    font-size: 9px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--fr-warning-deep);
    background: var(--fr-warning-tint);
    padding: 2px 6px;
    border-radius: 4px;
    margin-left: 6px;
}

/* Error / warning context-line states */
.fr-rm-error { color: var(--fr-negative-deep); font-family: var(--fr-font-mono); font-size: 11px; }
.fr-rm-warn  { color: var(--fr-warning-deep); font-family: var(--fr-font-mono); font-size: 11px; }

/* Empty / loading note */
.fr-rate-mgr-empty {
    font-family: var(--fr-font-serif);
    font-style: italic;
    font-size: 15px;
    color: var(--fr-ink-3);
    padding: 14px 4px;
}

/* ─────────────────────────────────────────────────────────────────────
   Eviction Watch panel (app/layouts/eviction_watch.py)
   Read-only delinquency → eviction risk forecast. Scoped under .fr-evw-page.
   ───────────────────────────────────────────────────────────────────── */
.fr-evw-page { max-width: 1280px; }

/* Portfolio summary tiles */
.fr-evw-stats {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 14px;
    margin: 4px 0 18px;
}
.fr-evw-stat {
    background: var(--fr-surface);
    border: 1px solid var(--fr-hairline);
    border-radius: var(--fr-r-md);
    padding: 16px 18px;
}
.fr-evw-stat-value {
    font-family: var(--fr-font-serif);
    font-size: 30px;
    line-height: 1.1;
    color: var(--fr-ink);
}
.fr-evw-stat-value--accent { color: var(--fr-accent); }
.fr-evw-stat-label {
    font-family: var(--fr-font-sans);
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--fr-ink-3);
    margin-top: 6px;
}

/* Portfolio horizon row */
.fr-evw-horizon-row {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    background: var(--fr-surface);
    border: 1px solid var(--fr-hairline);
    border-radius: var(--fr-r-md);
    padding: 12px 16px;
    margin-bottom: 20px;
}
.fr-evw-horizon-title {
    font-family: var(--fr-font-sans);
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--fr-ink-3);
    margin-right: 8px;
}
.fr-evw-hz-cell {
    display: flex;
    align-items: baseline;
    gap: 6px;
    padding: 2px 12px;
    border-left: 1px solid var(--fr-hairline);
}
.fr-evw-hz-num { font-family: var(--fr-font-serif); font-size: 20px; color: var(--fr-ink); }
.fr-evw-hz-label { font-size: 12px; color: var(--fr-ink-2); }

/* Risk chips */
.fr-evw-chip {
    font-family: var(--fr-font-sans);
    font-size: 10px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: 2px 8px;
    border-radius: 999px;
    white-space: nowrap;
}
.fr-evw-chip--critical { color: #fff;                 background: var(--fr-negative); }
.fr-evw-chip--high     { color: var(--fr-negative-deep); background: var(--fr-negative-tint); }
.fr-evw-chip--elevated { color: var(--fr-warning-deep);  background: var(--fr-warning-tint); }
.fr-evw-chip--watch    { color: var(--fr-ink-2);         background: var(--fr-hairline); }
.fr-evw-chip--low      { color: var(--fr-ink-3);         background: var(--fr-hairline); }

/* Per-property collapsible sections */
.fr-evw-props { display: flex; flex-direction: column; gap: 10px; }
.fr-evw-prop {
    background: var(--fr-surface);
    border: 1px solid var(--fr-hairline);
    border-radius: var(--fr-r-md);
    overflow: hidden;
}
.fr-evw-prop > summary {
    cursor: pointer;
    list-style: none;
    padding: 14px 18px;
}
.fr-evw-prop > summary::-webkit-details-marker { display: none; }
.fr-evw-prop[open] > summary { border-bottom: 1px solid var(--fr-hairline); }
.fr-evw-prop-summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
}
.fr-evw-prop-id { display: flex; align-items: baseline; gap: 10px; }
.fr-evw-prop-name { font-family: var(--fr-font-serif); font-size: 19px; color: var(--fr-ink); }
.fr-evw-prop-state {
    font-family: var(--fr-font-mono);
    font-size: 11px;
    color: var(--fr-ink-3);
    border: 1px solid var(--fr-hairline-strong);
    border-radius: 4px;
    padding: 1px 5px;
}
.fr-evw-prop-metrics { display: flex; gap: 18px; flex-wrap: wrap; }
.fr-evw-prop-metric { font-size: 13px; color: var(--fr-ink-2); }
.fr-evw-prop-metric strong { color: var(--fr-ink); font-weight: 600; }
.fr-evw-prop-metric--accent strong { color: var(--fr-accent); }
.fr-evw-horizon-strip { display: flex; gap: 12px; flex-wrap: wrap; }
.fr-evw-horizon-bit { font-size: 12px; color: var(--fr-ink-3); }
.fr-evw-horizon-bit strong { color: var(--fr-ink-2); }

/* Flagged-lease table */
.fr-evw-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.fr-evw-table thead th {
    font-family: var(--fr-font-sans);
    font-size: 10px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--fr-ink-3);
    text-align: left;
    padding: 10px 14px;
    border-bottom: 1px solid var(--fr-hairline);
}
.fr-evw-table tbody td {
    padding: 10px 14px;
    border-bottom: 1px solid var(--fr-hairline);
    color: var(--fr-ink-2);
    vertical-align: top;
}
.fr-evw-table tbody tr:last-child td { border-bottom: none; }
.fr-evw-table tbody tr:nth-child(even) { background: var(--fr-bg); }
.fr-evw-td-unit { font-family: var(--fr-font-mono); color: var(--fr-ink); white-space: nowrap; }
.fr-evw-td-num { font-family: var(--fr-font-mono); white-space: nowrap; }
.fr-evw-td-why { color: var(--fr-ink-3); font-size: 12px; max-width: 320px; }

/* Excluded benign-tail footnote — reconciles the visible list with the full
   delinquency count without alarming the reader. */
.fr-evw-minor-note {
    margin: 8px 0 2px;
    padding: 8px 14px 0;
    font-family: var(--fr-font-sans);
    font-size: 12px;
    color: var(--fr-ink-3);
    border-top: 1px dashed var(--fr-hairline);
}
.fr-evw-nl-minor { color: var(--fr-ink-3); }

/* Note + empty/error states */
.fr-evw-note {
    margin-top: 18px;
    font-family: var(--fr-font-sans);
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--fr-ink-3);
}
/* Workman page — keep the lede legible sans, not the editorial italic serif. */
.fr-evw-page .fr-page-lede {
    font-family: var(--fr-font-sans);
    font-style: normal;
    font-size: 15px;
    color: var(--fr-ink-2);
}
/* Unit Type column */
.fr-evw-td-utype {
    font-family: var(--fr-font-mono);
    font-size: 12px;
    color: var(--fr-ink);
    white-space: nowrap;
}
.fr-evw-empty {
    background: var(--fr-surface);
    border: 1px solid var(--fr-hairline);
    border-radius: var(--fr-r-md);
    padding: 40px 28px;
    text-align: center;
}
.fr-evw-empty-title { font-family: var(--fr-font-serif); font-size: 20px; color: var(--fr-ink); margin-bottom: 6px; }
.fr-evw-empty-sub { font-size: 14px; color: var(--fr-ink-3); }

@media (max-width: 900px) {
    .fr-evw-stats { grid-template-columns: repeat(2, 1fr); }
}

/* Eviction Watch — New Leases surfacing (subtle property-scoped strip) */
.fr-evw-nl {
    margin: 0 0 18px;
    background: var(--fr-warning-tint);
    border: 1px solid var(--fr-hairline);
    border-left: 3px solid var(--fr-warning);
    border-radius: var(--fr-r-md);
    overflow: hidden;
}
.fr-evw-nl > summary {
    cursor: pointer;
    list-style: none;
    padding: 11px 16px;
}
.fr-evw-nl > summary::-webkit-details-marker { display: none; }
.fr-evw-nl-headline {
    display: flex;
    align-items: baseline;
    gap: 12px;
    flex-wrap: wrap;
    font-size: 13px;
    color: var(--fr-ink-2);
}
.fr-evw-nl-mark { color: var(--fr-warning); font-size: 14px; }
.fr-evw-nl-title {
    font-family: var(--fr-font-sans);
    font-weight: 600;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--fr-warning-deep);
}
.fr-evw-nl-dash { color: var(--fr-ink-3); }
.fr-evw-nl-stat strong { color: var(--fr-ink); font-weight: 600; }
.fr-evw-nl-stat--accent strong { color: var(--fr-accent); }
.fr-evw-nl-chev { margin-left: auto; color: var(--fr-ink-3); transition: transform 0.15s ease; }
.fr-evw-nl[open] .fr-evw-nl-chev { transform: rotate(90deg); }
.fr-evw-nl-body {
    padding: 4px 8px 10px;
    background: var(--fr-surface);
    border-top: 1px solid var(--fr-hairline);
}
.fr-evw-nl-note {
    margin: 8px 8px 2px;
    font-family: var(--fr-font-sans);
    font-size: 12px;
    line-height: 1.5;
    color: var(--fr-ink-3);
}
