/* ═══════════════════════════════════════════════════════════════════════════
   calculators.css — shared calculator mechanics layer
   Sits between css/tepuy.css (site-wide) and each calculator's own
   page-specific stylesheet. First occupant: the Results Tab System
   ("rtab-*"), extracted from retirement.html's inline <style id="rtab-styles">
   block (2026-08 Stage 1 cleanup) — this is the literal source of the
   sitewide tab-underline pattern (see css/PvS_style.css's ".tabs/.tab"
   comment, which ported its structural values from this same block).

   !important flags from the original inline block were audited on
   extraction, not copied blindly:
   - .rtab-btn--active keeps !important on color/background because
     css/tepuy.css declares a competing !important rule for the same
     selector, and both values match.

     SECOND CORRECTION, 5 September 2026 — the border-bottom half of this
     paragraph had gone stale. It used to read "border-bottom does not
     [match] — tepuy.css has 2.5px solid #1a6b4a, this file has 2px solid
     #1a5236", and the measured table below listed border-bottom as a
     property THIS file wins on source order. Neither is true any more:
     #217 deleted `border-bottom: 2.5px solid #1a6b4a !important` from
     tepuy.css (see that file's own note directly above its surviving
     .rtab-btn--active block). tepuy.css now declares only color,
     background and font-weight, so this file's `border-bottom: 2px solid
     #1a5236 !important` is UNOPPOSED — it renders because nothing else
     declares the property, not because it wins a cascade race. Its
     !important is now belt-and-braces rather than load-bearing; do not
     read the flag as evidence a competitor exists.

     What is unchanged: color and background are still declared !important
     on both sides at the same values, and `font-weight: 800 !important`
     still comes solely from tepuy.css. The last bullet below is still
     right that .rtab-btn--active is the one selector where a competing
     tepuy.css declaration exists and wins a property — that property is
     now font-weight alone.

     CORRECTION, 2 September 2026. This paragraph used to end "That
     competing rule ... has since been deleted from tepuy.css as dead
     weight ... nothing about its correctness depended on tepuy.css's copy
     existing." Both halves were wrong, and the second one materially so.

     What was deleted in the 2026-08 consolidation audit was the
     .rtab-bar/.rtab-btn/.rtab-panel BASE rules — see tepuy.css's own
     comment directly above its surviving .rtab-btn--active block, which
     names exactly those three and no more. The .rtab-btn--active rule
     itself is still there (css/tepuy.css, search `.rtab-btn--active`)
     and still carries three !important declarations. (It carried FOUR
     when this paragraph was written; #217 removed the border-bottom.)

     And it is not inert. Measured in a browser on the real page — both
     sheets loaded, retirement.html loads tepuy.css first and this file
     last — the active tab computes:
         border-bottom  2px solid rgb(26,82,54)   <- THIS file, uncontested
         color          rgb(26,82,54)             <- tie, same value
         background     rgb(255,255,255)          <- tie, same value
         font-weight    800                       <- tepuy.css wins
     Colour and background are declared on both sides at equal specificity
     and equal importance with identical values, so the cascade is a tie
     that decides nothing. Border-bottom is declared HERE ONLY since #217.
     `font-weight: 800 !important` has NO counterpart here, so nothing in
     this file can override it: the active tab's boldness is supplied
     solely by tepuy.css. Deleting that rule would change the rendered
     page, which is the opposite of what the old wording implied.
   - Every other rule below had zero competing declaration anywhere in the
     codebase — this claim was WRONG when first written (2026-08
     consolidation audit found tepuy.css silently shadowing .rtab-bar/
     .rtab-btn/.rtab-panel's base padding/font-size/background with
     different values, cascade-order dependent, undetected until a
     later audit). Those three base copies are gone, so the claim holds
     for them — but see the correction above: .rtab-btn--active is the
     one selector where a competing tepuy.css declaration still exists
     and still wins a property.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Shared typography — root scale + insight/KPI card text ──────────────
   Added 2026-08 (calculators.css consolidation audit). Both retirement.html
   (.insight-label/.insight-value, tepuy.css) and property_vs_shares.html
   (.kpi-label/.kpi-value, PvS_style.css) had their own independent copies
   of this exact sizing — declared identically by coincidence, not by
   sharing a source, which is exactly how they drifted: PvS_style.css
   picked up a stray duplicate html{font-size} override AND a font-size
   "floor" rule that accidentally shrank .kpi-label below its own declared
   size, and nothing caught it because there was no single shared
   declaration either page's markup was actually depending on. This block
   is now that shared declaration — colors, margins, and card box-model
   (padding/border/shadow/hover) stay page-specific in each stylesheet
   (genuinely different per page — PvS's property/shares/gold value
   coloring is load-bearing, retirement's isn't), only text sizing moved. */
html { font-size: 16px; }

.insight-label,
.kpi-label {
  font-size: .8rem;
  font-weight: 600;
  letter-spacing: .4px;
  text-transform: uppercase;
  margin-bottom: 6px;
}

.insight-value,
.kpi-value {
  font-size: 1.5rem;
  font-weight: 700;
}

/* ══════════════════════════════════════════════════════════════════════════
   PERFORMANCE OVERVIEW KPI GRID  (moved here from css/PvS_style.css, 2026-09)

   The standing rule is that no calculator carries its own CSS for the
   sidebar, results, insights, tables or tabs. These rules used to live in
   css/PvS_style.css, which is loaded by one page; this file is loaded by all
   five, so the grid now sits beside the .insight-label/.insight-value
   typography it already shared with retirement.html.

   ── Mechanics copied from Retirement, not reinvented ──
   Retirement's .insights is `repeat(auto-fit, minmax(160px, 1fr))` and its
   .insight-card is a centred flex column with a 1px border, 8px radius, 1rem
   padding and a resting `0 2px 4px rgba(0,0,0,.05)` shadow. The same shape is
   used here. auto-fit/minmax rather than breakpoints is deliberate and is the
   pattern docs/OPEN-ITEMS.md already records as canonical for this repo (see
   the .repeater-card entry): the column count follows whatever width the
   component is given, with no media query and no reference to a page's own
   container name. That matters twice over here — the rules PvS_style.css used
   were keyed on `@container pvs-main`, a container only PvS declares, so they
   could never have worked from a shared file.

   min(100%, 20rem) as the track floor: 20rem gives two columns in the ~950px
   results panel and one column below ~652px, and the min(100%, …) guard stops
   the track exceeding a container narrower than 20rem (a 390px phone), which
   would otherwise overflow.
   ══════════════════════════════════════════════════════════════════════════ */
.kpi-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
  gap: 0.75rem;
  margin-bottom: 16px;
}

.kpi-card {
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  background: var(--bg-surface, #fff);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 1rem;
  box-shadow: 0 2px 4px rgba(0,0,0,.05);
  transition: var(--transition);
}
.kpi-card:hover {
  border-color: var(--border-focus);
  transform: translateY(-2px);
  box-shadow: var(--shadow-sm);
}

/* The verdict cell spans every column auto-fit produced — `1 / -1` rather
   than `span 2`, because the column count is not fixed. */
.kpi-card--verdict { grid-column: 1 / -1; }

/* Label colour matches retirement.html's .insight-label intent (a muted
   label above a coloured value) but reads the token rather than tepuy.css's
   hardcoded #5b6a73. On PvS --text-secondary resolves to #3d5248, 8.40:1 on
   white. */
.kpi-label { color: var(--text-secondary); }
.kpi-value { line-height: 1.1; margin-bottom: 5px; }
.kpi-sub   { font-size: .85rem; color: var(--text-secondary); line-height: 1.45; }

/* ── Winner colours ──
   Measured against #ffffff with the WCAG 2.x formula before being chosen:
   --gold-text (#8d5529, the AA-safe clay) 6.06:1 and --moss (#4C7A5B)
   4.95:1, both clearing the 4.5:1 AA minimum for normal text. They replace
   #4e8ef7 / #4ecb71, of which the green measured 2.08:1 and was failing on
   three nodes in every axe run. Bar FILLS use raw --clay/--moss (graphical
   objects, 3:1) while TEXT uses the darkened clay. */
.kpi-value--prop   { color: var(--gold-text); }
.kpi-value--shares { color: var(--moss); }

/* ── Verdict cell: the discounted restatement, clearly subordinate ── */
.kpi-verdict-pv {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: .35rem .5rem;
  margin: .7rem 0 0;
  padding-top: .6rem;
  border-top: 1px solid var(--border);
  width: 100%;
  font-size: .78rem;
  line-height: 1.4;
}
.kpi-verdict-pv-label { color: var(--text-secondary); }
.kpi-verdict-pv-value { font-weight: 600; color: var(--text-secondary); }

/* ── Who is ahead, year by year ──
   The track is filled by js/app1.js with one .kpi-be-seg per segment of
   summary.leadTrack, each carrying --kpi-seg-w: the segment's own widthPct,
   straight off the engine field. It is data, not styling: every colour, size
   and shape below is declared here, and the view never positions anything —
   the segments are flex items in source order and each boundary marker sits
   on its own segment's left edge, so no running total is taken anywhere.

   The clay / moss pair measures only 1.49:1 against EACH OTHER, so the
   segments are never distinguished by colour alone: a 2px --ink marker sits
   on every boundary, the legend names each side in text, and each segment
   carries a title attribute naming its side and years. */
.kpi-be { width: 100%; margin: .15rem 0 .55rem; }
.kpi-be-track {
  position: relative;
  display: flex;
  height: 10px;
  border-radius: 5px;
  background: var(--bg-raised);
  overflow: hidden;
}
.kpi-be-seg {
  position: relative;
  flex: 0 0 calc(var(--kpi-seg-w, 0) * 1%);
  min-width: 0;
}
.kpi-be-seg--prop   { background: var(--clay); }
.kpi-be-seg--shares { background: var(--moss); }
/* One boundary per segment after the first, on that segment's own left edge.
   Full-bleed vertically so it reads as a division of the track rather than as
   a tick inside it. */
.kpi-be-marker {
  position: absolute;
  top: 0; bottom: 0; left: 0;
  width: 2px;
  background: var(--ink);
}

.kpi-be-scale {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .4rem;
  margin-top: .35rem;
  font-size: .62rem;
  letter-spacing: .03em;
  color: var(--text-secondary);
}
.kpi-be-legend { display: inline-flex; gap: .6rem; flex-wrap: wrap; justify-content: center; }
.kpi-be-key { display: inline-flex; align-items: center; gap: .25rem; white-space: nowrap; }
.kpi-be-key::before {
  content: '';
  width: .5rem; height: .5rem; border-radius: 2px;
  background: var(--border);
}
.kpi-be-key--prop::before   { background: var(--clay); }
.kpi-be-key--shares::before { background: var(--moss); }
/* Only name a side the track actually shows. js/app1.js adds
   .kpi-be--has-prop / .kpi-be--has-shares per segment it appends. */
.kpi-be:not(.kpi-be--has-prop)   .kpi-be-key--prop,
.kpi-be:not(.kpi-be--has-shares) .kpi-be-key--shares { display: none; }

/* ── IRR: the gap, not the two levels ── */
.kpi-value.kpi-value-irr {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: .3rem;
  font-size: 1.35rem;
  font-weight: 800;
}
.kpi-value-irr .property { color: var(--gold-text); }
.kpi-value-irr .shares   { color: var(--moss); }
.kpi-value-irr .kpi-vs {
  font-size: .62rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--text-secondary);
}

.kpi-irr-gap {
  display: flex;
  align-items: center;
  gap: .45rem;
  width: 100%;
  max-width: 15rem;
  margin: .35rem 0 .1rem;
}
.kpi-irr-gap-bar {
  position: relative;
  flex: 1;
  height: 8px;
  border-radius: 4px;
  background: var(--bg-raised);
  overflow: hidden;
}
/* --kpi-irr-gap-pct: the spread as a share of the higher IRR, written by
   js/app1.js. --kpi-irr-gap-color: which side the spread favours. */
.kpi-irr-gap-fill {
  position: absolute;
  top: 0; bottom: 0; left: 0;
  width: calc(var(--kpi-irr-gap-pct, 0) * 1%);
  background: var(--kpi-irr-gap-color, var(--border));
  border-radius: 4px;
}
.kpi-irr-gap-label {
  font-size: .68rem;
  font-weight: 700;
  white-space: nowrap;
  color: var(--text-secondary);
}

/* ── The scenario, on one line, beneath the grid ──
   Deliberately not a card and not a bordered block: it is the smallest thing
   in the results area, subordinate to the three cells above it, and it earns
   its place only because a screenshot of those cells is unreadable without
   knowing what was fed in.

   No page-specific rule backs it — it lives here, with the rest of the KPI
   block, so a second calculator adding an assumption line gets the same one.

   It wraps rather than scrolls. Measured at 390px the eight items run to
   four lines at .72rem; a single non-wrapping line would need either a
   horizontal scroll (which a screenshot cannot show) or a font size below
   the 11px floor this repo holds elsewhere, and both defeat the point.

   --text-secondary, not --text-muted: PvS resolves it to #3d5248, 8.40:1 on
   the surface these sit on. The <strong> values take --text-primary so the
   values read ahead of their labels without a second colour. */
.kpi-assumptions {
  display: block;
  margin: 0;
  padding: 0;
  font-size: .72rem;
  line-height: 1.7;
  text-align: center;
  color: var(--text-secondary);
}
.kpi-assumptions .kpi-assumption { white-space: nowrap; }
.kpi-assumptions strong { font-weight: 600; color: var(--text-primary); }

/* scroll-margin-top: Retirement's own calc-init.js calls
   `.results-tabs-wrap.scrollIntoView({block:'start'})` after Calculate
   on narrow screens. Without this, that lands the wrap flush with
   viewport y:0 — directly behind the fixed navbar (tepuy.css's
   .navbar, position:fixed, ~64px tall) — since scrollIntoView has no
   built-in awareness of fixed headers. Confirmed as a REAL bug (not
   theoretical) on property_vs_shares.html's equivalent
   #results_content, via Playwright WebKit + a real calc-engine-driven
   render: the results rendered partially hidden under the navbar
   immediately after the first Run Comparison — see PvS_style.css's own
   #results_content{scroll-margin-top} comment for the full repro.
   Mortgage/Offset vs Shares/LMI don't currently call scrollIntoView on
   their own .results-tabs-wrap, so this is a no-op there today — but
   it's the shared file precisely so a future calculator adding that
   same "scroll to results on Calculate" convenience doesn't reintroduce
   the same bug. 80px matches --stick-top, tepuy.css's own canonical
   navbar-clearance value (used for the sticky sidebar's own top
   offset), not a new number invented for this. */
.results-tabs-wrap {
  background: #ffffff;
  border: 1px solid #dce8e1;
  border-radius: 14px;
  overflow: hidden;
  margin-bottom: 1.25rem;
  box-shadow: 0 2px 8px rgba(0,0,0,0.05);
  scroll-margin-top: 80px;
}
.rtab-bar {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 0.55rem 0.75rem 0;
  background: #f7faf8;
  border-bottom: 1px solid #e3ede8;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.rtab-bar::-webkit-scrollbar { display: none; }

/* ── Scroll affordance ─────────────────────────────────────────────────
   At narrow widths .rtab-bar's tabs overflow and the row scrolls
   (overflow-x:auto above), but nothing on screen said so — the trailing
   tab just truncated at the edge with no visual hint there was more to
   scroll to. .rtab-bar-wrap is a NEW element wrapping .rtab-bar (not
   .rtab-bar itself, which scrolls — a child positioned relative to a
   scrolling ancestor scrolls away with it) so the two fade overlays stay
   pinned to the visible edges regardless of scroll position.

   js/rtab-scroll-affordance.js toggles data-fade-start/data-fade-end on
   .rtab-bar-wrap from the nested .rtab-bar's scrollLeft/scrollWidth/
   clientWidth — on load, on scroll, and on resize — rather than a static
   always-on gradient: a bar whose tabs already fit (no scrolling
   possible) gets neither fade, and the leading fade only appears once
   scrolled away from the start.

   #f7faf8 matches .rtab-bar's own background exactly (no dark-mode
   override exists for it site-wide, confirmed by grep — nothing to
   theme this against). */
.rtab-bar-wrap {
  position: relative;
}
.rtab-fade {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 28px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s ease;
  z-index: 1;
}
.rtab-fade--start {
  left: 0;
  background: linear-gradient(to right, #f7faf8, transparent);
}
.rtab-fade--end {
  right: 0;
  background: linear-gradient(to left, #f7faf8, transparent);
}
.rtab-bar-wrap[data-fade-start="1"] .rtab-fade--start,
.rtab-bar-wrap[data-fade-end="1"] .rtab-fade--end {
  opacity: 1;
}

.rtab-btn {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.35rem 0.85rem 0.4rem;
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  /* #7a948a measured 3.11:1 on the tab strip and 3.27:1 on white — below
     the 4.5:1 AA minimum for text this size. #4a655a is the same green-grey
     family and was already validated at 6.23:1 in #185. */
  color: #4a655a;
  background: transparent;
  border: none;
  border-radius: 6px 6px 0 0;
  cursor: pointer;
  white-space: nowrap;
  transition: color 0.15s, background 0.15s;
  position: relative;
  bottom: -1px;
  border-bottom: 2px solid transparent;
  font-family: inherit;
}
.rtab-btn:hover:not(:disabled) { color: #1a5236; background: #eef5f1; }
.rtab-btn--active {
  color: #1a5236 !important;
  background: #ffffff !important;
  border-bottom: 2px solid #1a5236 !important;
}
.rtab-btn--soon { opacity: 0.5; }
.rtab-btn:disabled { cursor: default; }
.rtab-soon {
  font-size: 0.58rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  background: #e8f5ee;
  color: #3d7a5a;
  padding: 0.08rem 0.3rem;
  border-radius: 3px;
}
.rtab-panel { display: block; padding: 1rem 1.25rem 1.25rem; }
.rtab-panel--hidden { display: none; }
.rtab-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.75rem;
  flex-wrap: wrap;
}
.rtab-insights { margin-top: 0.75rem; }
.rtab-coming-soon {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  padding: 3rem 1rem;
  text-align: center;
  color: var(--text-muted);
}
.rtab-coming-soon p { font-size: 1rem; font-weight: 600; color: #3d5248; margin: 0; }
.rtab-coming-soon span { font-size: 0.8rem; max-width: 360px; line-height: 1.5; }
#rtab-montecarlo { padding: 0.75rem; }
#rtab-montecarlo .dd-panel,
#rtab-montecarlo .ttr-panel,
#rtab-montecarlo .mc-panel,
#rtab-montecarlo .gap-insights-panel { margin-bottom: 0.65rem; }
#rtab-yearbyyear .ret-table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
[data-pro-locked="true"] {
  opacity: 0.55;
  cursor: not-allowed;
  position: relative;
}
[data-pro-locked="true"]::after {
  content: " 🔒";
  font-size: 0.7em;
  opacity: 0.8;
}
[data-pro-locked="true"] {
  pointer-events: auto;
}
.ins-caps-title {
  font-size: 0.78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: #3d5248;
  margin: 0.5rem 0 0.3rem;
}
.ins-alert {
  margin-top: 0.75rem;
  padding: 0.65rem 0.9rem;
  border-radius: 8px;
  font-size: 0.84rem;
  line-height: 1.55;
  border-left: 3px solid transparent;
}
.ins-alert--warn {
  background: #fffbeb;
  border-color: #f59e0b;
  color: #78350f;
}
.ins-alert--info {
  background: #eff6ff;
  border-color: #3b82f6;
  color: #1e3a5f;
}
.ins-alert b { font-weight: 700; }
.insight-card--mc {
  position: relative;
  background: #f4fbf7;
  border: 1px solid #b3d9c4;
  cursor: pointer;
}
.insight-card--mc:hover { border-color: #3db07e; box-shadow: 0 0 0 3px rgba(61,176,126,0.12); }
.insight-mc-ring {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0.25rem auto 0.3rem;
  width: 52px;
  height: 52px;
}
.ins_mcPct {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.88rem;
  font-weight: 700;
  letter-spacing: -0.03em;
  color: #0f1c16;
  font-variant-numeric: tabular-nums;
}
.ins-risk-badge {
  display: inline-block;
  font-size: 0.6rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  padding: 0.15rem 0.5rem;
  border-radius: 20px;
  background: #e8f0eb;
  color: var(--text-muted);
  transition: background 0.3s, color 0.3s;
}
.ins-risk-badge--strong   { background: #e6f5ed; color: #1a6b4a; }
.ins-risk-badge--moderate { background: #fef3cd; color: #92400e; }
.ins-risk-badge--atrisk   { background: #fff0e6; color: #c05000; }
.ins-risk-badge--highrisk { background: #fde8e8; color: #b91c1c; }

/* ═══════════════════════════════════════════════════════════════════════════
   PvS-sourced standards (2026-08 Stage 2) — extracted from css/PvS_style.css,
   which stays loaded on property_vs_shares.html and still owns the design
   tokens (--gold, --text-secondary, --bg-surface, --radius-*, --shadow-*,
   --transition, etc.) these rules reference via var(). That's fine — custom
   property resolution isn't file-load-order-dependent, it resolves off the
   cascaded value at the point of use regardless of which stylesheet defines
   it. Values below are the WINNING computed values from PvS_style.css, not
   raw copies of its base rules — several selectors there carry 2-3
   scattered !important overrides layered on at different dates (see PR for
   the resolution trail); copying only the base rule would have silently
   dropped the properties those later passes actually control.

   Toggle switch and tooltip box were already single, undisputed
   definitions in PvS_style.css — copied verbatim. Everything else
   (button strip, PDF button, Learn & Reference) required consolidating
   2-3 competing/scattered declarations per selector down to one.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Toggle switch ── */
.toggle-row {
  display: flex; align-items: center; justify-content: space-between;
  padding: 5px 0;
}
.toggle-label { font-size: .72rem; color: var(--text-secondary); font-weight: 500; }
/* label.toggle (not bare .toggle) deliberately: tepuy.css has its own,
   unrelated ".toggle { background:#ccc; ... }" for a bare "<input
   class='toggle'>" control. PvS's markup puts the "toggle" class on the
   wrapping <label>, which tepuy.css's bare selector also matches — the
   extra specificity here is load-bearing, not decorative. */
label.toggle {
  position: relative; width: 40px; height: 22px; flex-shrink: 0;
  background: transparent; border-radius: 0; margin-right: 0;
}
label.toggle::after { content: none; }
.toggle input { opacity: 0; width: 0; height: 0; }
.toggle-slider {
  position: absolute; inset: 0;
  background: #8f8f8f;
  border-radius: 99px; cursor: pointer; transition: var(--transition);
}
.toggle-slider::before {
  content: '';
  position: absolute; width: 18px; height: 18px; left: 2px; top: 2px;
  background: #ffffff; border-radius: 50%; transition: var(--transition);
  box-shadow: 0 1px 3px rgba(0,0,0,.25);
}
.toggle input:checked + .toggle-slider { background: var(--gold); }
.toggle input:checked + .toggle-slider::before {
  transform: translateX(18px);
}

/* ── Tooltip box (JS-positioned, position:fixed — js/pvs-tooltip.js) ── */
.info-tip {
  display: inline-block;
  width: 14px;
  height: 14px;
  line-height: 14px;
  text-align: center;
  border-radius: 50%;
  font-size: .65rem;
  background: rgba(201,168,76,.15);
  color: var(--gold-text);
  cursor: help;
  position: relative;
  vertical-align: middle;
  margin-left: 2px;
}
.info-tip-popup {
  display: none;
  position: fixed;
  max-width: 260px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 10px 12px;
  font-size: .69rem;
  line-height: 1.6;
  color: var(--text-secondary);
  z-index: 1500;
  box-shadow: var(--shadow-lg);
  white-space: normal;
  pointer-events: none;
}

/* ── Export/Print/Reset/Save/Load button strip ──
   font-family: inherit on all three <button> rules below (2026-08 full
   font audit) — NOT redundant, despite computing to the same "Inter
   Tight", ... stack body already has: browsers don't cascade a page's
   font into form controls (button/input/select/textarea) through
   normal inheritance, only through the UA stylesheet's own default
   (confirmed live: a bare unstyled <button> on this page computes to
   plain "Arial", not body's font). Every one of these rules used to
   hardcode font-family: 'Inter Tight', sans-serif — a truncated copy
   of body's actual stack — which accidentally also fixed the button
   font-inheritance gap as a side effect. The first pass of this same
   audit mistakenly treated that as pure redundant duplication (same
   pattern as the non-button fixes elsewhere in this file) and removed
   it outright, which silently regressed these buttons to the browser's
   own "Arial" default — caught by re-running the computed-style check
   after the fact, not assumed. font-family: inherit keeps the fix
   (forces the real inheritance these elements don't get by default)
   without hardcoding a second copy of body's stack to drift from. */
.btn-calculate {
  width: 100%; margin-top: 14px;
  padding: 12px;
  background: linear-gradient(135deg, var(--gold-dim), var(--gold));
  border: none; border-radius: var(--radius-md);
  color: #0d0f14;
  font-family: inherit;
  font-size: .88rem; font-weight: 700;
  letter-spacing: .05em; text-transform: uppercase;
  cursor: pointer; transition: var(--transition);
  box-shadow: var(--shadow-gold);
}
.btn-calculate:hover {
  background: linear-gradient(135deg, var(--gold), var(--gold-light));
  transform: translateY(-1px);
  box-shadow: 0 3px 10px rgba(184,146,46,.35);
}
.btn-calculate:active { transform: translateY(0); }
.btn-secondary {
  flex: 1; padding: 8px 12px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  font-family: inherit;
  font-size: .72rem; font-weight: 500;
  cursor: pointer; transition: var(--transition);
}
.btn-secondary:hover { background: var(--bg-hover); border-color: var(--gold-dim); color: var(--gold-text); }
.btn-row { display: flex; gap: 8px; margin-top: 8px; }
.export-bar { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
.btn-export {
  display: flex; align-items: center; gap: 5px;
  padding: 7px 12px;
  background: var(--bg-surface); border: 1px solid var(--border);
  border-radius: var(--radius-sm); color: var(--text-secondary);
  font-family: inherit;
  font-size: .68rem; font-weight: 600;
  letter-spacing: .03em; text-transform: uppercase;
  cursor: pointer; transition: var(--transition);
}
.btn-export:hover { background: var(--bg-hover); border-color: var(--gold-dim); color: var(--gold-text); }
.btn-scenario {
  font-size: .68rem;
  border-color: var(--border-focus);
  color: var(--gold-text);
}
.btn-scenario:hover {
  background: rgba(201,168,76,.08);
  border-color: var(--gold);
  color: var(--gold-light);
}
.scenario-auth-hint {
  font-size: .66rem;
  padding: 5px 8px;
  border-radius: var(--radius-sm);
  background: var(--bg-raised);
  margin-top: 4px;
  line-height: 1.5;
  animation: fadeIn .2s ease;
}
.scenario-load-dropdown {
  background: var(--bg-surface);
  border: 1px solid var(--border-focus);
  border-radius: var(--radius-md);
  margin-bottom: 12px;
  overflow: hidden;
  box-shadow: var(--shadow-md);
  /* flex-shrink: 0 (fix-scenario-save-load-followup-bugs, 2026-08-30) — this
     is a flex item of .sidebar-inner (PvS's sidebar-scroll/footer flex
     column). A flex item with overflow other than visible gets an automatic
     minimum main-size of 0 (not its content size) per the flexbox spec, so
     under real space pressure from .sidebar-scroll's accordion content this
     collapsed to ~the header row's height, clipping .sld-list beneath it —
     Load always looked empty even though the item list was correctly in the
     DOM. flex-shrink: 0 matches the same protection .sidebar-footer already
     has in this file's own flex column, confirmed to fix the collapse. */
  flex-shrink: 0;
}
.sld-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 12px;
  background: var(--bg-raised);
  border-bottom: 1px solid var(--border);
  font-size: .68rem;
  font-weight: 700;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: .04em;
}
.sld-close {
  background: none;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  font-size: .75rem;
  padding: 0 4px;
}
.sld-close:hover { color: var(--danger); }
.sld-list {
  list-style: none;
  padding: 4px 0;
  margin: 0;
  max-height: 200px;
  overflow-y: auto;
}
.sld-item {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  font-family: inherit; /* <button> — see .btn-calculate's comment above */
  padding: 7px 14px;
  font-size: .74rem;
  color: var(--text-secondary);
  cursor: pointer;
  transition: var(--transition);
}
.sld-item:hover {
  background: var(--bg-hover);
  color: var(--gold-text);
}
@keyframes fadeIn { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; } }
.btn-share {
  border-color: var(--gold-dim, #8a6d28) !important;
  color: var(--gold, #c9a84c) !important;
}
.btn-share:hover {
  background: rgba(201,168,76,0.08) !important;
  border-color: var(--gold, #c9a84c) !important;
}
.btn-share-export {
  border-color: var(--gold-dim, #8a6d28) !important;
  color: var(--gold-text, #1a5236) !important;
}
.btn-share-export:hover {
  background: rgba(201,168,76,0.08) !important;
  border-color: var(--gold, #c9a84c) !important;
}

/* ── PDF report generation button — the ONE shared export treatment ──
   One class, five calculators, one file.

   BEFORE THIS, TWO TREATMENTS. Property vs Shares and Retirement wore
   .btn-pdf (this gold gradient, full width); LMI, Offset vs Shares and
   Mortgage wore .btn-ghost-sm, a transparent 1px ghost button forked
   VERBATIM into four page stylesheets (lmi-polish.css, offset-polish.css,
   mortgage-polish.css, retirement.css). Those four copies stay, because the
   PNG button beside each PDF button still uses them and this change is not
   about the PNG button — but no PDF button reads them any more, and no page
   stylesheet declares .btn-pdf at all. That is the constraint: the button
   strip belongs to this file.

   TWO CONTEXTS, NEITHER OF THEM PAGE-SPECIFIC. The base rule sizes the
   button to its content, which is what the .rtab-actions row next to the
   PNG button needs. The context rule below widens it to the column inside
   .sidebar-footer and .card-content, which is what the Retirement and PvS
   sidebars need. Both selectors live here, so all five pages change
   together.

   LABEL CONTRAST, AND WHY THE FILL IS FLAT. This rule used to be
   `linear-gradient(135deg, var(--gold), var(--gold-light))`. The gradient
   was itself a repair: --gold-dim is NOT consistently darker than --gold
   (css/PvS_style.css mixes it toward black, css/simulators.css sets it to
   #3db07e, which is LIGHTER than that file's #1a5236 --gold), so a
   gradient spanning --gold..--gold-dim left no single label colour valid —
   dark text failed on #1a5236 at 2.07:1, white failed on #3db07e at
   2.72:1 — and --gold-light was chosen to keep both stops inside one
   luminance band.

   A gradient is still the wrong fill here, for a reason a token audit
   cannot see: axe cannot resolve a background-image, so it reports
   color-contrast as INCOMPLETE rather than passing. Measured — extending
   this treatment to LMI, Offset vs Shares and Mortgage (whose PDF buttons
   were flat, transparent .btn-ghost-sm) added exactly one color-contrast
   incomplete node per page, and un-disabling Retirement's button added one
   more there (axe exempts aria-disabled elements from the rule entirely,
   so the old paid lock was hiding its own button from the check). A flat
   fill is resolvable, so the contrast is verified by the tool on every run
   instead of by hand once in a PR body.

   Measured off the RENDERED pages, computed styles, in both gate states:

     Property vs Shares  #0E1512 on clay rgb(201,122,58)   5.58:1
     other four          #ffffff on rgb(26,82,54)           9.11:1
     hover, PvS          #0E1512 on the light clay          8.26:1
     hover, other four   #ffffff on rgb(36,120,87)          5.39:1

   Worst case 5.39:1, on hover, so the label clears AA everywhere in both
   states. White on clay is NOT used here: it measures 3.32:1 against the
   live --clay of #C97A3A. (An earlier session recorded that pairing as
   3.95:1, which is right for #BE6A3E — the value css/PvS_style.css names
   as its FALLBACK. The live token resolves to #C97A3A, so the real figure
   is worse than the one on record, not better.) */
.btn-pdf {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  padding: 9px 16px;
  background: var(--gold);
  color: var(--gold-on, #0f1118);
  /* font-family: inherit — pre-existing gap, not introduced by this
     pass: this <button> never had ANY font-family rule (moved here
     from PvS_style.css in an earlier session with the property
     apparently dropped along the way), so it was rendering in
     Chromium's bare "Arial" button default the whole time. Caught by
     re-scanning every <button> on the page after this font audit's
     other fixes, not by the original per-category checklist. */
  font-family: inherit;
  font-weight: 700;
  font-size: .78rem;
  letter-spacing: 0.02em;
  line-height: 1.2;
  white-space: nowrap;
  border: none;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: var(--transition);
  box-shadow: 0 2px 8px rgba(201,168,76,.3);
}
.btn-pdf svg { flex-shrink: 0; }
.btn-pdf:hover {
  /* --gold-light, never --gold-dim: --gold-dim is #3db07e in
     css/simulators.css, where the --gold-on label is white — 2.72:1, a
     fail. --gold-light holds the pairing at 5.39:1 there and 8.26:1 on
     Property vs Shares, both measured on the rendered page. No
     brightness() filter: a filter on the element brightens the LABEL as
     well as the fill, which moves a ratio this rule is meant to pin. */
  background: var(--gold-light);
  box-shadow: 0 4px 16px rgba(201,168,76,.45);
  transform: translateY(-1px);
}
.btn-pdf:focus-visible {
  outline: 2px solid var(--gold-text, #1a5236);
  outline-offset: 2px;
}

/* Sidebar context: fill the column, and stand clear of the primary
   Calculate button above it. Retirement's PDF button used to sit flush
   against Calculate with zero space between them — both children of a
   plain <div id="advFooterActions">, which contributes no gap of its own
   however the .sidebar-footer flex column is spaced. */
.sidebar-footer .btn-pdf,
.card-content .btn-pdf {
  width: 100%;
}
.sidebar-footer .btn-pdf {
  margin-top: 0.5rem;
}

/* ── Sign-in gate state (js/signin-gate.js) ──
   PDF export is free and needs only an account, so the gated button is
   NOT disabled, NOT dimmed and NOT a paid-tier lock: it stays fully
   legible, keeps its own contrast, and takes only a padlock affordance.
   The paid-tier treatment above ([data-pro-locked]) drops opacity to
   0.55, which is exactly what made Retirement's locked PDF label
   unreadable — the whole point of this state is that it does not do
   that. */
[data-signin-gated="true"]::after {
  content: " 🔒";
  font-size: 0.7em;
}

/* Phone: the label is the affordance, so it is kept — only the padding
   tightens. (The PNG button beside it drops its label via css/tepuy.css's
   `.rtab-actions .btn-ghost-sm span` rule; a PDF button with no label and
   a padlock would read as a paid lock, which is the confusion this whole
   change removes.) */
@media (max-width: 560px) {
  .rtab-actions .btn-pdf {
    padding: 8px 11px;
    font-size: .74rem;
  }
}

/* ── "Learn & Reference" section ──────────────────────────────────────────
   2026-09: PvS and Retirement converted from this tabbed .edu-section/
   .edu-tabs/.edu-panel component to the long-form .calc-explainer pattern
   Mortgage/LMI/Offset already used (see css/calculators.css's own
   ".calc-explainer .worked-example" rules below) — content now renders
   unconditionally in document order, with a real anchor-nav
   (.explainer-jump-nav) replacing the tab bar. That was the motivating bug:
   a tabbed panel hides everything not on the active tab from in-page
   search (Ctrl+F) and from a screen reader's document-order reading, for
   content that is reference material meant to be searchable.
   The .edu-tabs/.edu-tab/.edu-panel/.edu-header/.edu-table/.edu-glossary/
   .edu-faq-item rules this comment used to sit above are gone with it —
   PvS and Retirement were their only two consumers (confirmed by grep
   across every .html file before removal), so nothing else was relying on
   them. .edu-links-grid/.edu-link-list below are UNRELATED to the tab
   mechanism (pure two-column layout for the Resources section) and both
   pages still use them, so they stay. */
/* 2026-09 restyle: the pill-button-in-a-bordered-strip treatment below read
   as a tab row even with the tab mechanism gone (confirmed live: 0
   .edu-tabs, 0 hidden panels — this was a pure styling complaint, not a
   functional regression). Now a plain link list — no background/border box
   around the row, no per-link pill/padding — separated by middot
   characters instead. Underline only on hover/focus, so the resting state
   reads as "reference index," not "buttons," while still affording that
   each word is a link. */
/* WHICH .calc-explainer SECTIONS GET A .explainer-jump-nav — 2026-09.
   Learn & Reference sections over ~2,000 words get a jump-nav; shorter
   ones don't need one to stay navigable by scrolling alone. Measured word
   counts at the time each page's nav was added/considered:
     PvS                3,655 words  — has a jump-nav (#315)
     Offset vs Shares    2,715 words  — has a jump-nav (this change)
     Retirement          2,304 words  — has a jump-nav (#315)
     LMI                 1,952 words  — no jump-nav (below the threshold)
     Mortgage              946 words  — no jump-nav (below the threshold)
   PvS and Retirement got theirs first only because of which pages #315
   happened to touch, not a length-based decision — Offset vs Shares was
   the one page left above the threshold without one, so this change is
   that decision applied consistently rather than a new one. Re-measure
   before adding a nav to a sixth calculator or a rewrite that changes
   these word counts materially, rather than assuming the numbers above
   still hold. */
.explainer-jump-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 0;
  margin: 0 0 1.75rem;
}
.explainer-jump-nav a {
  font-family: inherit;
  font-size: .85rem;
  font-weight: 500;
  color: var(--gold-text, var(--gold));
  text-decoration: none;
  padding: 2px 0;
  transition: var(--transition);
}
/* Middot separators between links, not after the last one. The pseudo-
   element carries its own color/no-underline so it never picks up the
   link's hover underline. */
.explainer-jump-nav a:not(:last-child)::after {
  content: '·';
  display: inline-block;
  margin: 0 10px;
  color: var(--border);
  text-decoration: none;
  font-weight: 400;
}
.explainer-jump-nav a:hover,
.explainer-jump-nav a:focus-visible {
  color: var(--gold-light, var(--gold));
  text-decoration: underline;
  text-underline-offset: 3px;
}
.edu-links-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 24px;
}
@container pvs-layout (max-width: 700px) { .edu-links-grid { grid-template-columns: 1fr; } }
.edu-link-list {
  list-style: none;
  padding: 0;
  margin: 6px 0;
}
.edu-link-list li {
  font-size: .73rem;
  color: var(--text-secondary);
  padding: 5px 0;
  border-bottom: 1px solid var(--border);
  line-height: 1.5;
}
.edu-link-list a {
  color: var(--gold-text);
  text-decoration: none;
}
.edu-link-list a:hover { text-decoration: underline; color: var(--gold-light); }

/* ── Combined trial-usage summary (Retirement Planner) ────────────────────
   Populated by TC.renderTrialUsageSummary() in js/calc-pro.js. Same palette
   as .pro-banner (tepuy.css) for visual consistency with the per-feature
   lock banners it sits above. Hidden via the `hidden` attribute when not
   applicable (signed out / Pro) — see calc-pro.js for that logic. */
.usage-summary-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
  background: #f4fbf7;
  color: #093;
  padding: 0.6rem 0.9rem;
  border-radius: 8px;
  font-size: 0.82rem;
  margin-bottom: 0.75rem;
}
.usage-summary-bar strong { font-weight: 700; }
.usage-summary-bar .usage-summary-upgrade {
  color: #093;
  font-weight: 700;
  text-decoration: underline;
  white-space: nowrap;
}

/* ─── .calc-explainer typography ─────────────────────────────────────────
   2026-09: added after a live measurement found PvS's .calc-explainer h2/p
   computing to margin: 0 exactly, while Mortgage/LMI/Offset/Retirement
   showed the browser's unstyled UA-stylesheet defaults (~19.92px/16px) —
   nobody had ever given .calc-explainer its own heading/paragraph spacing,
   so all five calculators were one accidental override away from this
   exact bug, and PvS already had one: PvS_style.css:170's
   `*, *::before, *::after { margin: 0; padding: 0; }` reset (the only
   margin rule of any kind reaching these elements — confirmed by CSSOM
   search across every stylesheet PvS loads) zeroes everything with no
   .calc-explainer-scoped rule to win the cascade back. These rules fix
   PvS by being MORE specific than that universal reset, and stop the
   other four from depending on UA defaults a future reset/normalize
   addition could just as easily zero out the same way. */
.calc-explainer {
  /* Independent of the surrounding column's width (body.calc .main-layout
     drops the sitewide 1200px cap for the full-width calculator shell —
     see tepuy.css's "full-width shell" comment — so this section's text
     was otherwise uncapped, measuring ~123ch/line at 1440px). 46rem sits
     inside the 42–48rem / ~75–85ch range typically recommended for long-
     form reading, independent of how wide the results column itself is. */
  max-width: 46rem;
}
.calc-explainer h2 {
  margin: 2.5rem 0 1rem;
  font-size: 1.5rem;
  font-weight: 700;
  color: var(--text-primary);
}
.calc-explainer h3 {
  margin: 1.75rem 0 0.75rem;
  font-size: 1.17rem;
  /* 600, not 700, and a distinct accent color rather than h2's ink — a
     reader scanning past the size difference alone (24px vs 18.72px, a
     1.28x ratio) still gets a colour/weight cue that this is a step down
     in the hierarchy, not just a smaller instance of the same heading. */
  font-weight: 600;
  color: var(--gold-text, var(--gold));
}
.calc-explainer p,
.calc-explainer ul,
.calc-explainer ol {
  margin: 0 0 1rem;
}
.calc-explainer li {
  margin-bottom: 0.6rem;
}
.calc-explainer li:last-child {
  margin-bottom: 0;
}

/* ─── Worked-example tables ──────────────────────────────────────────────
   Added 2026-09-05 with the Offset vs Shares and LMI worked examples
   (stream Q). Shared here rather than inlined on each page because both
   already load this file last, and a third calculator adding a worked
   example should not have to restate the styling.

   Deliberately quiet: this sits inside .calc-explainer, which is reference
   prose, so the table should read as part of the text rather than as a
   results panel. Colours come from the same tokens the rest of this file
   uses, so it follows the theme instead of pinning literals.

   overflow-x on the wrapper, not the table, so a narrow viewport scrolls
   the table alone and never the page body. */
.calc-explainer .worked-example {
  width: 100%;
  border-collapse: collapse;
  margin: 0.9rem 0 1.1rem;
  font-size: 0.9rem;
  font-variant-numeric: tabular-nums;
  display: block;
  overflow-x: auto;
}
.calc-explainer .worked-example caption {
  caption-side: top;
  text-align: left;
  font-size: 0.82rem;
  line-height: 1.5;
  color: var(--sb-muted, #55685F);
  padding: 0 0 0.55rem;
}
.calc-explainer .worked-example th,
.calc-explainer .worked-example td {
  text-align: left;
  padding: 0.45rem 0.7rem 0.45rem 0;
  border-bottom: 1px solid var(--border, #d6e0d8);
  vertical-align: top;
}
.calc-explainer .worked-example thead th {
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--sb-muted, #55685F);
  font-weight: 700;
}
.calc-explainer .worked-example tbody th {
  font-weight: 600;
  white-space: nowrap;
  padding-right: 1.1rem;
}
.calc-explainer .worked-example tbody tr:last-child th,
.calc-explainer .worked-example tbody tr:last-child td { border-bottom: none; }
