/* =============================================================================
   Insights — the application stylesheet
   -----------------------------------------------------------------------------
   ONE layer between the tokens and the app. It replaces two files that used to
   sit here:

     assets/css/material-dashboard.min.css   (Creative Tim, ~40k lines) — RETIRED
     InsightsAssets/bootstrap-adapter.css    (the token bridge)         — MERGED HERE

   Why the retirement was safe: Wt's WBootstrap5Theme already serves the whole of
   Bootstrap 5.3.8 from resources/themes/bootstrap/5/main.css, so Material
   Dashboard was only ever contributing (a) a duplicate copy of Bootstrap and
   (b) about forty of its own classes that the templates actually reference.
   Section 12 below reimplements exactly those forty on the design tokens; the
   other ~4,860 Material Dashboard classes were unused. The measured set is in
   DRIFT.md — it was derived by diffing every class in approot/*.xml and every
   addStyleClass()/setStyleClass() in WebFrontEnd/ + ReportsAndCharts/ against
   what Bootstrap already ships.

   Load order:  resources/.../main.css (Bootstrap, via WBootstrap5Theme)
             -> InsightsAssets/styles.css      (tokens)
             -> THIS FILE
             -> InsightsAssets/Insights-Global.css   (screen-specific, wins last)

   Two standing rules, from DECISIONS.md #15.1:
     1. This file may only map tokens onto classes the markup already emits.
        It does not invent visual behaviour.
     2. Any genuinely new class it adds gets recorded in DRIFT.md §2 until the
        design system absorbs it.

   Real Wt widgets implementing components/*.d.ts replace sections of this file
   one at a time. Nothing here needs a big-bang rewrite.
   ============================================================================= */

/* -----------------------------------------------------------------------------
   0. Bootstrap variable bridge
   Bootstrap 5.3 resolves most of its own chrome through --bs-* custom
   properties. Redefining those at :root is far cheaper — and far less brittle —
   than overriding the rules that consume them, so anything Bootstrap draws for
   itself (validation states, close buttons, list groups, pagination, progress
   bars, offcanvas, popovers) inherits the design system without a rule here.
   ----------------------------------------------------------------------------- */
:root{
  --bs-body-bg:var(--canvas);
  --bs-body-color:var(--text-body);
  --bs-body-font-family:var(--font-body);
  --bs-body-font-size:var(--fs-body);
  --bs-emphasis-color:var(--text-primary);
  --bs-secondary-color:var(--text-muted);
  --bs-tertiary-color:var(--text-faint);
  --bs-secondary-bg:var(--surface-raised);
  --bs-tertiary-bg:var(--surface-inset);

  --bs-border-color:var(--border-subtle);
  --bs-border-color-translucent:var(--border-subtle);
  --bs-border-radius:var(--radius-md);
  --bs-border-radius-sm:var(--radius-sm);
  --bs-border-radius-lg:var(--radius-lg);
  --bs-border-radius-xl:var(--radius-xl);
  --bs-border-radius-pill:var(--radius-pill);

  --bs-primary:var(--teal-500);
  --bs-primary-rgb:23,179,166;
  --bs-link-color:var(--text-accent);
  --bs-link-hover-color:var(--action-hover);
  --bs-link-color-rgb:62,203,190;

  --bs-heading-color:var(--text-primary);
  --bs-highlight-bg:var(--signal-warn-bg);
  --bs-code-color:var(--text-accent);

  /* Focus is one ring everywhere — never Bootstrap's blue halo. */
  --bs-focus-ring-color:var(--focus-ring);
  --bs-focus-ring-width:3px;

  /* The left rail. DECISIONS.md #15.4: the rail widens rather than truncating a
     destination name, and translated labels are the case that forces it. 236px
     is the design system's figure; it is a variable so density work can tune it
     in one place. */
  /* Foreground for an amber fill. tokens/colors.css defines --action-fg for the
     teal fill but has no amber counterpart; this is the same value the adapter
     used, named once instead of repeated at each call site. Promote it into
     tokens/colors.css the next time the design system is re-synced. */
  --value-fg:#3A2405;

  /* Zebra striping for dense rows. A white wash on dark, an ink wash on light —
     it has to flip, and there is no token for it in tokens/ yet. */
  --row-stripe:rgba(255,255,255,.045);

  --rail-width:200px;
  --rail-gap:0px;
  --topbar-height:64px;
}
[data-theme="light"],.theme-light{
  --row-stripe:rgba(16,26,34,.035);
}

/* =============================================================================
   1. Application shell — the rail, the top bar, the content column
   Previously supplied by Material Dashboard's .sidenav / .navbar-vertical /
   .main-content rules plus a partial copy of them in Insights-Global.css. This
   is the whole layout, in one place, on tokens.

   DECISIONS.md #4 (rail keeps destinations, a slim top bar takes identity and
   session) is a TEMPLATE change, not a CSS one — the markup in
   approot/Insights-Main-Page-And-Footer.xml still has both in the rail. The
   .frs-topbar hooks below are here so that restructure has somewhere to land.
   ============================================================================= */
main.main-content,
.sidenav{
  transition:margin var(--dur-base) var(--ease-standard),
             transform var(--dur-base) var(--ease-standard),
             max-width var(--dur-base) var(--ease-standard);
}

.sidenav{
  z-index:1030;
  background:var(--surface) !important;   /* beats .bg-white in the template */
  border-right:1px solid var(--border-subtle);
  box-shadow:none;
  color:var(--text-body);
}
.sidenav .navbar-brand,
.sidenav .navbar-heading{display:block}

/* The rail proper. The template says
   `navbar-vertical navbar-expand-xs fixed-start`, so only the -xs breakpoint
   variant is implemented — the -sm/-md/-lg/-xl/-xxl copies Material Dashboard
   shipped were never referenced by any template. */
.navbar-vertical.navbar-expand-xs{
  display:block;
  position:fixed;
  top:var(--topbar-height);
  bottom:0;
  width:100%;
  max-width:var(--rail-width);
  overflow-y:auto;
  padding:0;
  box-shadow:none;
}
.navbar-vertical.navbar-expand-xs.fixed-start{left:0}
.navbar-vertical.navbar-expand-xs.fixed-end{right:0}
/* The rail used to reserve 200px for a brand/session header. That header is now
   the top bar, so the destination list owns the full height below it. */
.navbar-vertical.navbar-expand-xs .navbar-collapse{
  display:block;
  overflow:auto;
  height:calc(100vh - var(--topbar-height));
  padding-top:var(--space-3);
}
.navbar-vertical.navbar-expand-xs>[class*=container]{
  flex-direction:column;
  align-items:stretch;
  min-height:100%;
  padding-left:0;
  padding-right:0;
}

.sidenav-header{
  border-bottom:1px solid var(--border-subtle);
  padding-bottom:var(--space-3);
}
.navbar-vertical .navbar-brand>img,
.navbar-vertical .navbar-brand-img{max-width:100%;max-height:3.5rem}
.navbar-brand,
.navbar-brand span,
.navbar-brand div{color:var(--text-primary);font-family:var(--font-heading)}

/* Destination rows. Labels wrap rather than truncate — #15.4 again. */
.navbar-vertical .navbar-nav .nav-link{
  display:flex;
  align-items:center;
  gap:var(--space-2);
  padding:var(--space-2) var(--space-3);
  margin:1.5px var(--space-2);
  border-radius:var(--radius-md);
  font-family:var(--font-body);
  font-size:var(--fs-body-sm);
  font-weight:var(--fw-medium);
  color:var(--text-body);
  white-space:normal;
  transition:var(--transition-control);
}
.navbar-vertical .navbar-nav .nav-link>i{min-width:1rem;font-size:1.125rem;text-align:center}
.navbar-vertical .navbar-nav .nav-item{width:100%}
.navbar-vertical .navbar-nav>.nav-item{margin-top:.125rem}
.navbar-vertical .navbar-nav .nav-link .nav-link-text,
.navbar-vertical .navbar-nav .nav-link i{pointer-events:none}

.sidenav .nav-link:hover,
.navbar-vertical .nav-item:hover>.nav-link{
  background:var(--action-subtle);
  color:var(--text-primary);
}
.sidenav .nav-link.active,
.navbar-vertical .navbar-nav>.nav-item .nav-link.active{
  background:var(--action-subtle);
  color:var(--text-accent);
  font-weight:var(--fw-semibold);
  box-shadow:none;
}
/* Second-level (collapsed sub-nav) rows sit back a step. */
.navbar-vertical .navbar-nav .nav-item .collapse .nav .nav-item .nav-link,
.navbar-vertical .navbar-nav .nav-item .collapsing .nav .nav-item .nav-link{
  background:transparent;
  box-shadow:none;
  color:var(--text-muted);
  padding-left:var(--space-5);
}
.navbar-vertical .navbar-nav .nav-item .collapse .nav .nav-item .nav-link.active{color:var(--text-accent)}

.navbar-vertical .navbar-heading{
  padding:var(--space-2) var(--space-3);
  font-size:var(--fs-label);
  text-transform:uppercase;
  letter-spacing:var(--ls-label);
  color:var(--text-muted);
}

/* Disclosure chevron on collapsible rows. Material Dashboard drew this with a
   Font Awesome 5 glyph (\f107 in "Font Awesome 5 Free"); Wt ships Font Awesome
   4.7, where neither the family name nor the codepoint match — so that arrow
   has been rendering as tofu or nothing. Drawn in CSS instead: no font
   dependency, and it inherits currentColor. */
.navbar-vertical .navbar-nav .nav-link[data-bs-toggle=collapse]::after{
  content:"";
  margin-left:auto;
  width:7px;height:7px;
  border-right:1.5px solid currentColor;
  border-bottom:1.5px solid currentColor;
  transform:rotate(45deg) translate(-2px,-2px);
  opacity:.55;
  transition:transform var(--dur-fast) var(--ease-standard),opacity var(--dur-fast) var(--ease-standard);
}
.navbar-vertical .navbar-nav .nav-link[data-bs-toggle=collapse][aria-expanded=true]::after{
  transform:rotate(-135deg) translate(-2px,-2px);
  opacity:1;
}

/* The content column sits beside the rail on wide screens and under a slide-out
   rail below 1200px, which is the breakpoint the template's `d-xl-none` menu
   button already assumes. */
/* Scoped to the <main> element deliberately. `main-content` is a Material
   Dashboard LAYOUT class and belongs to the app shell alone, but it is an
   inviting name and the project card grids had picked it up — so giving the
   bare class the shell's padding-top put a second 64px gap above the first card
   on the Projects screen. Those call sites now use
   .insights-main-content-projects (ProjectNavigator.cpp), which is the grid.
   The element qualifier stays anyway: the shell is the only <main> in the
   document, so it keeps this rule from ever reaching an inner container again. */
main.main-content{min-height:100vh;background:var(--canvas);padding-top:var(--topbar-height)}
@media (min-width:1200px){
  .sidenav.fixed-start+main.main-content{margin-left:var(--rail-width)}
  .sidenav.fixed-end+main.main-content{margin-right:var(--rail-width)}
}
@media (max-width:1199.98px){
  .sidenav{z-index:1035}
  .g-sidenav-show:not(.rtl) .sidenav{transform:translateX(calc(-1 * var(--rail-width)))}
  .g-sidenav-show.rtl .sidenav{transform:translateX(var(--rail-width))}
  .g-sidenav-show .sidenav.fixed-start+main.main-content{margin-left:0 !important}
  .g-sidenav-show.g-sidenav-pinned .sidenav{transform:translateX(0)}
}
.g-sidenav-show .sidenav .nav-item .collapse{height:auto}

/* --- Rail width states -------------------------------------------------------
   data-rail on <html>, written by ApplicationInit::setRailState().

   The whole mechanism is redefining ONE token. --rail-width is read by the rail's
   own max-width (§1 above) and by main.main-content's margin, and both already
   carry a transition, so the rail and the content move together and animated
   without either rule being touched.

   Below 1200px this is inert by design: there the rail is already an overlay
   driven by g-sidenav-pinned, and the top-bar control is hidden. The one thing
   the token still does at that width is feed the off-screen translate, which
   stays correct at every value. */
:root[data-rail="icons"]{--rail-width:64px}
:root[data-rail="hidden"]{--rail-width:0px}

/* Icons only. Everything in a nav row that is not the glyph goes, including the
   collapse chevron, and the row centres on what is left. */
:root[data-rail="icons"] .navbar-vertical .navbar-nav .nav-link{
  justify-content:center;
  padding-inline:0;
}
:root[data-rail="icons"] .navbar-vertical .navbar-nav .nav-link>:not(i){display:none}
:root[data-rail="icons"] .navbar-vertical .navbar-nav .nav-link::after{display:none}
:root[data-rail="icons"] .navbar-vertical .navbar-nav .nav-link>i{
  margin-inline:0 !important;
  min-width:auto;
}
:root[data-rail="icons"] .sidenav{overflow-x:hidden}

/* Gone. max-width is already 0 from the token; overflow and the border are what
   stop a 1px seam and a scrollbar surviving the collapse. */
:root[data-rail="hidden"] .sidenav{
  overflow:hidden;
  border-right-width:0;
}

.frs-topbar__rail-toggle{align-items:center}

/* --- The run band -----------------------------------------------------------
   What the next run will be, beside what the last run produced. Two panels rather
   than a col-3 / col-9 split, because the boundary between them is the question
   each answers, not a grid gutter.

   320px is the settings column's natural width - the widest of its label/value
   rows plus the Train button - and it stops being a column below 1100px, where a
   third of the width is no longer enough for either half. */
.frs-runband{
  display:grid;
  grid-template-columns:320px minmax(0,1fr);
  gap:var(--space-4);
  /* Stretch, and the FIT panel is deliberately the taller of the two so that the
     slack lands in the settings column rather than under the chart. Highcharts
     sizes itself once and does not reflow when a CSS box grows underneath it, so
     any height handed to the fit panel beyond what the chart already took became
     a blank half-card - which is why the chart's own height went up rather than
     this going to `start`. A settings card with room under its last row reads as
     a column; a chart card with a hole under its legend reads as broken. */
  align-items:stretch;
  margin-bottom:var(--space-4);
}

/* Run settings holds still while the column cards scroll past it.
   The settings are what you are editing; the cards are what you are editing them
   against, and there are enough of them to scroll for. Sticky needs the panel to
   keep its natural height - a stretched grid item is already as tall as its row
   and has nothing to stick within - so this opts out of the stretch above. The
   slack still lands in this column, now as space below the card rather than
   inside it.
   max-height/overflow keeps it usable when the settings themselves are taller
   than the viewport: it scrolls internally rather than having its bottom cut off. */
.frs-panel--settings{
  position:sticky;
  top:calc(var(--topbar-height) + var(--space-3));
  align-self:start;
  max-height:calc(100vh - var(--topbar-height) - var(--space-5));
  overflow-y:auto;
}
@media (max-width:1100px){
  /* One column below this width - the panel is stacked ABOVE the cards, so
     sticking it would park it over the content it is meant to sit beside. */
  .frs-panel--settings{position:static;max-height:none;overflow-y:visible}
}
@media (max-width:1100px){
  .frs-runband{grid-template-columns:minmax(0,1fr)}
}

.POfrs-panel{
  background:var(--surface-card,var(--surface));
  border:1px solid var(--border-subtle);
  border-radius:var(--radius-card);
  box-shadow:var(--shadow-card);
  padding:var(--space-4);
  margin-bottom:var(--space-4);
  min-width:0;
}
.frs-panel__head{
  display:flex;
  align-items:center;
  gap:var(--space-3);
  flex-wrap:wrap;
  margin-bottom:var(--space-3);
}
.frs-panel__title{
  font-family:var(--font-body);
  font-size:var(--fs-label);
  font-weight:var(--fw-semibold);
  letter-spacing:var(--ls-label);
  text-transform:uppercase;
  color:var(--text-muted);
}
.frs-panel__spacer{flex:1}
.frs-panel--fit{display:flex;flex-direction:column}
.frs-runband__actions{
  display:flex;
  flex-wrap:wrap;
  gap:var(--space-2);
  margin-top:var(--space-3);
  padding-top:var(--space-3);
  border-top:1px solid var(--border-subtle);
}

/* The version on screen, named where the fit is - and, beside it, which version
   the rest of the platform will actually use. They are routinely different. */
.frs-fit__version-value{
  font-family:var(--font-figure);
  font-variant-numeric:tabular-nums;
  font-size:var(--fs-h4);
  font-weight:var(--fw-semibold);
  color:var(--text-accent);
}
.frs-trophy{
  display:inline-flex;
  align-items:center;
  gap:4px;
  padding:1px 8px;
  border-radius:var(--radius-pill);
  border:1px solid var(--border);
  font-size:var(--fs-caption);
  white-space:nowrap;
}
.frs-trophy i{font-size:15px;line-height:1}
/* This IS the one Simul./Opt. uses. */
.frs-trophy--is{border-color:var(--signal-warn);background:var(--signal-warn-bg);color:var(--signal-warn)}
/* It is NOT - so say which one is, because that is the model the rest of the
   platform will run on no matter what is drawn here. */
.frs-trophy--other{border-color:var(--border-strong);background:var(--surface-inset);color:var(--text-muted)}
.frs-trophy--none{border-style:dashed;color:var(--text-faint)}

/* Was .chart-container, which is width:60vw - a viewport measurement inside a grid
   cell that is not the viewport. At 1365px that is 819px of chart in a 740px panel,
   and the legend was clipped off the right edge. The panel decides the width now. */
.frs-fit__chart{
  width:100%;
  min-width:0;
  /* No height here - TrainingScatterPlot sets its own through Wt's layout, which
     is the only way Highcharts actually redraws at a new size. A height on this
     box would just be a taller box around an unchanged chart. */
  position:relative;
}
.frs-fit__status{padding-top:var(--space-3)}

/* --- The three figures ------------------------------------------------------
   How well does it fit, is that fit real, and by how much is it wrong on weeks it
   never saw - in that order, because that is the order the question is asked. */
.frs-stats{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(150px,1fr));
  gap:var(--space-3);
  margin-bottom:var(--space-4);
}
.frs-stat{
  display:flex;
  flex-direction:column;
  gap:2px;
  padding:var(--space-3);
  border:1px solid var(--border-subtle);
  border-radius:var(--radius-sm);
  background:var(--surface-inset);
  min-width:0;
}
.frs-stat__label{
  font-size:var(--fs-caption);
  letter-spacing:var(--ls-label);
  text-transform:uppercase;
  color:var(--text-muted);
}
.frs-stat__value{
  font-family:var(--font-figure);
  font-variant-numeric:tabular-nums;
  font-size:var(--fs-metric,27px);
  font-weight:var(--fw-semibold);
  color:var(--text-primary);
  line-height:1.15;
}
.frs-stat__sub{
  font-family:var(--font-figure);
  font-variant-numeric:tabular-nums;
  font-size:var(--fs-caption);
  color:var(--text-faint);
}

/* --- The experiment log -----------------------------------------------------
   A version list is only a list of scores until it says what was different about
   each one. The chips are derived from the configuration every version already
   stored, so "logging a column and unlogging it" leaves no trace - the same rule
   the pending banner follows, and the same code. */
.frs-log__head{display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-2)}
.frs-log__spacer{flex:1}
.frs-log__hint{font-size:var(--fs-caption);color:var(--text-faint)}
.frs-log__changes{display:flex;flex-wrap:wrap;gap:4px;margin-top:2px}
.frs-log__chip{
  display:inline-block;
  padding:0 6px;
  border-radius:var(--radius-pill);
  background:var(--surface-inset);
  border:1px solid var(--border-subtle);
  color:var(--text-muted);
  font-size:var(--fs-caption);
  line-height:1.6;
  max-width:100%;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}
.frs-log__chip--more{border-style:dashed}
/* THE TOPOLOGY IS AN IDENTIFIER, NOT A CHANGE. Every other chip on the row names
   something the modeller did between this version and the last one; the shape names
   what this version IS, and it is present on every row including the first. Monospace
   so the four numbers line up down the column and can be scanned vertically, and a
   flat background with no border so it reads as a label rather than as one more edit. */
.frs-log__chip--shape{
  font-family:var(--font-mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace);
  background:transparent;
  border-color:transparent;
  color:var(--text-faint);
  padding-left:0;
  font-variant-numeric:tabular-nums;
}
/* Not a change, and not nothing: the same settings solved again. Any difference in
   R² between two such versions is the optimiser, not the modeller. */
.frs-log__same{font-size:var(--fs-caption);color:var(--text-faint);font-style:italic}
.frs-log__first{font-size:var(--fs-caption);color:var(--text-faint)}

/* THE EXPERIMENT LOG AS A TABLE.
   Every row lays out on the same explicit grid, so figures line up down a column
   even though each row is its own widget. The elastic track is "what changed" -
   the only cell whose length cannot be known in advance - and everything else is
   sized to its content so a long chip list never pushes the metrics off the end. */
.frs-log__scroll{
  overflow-x:auto;
  /* Five versions before it scrolls. Was four, which in practice showed as about
     3.9 because --frs-log-row is one SINGLE-LINE row and a version whose chip list
     wraps is taller - so the fifth row was always half-cut and looked like a
     rendering fault rather than a scroll hint. Five is also the number of runs it
     takes to see a trend rather than a pair. Expressed as head + n rows rather
     than a flat pixel figure so the intent survives a change to the row padding. */
  --frs-log-rows:5;
  --frs-log-row:47px;
  --frs-log-head:34px;
  max-height:calc(var(--frs-log-head) + var(--frs-log-rows) * var(--frs-log-row));
  overflow-y:auto;
}
.frs-log__row{
  display:grid;
  /* COLUMN ORDER IS BY EVIDENTIAL WEIGHT, left to right, reordered 2026-09-07.
     The old order was historical - R-Sq first because it was the first metric the
     platform had - and it put the two least trustworthy numbers where the eye lands.
     What this session measured, across nine regions and 55 candidate models:
       - CV MAPE / CV R2 are the only figures taken on rows nothing in their own
         construction was fitted to (~118 of them), and CV MAPE is what the shape
         sweep ranks on. They lead.
       - Score and Cover judge the forecast band, and only mean anything together.
         Score changed the West Palm Beach recommendation when it was added.
       - MAPE HO / R2 HO are honest but rest on ~13 rows, and rank models inversely
         to CV (mean Spearman -0.106 over the nine regions).
       - MAPE / R2 are in-sample, so they measure memory. 44 of 55 swept candidates
         reported a training R2 between 0.850 and 0.855 because that is where
         kDefaultRsqAutoStop halts the run - a number four fifths of models share by
         construction cannot separate them. They go last.
     TROPHY MOVED TO COLUMN 0. It sat next to Archive, which is a destructive control
     with an adjacent hit target - Mark, 2026-09-07: "I could see someone accidentally
     hitting the archive button instead of the trophy button."
     The gap column is gone; its overtraining signal is now the COLOUR of R2 HO, since
     both numbers it was derived from are on the same row and the arithmetic is
     mental. */
  grid-template-columns:
    44px            /* trophy - first, and far from archive */
    64px            /* version */
    minmax(160px,1fr) /* what changed - the 1fr track, so it takes all spare width */
    76px 70px       /* CV MAPE, CV R2 - out-of-fold, ~118 rows */
    68px 72px       /* Score, Cover - the forecast band */
    76px 70px       /* MAPE HO, R2 HO - out-of-sample, ~13 rows */
    68px 62px       /* MAPE, R2 - in-sample */
    72px 96px       /* trained by, when - provenance, not evidence */
    44px;           /* archive */
  gap:var(--space-2);
  align-items:center;
  /* 1042px of tracks plus 13 gaps at 8px. NARROWER than the 1154px this row measured
     before the CV columns existed, despite carrying two more of them - dropping the
     gap column and tightening the numerics paid for both. */
  min-width:1146px;
  padding:var(--space-2) var(--space-1);
  border-bottom:1px solid var(--border-subtle);
}
.frs-log__row:last-child{border-bottom:0}
/* WHICH VERSION AM I LOOKING AT. The charts below the log show one version at a
   time and said so only in the header at the very top of the page, so reading a
   bar chart meant scrolling up to find out whose bars they were.
   Deliberately a TINT plus a rail rather than a strong fill: the row still has to
   be read as one of a list of comparable runs, and the trophy radio in the same
   row is a different state that must stay distinguishable from this one. The rail
   is what survives at a glance; the tint is what finds it when scrolling. */
.frs-log__row--loaded{
  background:var(--action-subtle,var(--surface-raised));
  box-shadow:inset 3px 0 0 0 var(--action);
}
.frs-log__row--loaded .frs-log__version{
  border-color:var(--action);
  color:var(--text-accent);
  font-weight:var(--fw-bold,700);
}
.frs-log__row--head{
  position:sticky;
  top:0;
  z-index:1;
  background:var(--surface-card,var(--surface));
  /* Density exception: a column header is a label you read once, not prose. */
  font-size:var(--fs-label);
  letter-spacing:.04em;
  text-transform:uppercase;
  color:var(--text-faint);
  border-bottom:1px solid var(--border-strong);
}
/* EVERY CELL CENTRED - Mark, 2026-09-07. The numeric columns were right-aligned, which
   is the usual choice for figures because it lines up the decimal point. It earns that
   here only if the values differ in width, and they do not: every metric in this row is
   a percentage of two or three significant figures set in tabular-nums, so the column
   scans just as cleanly centred and the row reads as one object rather than as a ragged
   left half and a ragged right half. */
.frs-log__cell{
  font-size:var(--fs-body-sm);
  color:var(--text-body);
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
  text-align:center;
}
.frs-log__cell--num{
  font-family:var(--font-figure);
  font-variant-numeric:tabular-nums;
}
.frs-log__cell--icon{text-align:center}
/* The trophy radio and the archive button are 44px tracks and their controls must not
   stretch to fill them - a WPushButton left to its own devices takes the whole cell,
   which is how the archive control came to render as a full-width bordered box when the
   row wrapped on 2026-09-07. */
.frs-log__trophy,.frs-log__archive{width:44px;justify-self:center}
/* The version pill and the change-chip list are grid ITEMS rather than runs of text, so
   text-align does not place them - justify-self does. Without this the pill sits hard
   left in its track while every figure beside it is centred. */
.frs-log__version{justify-self:center}
/* .frs-log__changes is display:flex (see the rule further up), so text-align does not
   place its chips - justify-content does. It also carries the shape line as a text node,
   which text-align does handle, so both are set. */
.frs-log__changes{justify-content:center;text-align:center}
.frs-log__cell--who,.frs-log__cell--when{color:var(--text-muted)}
/* A metric that was never measured, said as such. An empty cell reads as a bug. */
.frs-log__cell--none{color:var(--text-faint)}
.frs-log__version{
  font-family:var(--font-figure);
  font-variant-numeric:tabular-nums;
  font-weight:var(--fw-semibold);
  font-size:var(--fs-body-sm);
  padding:2px 8px;
  border-radius:var(--radius-pill,999px);
  border:1px solid var(--border-subtle);
  background:none;
  color:var(--text-body);
  box-shadow:none;
}
.frs-log__version:hover{border-color:var(--border-strong);color:var(--text-accent)}
/* The trophy sets itself apart by being FILLED, not by being the only glyph in
   the row: archive sits beside it and both are 20px marks. */
.frs-log__trophy{
  display:inline-flex;
  justify-content:center;
  color:var(--text-faint);
}
.frs-log__trophy span{cursor:pointer}
.frs-log__trophy i{font-size:20px;line-height:1}
.frs-log__trophy input{position:absolute;opacity:0;width:0;height:0}
.frs-log__trophy input:checked + span i,
.frs-log__trophy input:checked ~ span i{
  color:var(--signal-warn);
  font-variation-settings:'FILL' 1;
}
.frs-log__archive{
  padding:0;
  border:0;
  background:none;
  box-shadow:none;
  color:var(--text-faint);
}
.frs-log__archive i{font-size:20px;line-height:1}
.frs-log__archive:hover{color:var(--signal-down)}
.frs-log__archive:disabled{opacity:.35}
.frs-log__changes{min-width:0}

/* The contribution panel's scale toggle. Sits in the panel head next to the
   subtitle, so it is sized down to sit on that line without becoming a control
   the eye has to hunt for. */
.frs-contrib__scale{
  font-size:var(--fs-label);
  padding:2px 10px;
  margin:0 var(--space-2) 0 0;
  white-space:nowrap;
  font-variant-numeric:tabular-nums;
}

/* --- Geography breakdown ----------------------------------------------------
   Two comparisons on one panel, and they answer different questions:

     "This model, market by market"  - the open model's own fit, evaluated on each
                                       market's rows. Subsets of ONE fit.
     "Each geography's own model"    - what each market scores when it gets a model
                                       of its own. Separate fits.

   Confusing the two is the mistake the panel is built to prevent, so they are
   labelled rather than merged, and the framing note sits above both. */
.frs-geo__head{display:flex;align-items:baseline;gap:var(--space-3);margin-bottom:var(--space-3)}
.frs-geo__title{
  font-family:var(--font-body);
  font-size:var(--fs-label);
  font-weight:var(--fw-semibold);
  letter-spacing:var(--ls-label);
  text-transform:uppercase;
  color:var(--text-muted);
}
.frs-geo__spacer{flex:1}
.frs-geo__hint{font-size:var(--fs-caption);color:var(--text-faint)}

/* Info, not warning. Nothing here is wrong; the reader is about to misread
   something that is behaving exactly as it should. */
.frs-geo__note{
  display:block;
  padding:var(--space-3) var(--space-4);
  margin-bottom:var(--space-4);
  border-left:3px solid var(--signal-info);
  border-radius:var(--radius-sm);
  background:var(--signal-info-bg);
  color:var(--text-body);
  font-size:var(--fs-body-sm);
  line-height:var(--lh-snug);
}
.frs-geo__note b{color:var(--text-primary)}

/* The MAPE-fine / R²-negative pair, explained where it appears. */
.frs-geo__subset-note{
  display:block;
  margin-bottom:var(--space-3);
  color:var(--text-muted);
  font-size:var(--fs-caption);
  line-height:var(--lh-snug);
}

/* Nothing to score against. Stated, not implied by a blank space. */
.frs-geo__unavailable{
  display:block;
  padding:var(--space-3) var(--space-4);
  border:1px dashed var(--border);
  border-radius:var(--radius-sm);
  background:var(--surface-inset);
  color:var(--text-muted);
  font-size:var(--fs-body-sm);
}

.frs-geo__section-h{
  display:block;
  margin:var(--space-4) 0 var(--space-2);
  font-size:var(--fs-label);
  font-weight:var(--fw-semibold);
  letter-spacing:var(--ls-label);
  text-transform:uppercase;
  color:var(--text-muted);
}
.frs-geo__tier-h{
  display:block;
  margin:var(--space-3) 0 var(--space-2);
  font-size:var(--fs-body-sm);
  font-weight:var(--fw-semibold);
  color:var(--text-body);
}
.frs-geo__tier-count{
  font-family:var(--font-figure);
  font-variant-numeric:tabular-nums;
  font-size:var(--fs-caption);
  color:var(--text-faint);
  margin-left:var(--space-2);
}

.frs-geo__grid{
  display:grid;
  grid-template-columns:repeat(auto-fill,minmax(212px,1fr));
  gap:var(--space-2);
}
.frs-geo__card{
  display:flex;
  flex-direction:column;
  gap:5px;
  padding:var(--space-3);
  border:1px solid var(--border-subtle);
  border-radius:var(--radius-sm);
  background:var(--surface-inset);
  min-width:0;
}
/* The model you are looking at, so its position among its siblings is findable
   without reading every card. */
.frs-geo__card--open{border-color:var(--action);background:var(--action-subtle)}
.frs-geo__card-name{
  font-size:var(--fs-body-sm);
  font-weight:var(--fw-semibold);
  color:var(--text-primary);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.frs-geo__card-figures{display:flex;align-items:baseline;gap:var(--space-2)}
.frs-geo__card-r2{
  font-family:var(--font-figure);
  font-variant-numeric:tabular-nums;
  font-size:var(--fs-metric-sm,19px);
  font-weight:var(--fw-semibold);
  color:var(--text-body);
}
.frs-geo__card--open .frs-geo__card-r2{color:var(--text-accent)}
.frs-geo__card-meta{
  font-family:var(--font-figure);
  font-variant-numeric:tabular-nums;
  font-size:var(--fs-caption);
  color:var(--text-faint);
}
.frs-geo__bar{height:5px;border-radius:3px;background:var(--surface);overflow:hidden}
.frs-geo__bar-fill{height:100%;border-radius:3px;background:var(--signal-info)}
.frs-geo__card--open .frs-geo__bar-fill{background:var(--action)}

/* --- Nothing changes in isolation ------------------------------------------
   The banner that carries pending configuration changes, the Train button that
   is asking to be pressed, and the fit that is admitting it is the old one.

   Amber, not vermillion: nothing is broken and nothing has been lost. The model
   on screen is a real fit of a configuration that is no longer the one in front
   of you, which is a caution, not an error. */
.frs-pending{
  display:block;
  margin-bottom:var(--space-3);
  padding:var(--space-3) var(--space-4);
  border:1px solid var(--signal-warn);
  border-left-width:var(--border-width-accent,3px);
  border-radius:var(--radius-card);
  background:var(--signal-warn-bg);
}
.frs-pending__row{display:flex;align-items:flex-start;gap:var(--space-3);flex-wrap:wrap}
.frs-pending__icon{color:var(--signal-warn);font-size:22px;line-height:1.1;flex:none}
.frs-pending__copy{display:flex;flex-direction:column;gap:var(--space-1);flex:1 1 320px;min-width:0}
.frs-pending__copy>span:first-child{color:var(--text-primary);font-size:var(--fs-body-sm)}
.frs-pending__chip{
  display:inline-block;
  padding:1px 8px;
  margin:2px 0;
  border-radius:var(--radius-pill);
  background:var(--surface);
  border:1px solid var(--border);
  color:var(--text-body);
  font-size:var(--fs-caption);
}
.frs-pending__sep{padding:0 var(--space-1);color:var(--text-faint)}
/* The sentence that is the entire reason the banner exists. */
.frs-pending__why{
  flex:0 1 auto;
  align-self:center;
  color:var(--text-muted);
  font-size:var(--fs-caption);
  font-style:italic;
}

/* Train is already the committing action; when something is pending it is also
   the ONLY thing that makes the screen true again. A ring, not a new colour. */
.btn.frs-train--pending{
  box-shadow:0 0 0 3px var(--signal-warn-bg), 0 0 0 4px var(--signal-warn);
}

/* Old, not wrong. Enough desaturation to read as past tense at a glance, not so
   much that the numbers stop being legible - they are still the figures you are
   deciding against. */
.frs-fit--stale{
  opacity:.72;
  filter:saturate(.55);
  transition:opacity var(--dur-base) var(--ease-standard),
             filter var(--dur-base) var(--ease-standard);
}

/* --- Training columns workspace ----------------------------------------------
   The cards flow into as many tracks as fit rather than a fixed Bootstrap column
   count, because how many fit depends on what the rail is doing, not on a
   breakpoint. 190px is the narrowest a card can be and still show a column name
   plus its three controls without wrapping. */
.frs-variable-grid{
  display:grid;
  /* 260, not 190. A card is a response curve plus two controls now, and 190 was
     the width of a name and three icon buttons. */
  grid-template-columns:repeat(auto-fill,minmax(260px,1fr));
  gap:var(--space-4);
  align-items:start;
}
.frs-variable-grid>*{min-width:0}

/* --- One variable, one card -------------------------------------------------
   Curve, evidence and controls in one object. The card is the WTemplate itself,
   so inclusion is a class on the root rather than a value inside the markup: a
   template re-render would rebuild the Highcharts instance in the middle of it. */
.frs-vcard{
  display:flex;
  flex-direction:column;
  gap:var(--space-2);
  padding:var(--space-3);
  background:var(--surface-card,var(--surface));
  border:1px solid var(--border-subtle);
  border-radius:var(--radius-card);
  box-shadow:var(--shadow-card);
  min-width:0;
}
/* Excluded is quiet, not hidden and not struck through. The column is still an
   object with settings; it is simply not in this run. */
.frs-vcard--excluded{
  background:var(--canvas-sunken);
  border-style:dashed;
}
.frs-vcard--excluded .frs-vcard__chart,
.frs-vcard--excluded .frs-vcard__evidence,
.frs-vcard--excluded .frs-vcard__controls{opacity:.55}

.frs-vcard__head{
  display:flex;
  align-items:center;
  gap:var(--space-2);
  min-width:0;
}
.frs-vcard__tick{flex:none;margin:0}
.frs-vcard__name{
  flex:1 1 auto;
  min-width:0;
  font-family:var(--font-body);
  font-size:var(--fs-body-sm);
  font-weight:var(--fw-semibold);
  color:var(--text-primary);
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}
.frs-vcard__kind{
  flex:none;
  padding:1px 7px;
  border-radius:var(--radius-pill);
  border:1px solid var(--border);
  background:var(--surface-inset);
  color:var(--text-muted);
  font-family:var(--font-figure);
  font-size:var(--fs-caption);
  letter-spacing:var(--ls-label);
}
/* Advertising is one column by design - LongTerm.ShortTerm.Composite, compressed
   upstream - so it is worth marking as a different kind of thing from an
   ordinary numeric column, not worth shouting about. */
.frs-vcard__kind[data-kind="adv"]{
  border-color:var(--action);
  color:var(--text-accent);
}

.frs-vcard__chart{width:100%;min-width:0;height:132px;overflow:hidden}
.frs-vcard__chart-plot{width:100%;height:132px}
.frs-vcard__chart--empty{
  display:flex;
  align-items:center;
  justify-content:center;
  min-height:96px;
  padding:var(--space-3);
  border:1px dashed var(--border-subtle);
  border-radius:var(--radius-md);
  background:var(--surface-inset);
  color:var(--text-faint);
  font-size:var(--fs-caption);
  text-align:center;
}

.frs-vcard__evidence{display:block}
.frs-vcard__stat{
  display:flex;
  align-items:baseline;
  justify-content:space-between;
  gap:var(--space-2);
  padding-block:2px;
}
.frs-vcard__stat-key{
  font-size:var(--fs-caption);
  color:var(--text-muted);
}
/* Tabular, uncoloured, unranked. Correlation is context: store count correlates
   at 64% because more stores sell more. Shading it would tell a traditional MMM
   modeller to drop the most structural column in the matrix. */
.frs-vcard__stat-value{
  font-family:var(--font-figure);
  font-variant-numeric:tabular-nums;
  font-size:var(--fs-body-sm);
  color:var(--text-body);
}

.frs-vcard__flag{
  display:flex;
  align-items:flex-start;
  gap:var(--space-2);
  margin-top:var(--space-2);
  padding:var(--space-2);
  border-radius:var(--radius-sm);
  font-size:var(--fs-caption);
  line-height:var(--lh-snug);
}
.frs-vcard__flag-icon{font-size:16px;line-height:1.2;flex:none}
.frs-vcard__flag[data-sev="warn"]{background:var(--signal-warn-bg);color:var(--signal-warn)}
.frs-vcard__flag[data-sev="down"]{background:var(--signal-down-bg);color:var(--signal-down)}
.frs-vcard__flag[data-sev="info"]{background:var(--signal-info-bg);color:var(--signal-info)}
.frs-vcard__flag[data-sev="ok"]{background:var(--signal-up-bg);color:var(--signal-up)}
.frs-vcard__flag b{font-weight:var(--fw-semibold)}

.frs-vcard__controls{
  display:flex;
  flex-direction:column;
  gap:var(--space-2);
  margin-top:auto;
  padding-top:var(--space-2);
  border-top:1px solid var(--border-subtle);
}
.frs-vcard__field{display:flex;flex-direction:column;gap:2px}
.frs-vcard__field-label{
  font-size:var(--fs-caption);
  color:var(--text-muted);
  letter-spacing:var(--ls-label);
}
.frs-vcard__field-note{font-size:var(--fs-caption);color:var(--text-faint)}

/* A control that is no longer at its default has to say so from across the card.
   Both of these are read at a glance while scanning twenty columns, so the signal
   is on the control itself rather than in a note beside it.

   Constraint carries the WARNING colour because forcing a slope overrides what the
   data said - it is the setting most likely to be left on by accident and then
   forgotten. Transform gets an INFO outline instead: log() or arcTan() changes what
   the network is fed, but it does not overrule the fit, so it is a different kind
   of not-default and should not read as a caution. */
.frs-vcard__field .form-select.frs-select--constrained{
  border-color:var(--amber-600);
  background:var(--signal-warn-bg);
  color:var(--amber-400);
  font-weight:var(--fw-semibold);
}
.frs-vcard__field .form-select.frs-select--transformed{
  border-color:var(--signal-info);
  box-shadow:inset 0 0 0 1px var(--signal-info);
}
.frs-vcard__alpha-note{font-size:var(--fs-caption);color:var(--text-muted);line-height:var(--lh-snug)}

/* All columns, or only the run. Sits with All / None, because it is about the same
   bit on the same columns - not with the view switch, which is about how they are
   drawn. */
.frs-workspace-filter{display:inline-flex;align-items:center;gap:var(--space-2)}
.frs-workspace-filter__label{
  font-size:var(--fs-caption);
  color:var(--text-muted);
  letter-spacing:var(--ls-label);
  white-space:nowrap;
}
.frs-workspace-filter .form-switch{padding-left:0;margin:0}

/* --- The three views --------------------------------------------------------
   Gallery, Table, Raw data. A segmented control, so it reads as "which one",
   not as three separate actions. */
.frs-viewswitch{display:inline-flex;border-radius:var(--radius-control);overflow:hidden}
.frs-viewswitch .btn{border-radius:0;margin:0}
.frs-viewswitch .btn+.btn{border-left:1px solid var(--border)}
.frs-viewswitch .frs-viewswitch__btn--current{
  background:var(--action-subtle);
  color:var(--text-accent);
  border-color:var(--action);
}

/* --- The table view ---------------------------------------------------------
   For the 40-variable case, where a gallery is a scroll rather than a glance.
   Deliberately no sparkline per row: the table exists to avoid paying for 40
   more chart instances. */
.frs-vtable-wrap{overflow-x:auto}
.frs-vtable{width:100%;font-size:var(--fs-body-sm)}
.frs-vtable th{
  font-size:var(--fs-label);
  letter-spacing:var(--ls-label);
  text-transform:uppercase;
  color:var(--text-muted);
  white-space:nowrap;
}
.frs-vtable td{vertical-align:middle}
.frs-vtable tbody tr:nth-child(odd){background:var(--row-stripe)}
.frs-vtable__figure{font-family:var(--font-figure);font-variant-numeric:tabular-nums}
.frs-vtable__muted{color:var(--text-faint)}
.frs-vtable__slope{color:var(--text-body)}
.frs-vtable__row--excluded td{opacity:.55}

/* --- The review strip -------------------------------------------------------
   What is left where the per-variable curves used to be: the one affordance on
   this screen that is about the model as a whole. */
.frs-review-strip{
  display:flex;
  align-items:center;
  gap:var(--space-3);
  flex-wrap:wrap;
  margin-top:var(--space-3);
  padding-top:var(--space-3);
  border-top:1px solid var(--border-subtle);
}
.frs-review-strip__copy{display:flex;flex-direction:column;flex:1 1 220px;min-width:0}
/* In the settings column there is no room for a button beside the copy. */
.frs-review-strip .btn{width:100%}
.frs-review-strip__copy b{color:var(--text-primary)}
.frs-review-strip__copy span{font-size:var(--fs-body-sm);color:var(--text-muted)}

.frs-workspace-head{
  display:flex;
  flex-wrap:wrap;
  align-items:center;
  gap:var(--space-3);
  margin-bottom:var(--space-4);
}
.frs-workspace-title{
  font-family:var(--font-body);
  font-size:var(--fs-label);
  font-weight:var(--fw-semibold);
  letter-spacing:var(--ls-label);
  text-transform:uppercase;
  color:var(--text-muted);
}
.frs-workspace-count{
  font-family:var(--font-figure);
  font-variant-numeric:tabular-nums;
  font-size:var(--fs-body-sm);
  color:var(--text-body);
}
.frs-workspace-count b{color:var(--text-accent);font-weight:var(--fw-semibold)}
.frs-workspace-spacer{flex:1}



/* -----------------------------------------------------------------------------
   1b. The top bar — DECISIONS.md #4, now built
   Three zones: identity on the left, RiskPosture in the centre (the one place
   that reads as session-wide), language + account + theme on the right. It
   carries STATE, not destinations — the rail still owns navigation.
   ----------------------------------------------------------------------------- */
.frs-topbar{
  display:flex;
  align-items:center;
  gap:var(--space-4);
  height:var(--topbar-height);
  min-height:var(--topbar-height);
  padding:0 var(--space-5);
  background:var(--surface) !important;
  border-bottom:1px solid var(--border-subtle);
  box-shadow:none !important;
  /* Full width, above the rail. DECISIONS.md #4 puts the mark in the top bar's
     left zone, which only reads correctly if the bar spans the window rather
     than starting where the content column does. The rail is offset below it. */
  position:fixed;
  top:0;left:0;right:0;
  z-index:1040;
}
.frs-topbar__zone{display:flex;align-items:center;gap:var(--space-3);min-width:0}
.frs-topbar__zone--start{flex:1 1 auto}
.frs-topbar__zone--centre{flex:0 0 auto;justify-content:center}
.frs-topbar__zone--end{flex:1 1 auto;justify-content:flex-end}

.frs-topbar__brand{
  display:flex;align-items:center;gap:var(--space-2);
  color:var(--text-primary);
  border-bottom:0;
  flex:none;
}
.frs-topbar__brand img{height:42px;width:auto;display:block}
.frs-topbar__brand span{
  font-family:var(--font-heading);
  font-size:var(--fs-h4);
  font-weight:var(--fw-bold);
  letter-spacing:var(--ls-heading);
}
.frs-topbar__brand:hover{color:var(--text-primary)}

/* "Weekly · 9 markets · 1,284 line items" — the current project's meta line.
   Truncates rather than wraps: unlike a navigation label (#15.4) this is
   context, and losing the tail of it costs nothing. */
.frs-version-chip{
  flex:none;
  display:inline-flex;align-items:center;
  padding:2px 8px;
  border:1px solid var(--border);
  border-radius:var(--radius-pill);
  background:var(--surface-inset);
  color:var(--text-muted);
  font-family:var(--font-figure);
  font-variant-numeric:tabular-nums;
  font-size:var(--fs-caption);
  cursor:pointer;
  transition:var(--transition-control);
}
.frs-version-chip:hover{
  border-color:var(--teal-700);
  background:var(--action-subtle);
  color:var(--text-accent);
}

.frs-topbar__context{
  color:var(--text-muted);
  font-size:var(--fs-body-sm);
  padding-left:var(--space-4);
  margin-left:var(--space-1);
  border-left:1px solid var(--border-subtle);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;
}
.frs-topbar__context:empty{display:none}

.frs-topbar__menu{
  display:inline-flex;align-items:center;justify-content:center;
  width:34px;height:34px;
  background:transparent;border:1px solid transparent;
  border-radius:var(--radius-control);
  color:var(--text-body);cursor:pointer;
  transition:var(--transition-control);
}
.frs-topbar__menu:hover{background:var(--action-subtle);color:var(--text-primary)}
.frs-topbar__legacy-toggle{display:none}

/* A quiet square icon control — the theme toggle, and the pattern for anything
   else that lands in the top bar's right zone. */
.frs-icon-btn{
  display:inline-flex;align-items:center;justify-content:center;
  width:34px;height:34px;padding:0;
  background:transparent;border:1px solid transparent;
  border-radius:var(--radius-control);
  color:var(--text-body);
  transition:var(--transition-control);
}
.frs-icon-btn:hover{background:var(--action-subtle);border-color:transparent;color:var(--text-primary)}
.frs-icon-btn:focus-visible{outline:2px solid var(--border-focus);outline-offset:2px}
.frs-icon-btn .material-symbols-rounded{font-size:20px}

/* The account menu. DECISIONS.md #4 puts profile, 2FA and logout inside it, and
   moves the version number out of the rail — it is diagnostic information, not
   navigation, so it sits at the foot of this menu. */
.frs-account__trigger{
  display:inline-flex;align-items:center;gap:var(--space-2);
  height:34px;padding:0 var(--space-2) 0 var(--space-2);
  background:transparent;border:1px solid transparent;
  border-radius:var(--radius-control);
  color:var(--text-body);font-size:var(--fs-body-sm);cursor:pointer;
  transition:var(--transition-control);
  max-width:220px;
}
.frs-account__trigger:hover,
.frs-account.show .frs-account__trigger{background:var(--action-subtle);color:var(--text-primary)}
.frs-account__trigger .material-symbols-rounded{font-size:22px}
.frs-account__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.frs-account__menu{min-width:240px;padding:var(--space-2)}
.frs-account__item{display:block}
.frs-account__action{
  display:flex;align-items:center;gap:var(--space-2);
  width:100%;
  background:transparent;border:0;
  border-radius:var(--radius-sm);
  padding:var(--density-compact-pad-y) var(--space-3);
  color:var(--text-body);font-size:var(--fs-body-sm);font-weight:var(--fw-regular);
  text-align:left;text-transform:none;
  transition:var(--transition-control);
}
.frs-account__action:hover{background:var(--action-subtle);color:var(--text-primary)}
.frs-account__action .material-symbols-rounded{font-size:18px}

/* Any other navbar in the app — the login splash is the only one left, and it
   is .navbar-transparent, which overrides all of this in §11. Deliberately does
   NOT set a height: .frs-topbar owns that, and applying --topbar-height to a
   bare .navbar meant the sidenav (which also carries .navbar) inherited it. */
.navbar:not(.sidenav):not(.frs-topbar){
  background:var(--surface) !important;
  border-bottom:1px solid var(--border-subtle);
  box-shadow:none !important;
}
.navbar .nav-link,
.navbar-nav .nav-link{color:var(--text-body)}
.navbar .nav-link:hover{color:var(--text-primary)}

.footer{
  color:var(--text-muted);
  font-size:var(--fs-body-sm);
  padding:var(--space-6) 0;
}
.footer .nav-link{color:var(--text-muted);font-size:var(--fs-body-sm);padding-top:0;padding-bottom:.25rem}
.footer .nav-link:hover{color:var(--text-primary)}
.footer .footer-logo{max-width:2rem}

/* =============================================================================
   2. Buttons
   DECISIONS.md #1/#5, resolved: colour carries meaning, not identity.
     Teal        -> the one committing action in the view  (primary/info/success)
     Amber       -> actions that move money                (warning)
     Vermillion, OUTLINE ONLY -> destructive               (danger)
     Ghost       -> everything else       (secondary/light/dark/link/outline-*)

   This is still a mechanical remap by Bootstrap class, not a button-by-button
   semantic audit. DECISIONS.md #5 names roughly a dozen buttons whose class
   doesn't match its real job; fixing those individually is the follow-up. What
   this section guarantees is that each Bootstrap class means one thing
   everywhere it appears.
   ============================================================================= */
.btn{
  font-family:var(--font-body);
  font-weight:var(--fw-semibold);
  font-size:var(--fs-body-sm);
  letter-spacing:.01em;
  line-height:1.4;
  text-transform:none;      /* Insights-Global used to force uppercase here */
  border-radius:var(--radius-control);
  border:1px solid transparent;
  padding:var(--density-compact-pad-y) var(--density-compact-pad-x);
  transition:var(--transition-control);
  box-shadow:none;
}
.btn-sm{padding:4px 8px;font-size:var(--fs-caption)}
.btn-lg{padding:var(--density-comfortable-pad-y) var(--density-comfortable-pad-x);font-size:var(--fs-body)}
.btn:focus-visible{outline:2px solid var(--border-focus);outline-offset:2px;box-shadow:none}
.btn:disabled,.btn.disabled{opacity:.42;cursor:not-allowed}

.btn-primary,.btn-info,.btn-success{
  background:var(--action);border-color:var(--action);color:var(--action-fg);
}
.btn-primary:hover,.btn-info:hover,.btn-success:hover{background:var(--action-hover);border-color:var(--action-hover);color:var(--action-fg)}
.btn-primary:active,.btn-info:active,.btn-success:active{background:var(--action-active);border-color:var(--action-active)}

.btn-warning{background:var(--signal-warn);border-color:var(--signal-warn);color:var(--value-fg)}
.btn-warning:hover{background:var(--amber-400);border-color:var(--amber-400);color:var(--value-fg)}

.btn-danger,.btn-outline-danger{background:transparent;border-color:var(--verm-600);color:var(--signal-down)}
.btn-danger:hover,.btn-outline-danger:hover{background:var(--signal-down-bg);border-color:var(--verm-500);color:var(--signal-down)}

/* Ghost fills with a quiet raised surface at rest rather than going fully
   transparent. The design system's IconButton specs transparent-at-rest, which
   is right for a control inside a toolbar or table row where surrounding chrome
   supplies the boundary. Several of these in Insights are the only interactive
   element in their area (a lone Edit icon on a plain card, a full-width Back
   bar, a filename-as-download-button) and read as inert with nothing to lean
   on. Teal/amber/vermillion still mean exactly what they meant before. */
.btn-secondary,.btn-light,.btn-dark,
.btn-outline-secondary,.btn-outline-primary,.btn-outline-info,.btn-outline-light,.btn-outline-dark{
  background:var(--surface-raised);border-color:var(--border-strong);color:var(--text-primary);
}
.btn-secondary:hover,.btn-light:hover,.btn-dark:hover,
.btn-outline-secondary:hover,.btn-outline-primary:hover,.btn-outline-info:hover,.btn-outline-light:hover,.btn-outline-dark:hover{
  background:var(--action-subtle);border-color:var(--border-strong);color:var(--text-primary);
}

.btn-outline-warning{background:transparent;border-color:var(--amber-600);color:var(--amber-400)}
.btn-outline-warning:hover{background:var(--signal-warn-bg);border-color:var(--amber-500);color:var(--amber-400)}
.btn-outline-success{background:transparent;border-color:var(--action);color:var(--text-accent)}
.btn-outline-success:hover{background:var(--action-subtle);color:var(--text-accent)}

.btn-link{color:var(--text-accent);text-decoration:none;border-color:transparent;background:transparent}
.btn-link:hover{color:var(--action-hover);text-decoration:underline;background:transparent}

.btn .material-symbols-rounded{font-size:1.1em;vertical-align:middle;position:relative;top:-1px}
.btn-inner--icon{margin-right:var(--space-2);vertical-align:middle}
.btn-inner--text{vertical-align:middle}

/* DECISIONS.md #15.5, REJECTED and now actually removed: Material Dashboard's
   .move-on-hover applied a perspective/translate lift on hover. A lifting card
   is a consumer-app affordance and reads wrong on a surface that audits money.
   Hover is a colour change only. The class is kept (roughly 35 call sites) so
   no markup has to change; it simply no longer moves anything. */
.move-on-hover{transition:var(--transition-control);transform:none}
.move-on-hover:hover{transform:none}

/* Admin is a badge, not a button (DECISIONS.md #5). Per #15.3 this follows the
   Badge component's `neutral`/`accent` tone rather than being a one-off, so
   .frs-badge is the general form and .badge-admin-link is retained as an alias
   for the single existing call site in AuthenticatedController.cpp. */
.frs-badge,
.badge-admin-link{
  display:inline-flex;align-items:center;gap:var(--space-1);
  background:var(--surface-inset);border:1px solid var(--border);color:var(--text-muted);
  font-family:var(--font-body);font-size:var(--fs-label);font-weight:var(--fw-semibold);
  letter-spacing:var(--ls-label);text-transform:uppercase;
  border-radius:var(--radius-pill);padding:3px 10px;
}
.frs-badge--accent,
.badge-admin-link{background:var(--action-subtle);border-color:var(--teal-700);color:var(--text-accent)}
.frs-badge--value{background:var(--signal-warn-bg);border-color:var(--amber-700);color:var(--amber-400)}
.frs-badge--risk{background:var(--signal-down-bg);border-color:var(--verm-700);color:var(--signal-down)}
.badge-admin-link{cursor:pointer}
.badge-admin-link:hover{background:var(--action-subtle-hover)}

/* The second half of a two-part heading — "Scenario name — Group name". Both
   halves were the accent colour; the qualifier steps back instead, so the
   hierarchy is carried by weight and tone rather than by spending teal twice. */
.frs-title-qualifier{color:var(--text-muted);font-weight:var(--fw-regular)}

/* -----------------------------------------------------------------------------
   2b. Figures, hints and direction
   Three patterns the product kept expressing with Bootstrap contextual colour
   because nothing better existed.
   ----------------------------------------------------------------------------- */

/* Guidance text. Five screens styled their "click to add a file" line
   .text-success — it is an instruction, not a success state, and painting it
   the accent colour made every empty screen shout. */
.frs-hint{
  color:var(--text-muted);
  font-size:var(--fs-body-sm);
}

/* Any number the user reads as a figure: mono, tabular, so columns of them line
   up and a digit never changes width as it counts. readme.md 3. */
.frs-metric{
  font-family:var(--font-figure);
  font-variant-numeric:tabular-nums;
  font-feature-settings:"tnum" 1;
}
/* Money and lift are AMBER, never teal — tokens/colors.css: "Amber: money, lift,
   attention. Never used for action." Teal is reserved for the one committing
   action in the view, and a revenue readout is not an action. */
.frs-metric--value{color:var(--text-value)}
.frs-metric--muted{color:var(--text-muted)}

/* Boolean existence indicator for a table cell — e.g. "does this tag appear on
   this channel?" A checkmark glyph and a dash, never a checkbox: there's nothing
   to toggle, it's a fact about the data, not a control. */
.frs-tag-exists{color:var(--signal-up);font-weight:var(--fw-bold);font-size:var(--fs-h3);text-align:center}
.frs-tag-absent{color:var(--text-faint);font-weight:var(--fw-bold);font-size:var(--fs-h3);text-align:center}

/* Direction. The design system requires colour AND an arrow, always — the
   colours are colour-blind-safe but colour alone is not (HANDOFF.md decision
   table). Drawn with borders rather than a glyph so it needs no font coverage
   and inherits currentColor. */
.frs-delta{
  font-family:var(--font-figure);
  font-variant-numeric:tabular-nums;
  font-weight:var(--fw-semibold);
  white-space:nowrap;
}
.frs-delta::before{
  content:"";
  display:inline-block;
  width:0;height:0;
  margin-inline-end:.35em;
  vertical-align:baseline;
  border-inline-start:.32em solid transparent;
  border-inline-end:.32em solid transparent;
}
.frs-delta--up{color:var(--signal-up)}
.frs-delta--up::before{border-bottom:.42em solid currentColor}
.frs-delta--down{color:var(--signal-down)}
.frs-delta--down::before{border-top:.42em solid currentColor}
.frs-delta--flat{color:var(--signal-flat)}
/* A flat delta gets a bar, not a triangle — no direction to point. */
.frs-delta--flat::before{
  width:.64em;height:0;
  border:0;border-top:.16em solid currentColor;
  vertical-align:middle;
}

/* =============================================================================
   3. Cards
   ============================================================================= */
.card{
  background:var(--surface-card);
  border:1px solid var(--border-subtle);
  border-radius:var(--radius-card);
  box-shadow:var(--shadow-card);
  color:var(--text-body);
}
.card-header{
  background:transparent;
  border-bottom:1px solid var(--border-subtle);
  padding:var(--space-4) var(--space-5);
  font-family:var(--font-heading);color:var(--text-primary);font-weight:var(--fw-semibold);
}
.card-body{padding:var(--space-5);color:var(--text-body)}
.card-footer{background:transparent;border-top:1px solid var(--border-subtle);padding:var(--space-4) var(--space-5)}
.card-title{font-family:var(--font-heading);color:var(--text-primary);font-size:var(--fs-h4)}
.card-subtitle{color:var(--text-muted);font-size:var(--fs-body-sm)}

/* =============================================================================
   4. Forms
   ============================================================================= */
/* background-COLOR, never the `background` shorthand, on anything that can be a
   field. Bootstrap and Wt paint glyphs into these through background-image - the
   spinner chevrons on a WSpinBox, the red "!" on an invalid value - and position
   them with background-repeat/position/size. The shorthand resets all three to
   their initial values, and `repeat` is one of them, so the glyph tiled across the
   whole field: rows of red "!" and grids of chevrons. It showed up while typing
   because `.form-control:focus` and `.form-control.is-invalid` have the same
   specificity and ours is loaded later, so a focused invalid field took our reset. */
.form-control,.form-select{
  background-color:var(--surface-inset);
  border:1px solid var(--border);
  color:var(--text-primary);
  border-radius:var(--radius-input);
  font-family:var(--font-body);font-size:var(--fs-body-sm);
  padding:var(--density-compact-pad-y) var(--density-compact-pad-x);
}
.form-control::placeholder{color:var(--text-faint)}
.form-control:focus,.form-select:focus{
  background-color:var(--surface-inset);border-color:var(--border-focus);color:var(--text-primary);
  box-shadow:var(--glow-focus);
}
.form-control:disabled,.form-select:disabled{background-color:var(--surface);color:var(--text-faint)}
.form-label{color:var(--text-primary);font-size:var(--fs-body-sm);font-weight:var(--fw-medium)}
.form-text{color:var(--text-muted);font-size:var(--fs-caption)}
.form-check-input{background-color:var(--surface-inset);border-color:var(--border-strong)}
.form-check-input:checked{background-color:var(--action);border-color:var(--action)}
.form-check-input:focus{border-color:var(--border-focus);box-shadow:var(--glow-focus)}
.form-check-inline{display:inline-block;margin-right:var(--space-1)}
.form-switch{padding-left:calc(var(--bs-gutter-x) * .5);justify-content:space-between}
.input-group-text{background:var(--surface-raised);border-color:var(--border);color:var(--text-muted)}

/* Every figure is mono with tabular numerals — readme.md §3. */
.form-control[type=number],
input.Wt-spinbox,
.spinbox input{font-family:var(--font-figure);font-variant-numeric:tabular-nums}

/* Material Dashboard's floating-label input. The templates use
   `input-group input-group-outline` with `is-filled` toggled by Wt, so the
   pattern stays; only its rendering moves onto the tokens. The original drew
   the notched outline with three pseudo-elements and a hard-coded #d2d6da —
   a plain 1px token border does the same job and survives a theme switch. */
.input-group.input-group-outline{position:relative}
.input-group.input-group-outline .form-control{
  background-color:var(--surface-inset);
  border:1px solid var(--border);
  border-radius:var(--radius-input);
  color:var(--text-primary);
}
.input-group.input-group-outline .form-control:focus{border-color:var(--border-focus);box-shadow:var(--glow-focus)}
.input-group.input-group-outline .form-control[disabled]{background-color:var(--surface);color:var(--text-faint)}
.input-group.input-group-outline .form-label{
  color:var(--text-muted);
  font-size:var(--fs-body-sm);
  margin-bottom:var(--space-1);
}
.input-group.input-group-outline.is-filled .form-label,
.input-group.input-group-static.is-filled .form-label{color:var(--text-primary)}
.input-group.input-group-outline.is-valid .form-control{border-color:var(--action)}
.input-group.input-group-outline.is-invalid .form-control{border-color:var(--verm-500)}
.input-group.input-group-static.input-group-sm label{font-size:var(--fs-caption)}
.input-group.input-group-static.input-group-lg label{font-size:var(--fs-body)}

/* =============================================================================
   5. Tables
   ============================================================================= */
.table{color:var(--text-body);--bs-table-color:var(--text-body);--bs-table-bg:transparent}
.table > :not(caption) > * > *{border-bottom-color:var(--border-subtle);background:transparent;color:inherit}
.table thead th{
  color:var(--text-muted);font-size:var(--fs-label);font-weight:var(--fw-semibold);
  text-transform:uppercase;letter-spacing:var(--ls-label);
  border-bottom:1px solid var(--border);background:transparent;
}
.table-striped > tbody > tr:nth-of-type(odd) > *{background:var(--row-stripe)}
.table-hover > tbody > tr:hover > *{background:var(--action-subtle);color:var(--text-primary)}
.table td.num,.table .frs-num{font-family:var(--font-figure);font-variant-numeric:tabular-nums;text-align:right}

/* =============================================================================
   6. Badges
   ============================================================================= */
.badge{border-radius:var(--radius-chip);font-weight:var(--fw-semibold);letter-spacing:var(--ls-label);text-transform:uppercase;font-size:var(--fs-caption)}
.badge.bg-primary,.badge.bg-info,.badge.bg-success{background:var(--action-subtle) !important;color:var(--text-accent) !important}
.badge.bg-warning{background:var(--signal-warn-bg) !important;color:var(--amber-400) !important}
.badge.bg-danger{background:var(--signal-down-bg) !important;color:var(--signal-down) !important}
.badge.bg-secondary,.badge.bg-light,.badge.bg-dark{background:var(--surface-inset) !important;color:var(--text-muted) !important}

/* =============================================================================
   7. Colour utilities
   Bootstrap's contextual utilities ship their own !important colour resolved
   from --bs-*-rgb, so unlike most of this file they need !important to win.
   ============================================================================= */
/* These three used to collapse onto one accent colour. That was defensible as a
   first pass, but the product uses .text-info and .text-success as generic
   emphasis on roughly 35 call sites — section headings, file status, console
   output, instructions — so collapsing them made teal the colour of everything
   and stopped it meaning "the committing action". Split onto the semantic
   tokens the design system already defines, which is also what the Bootstrap
   names mean:
     primary -> the accent            info -> informational (slate blue)
     success -> positive direction (teal-400, same family as the accent)
   The call sites that use these for emphasis rather than meaning still need a
   per-site audit — DECISIONS.md #5's "necessary follow-up". This only stops the
   stylesheet from making the problem worse than the markup does. */
.text-primary{color:var(--text-accent) !important}
.text-info{color:var(--signal-info) !important}
.text-success{color:var(--signal-up) !important}

/* A section or panel heading: the design system's answer for the ~8 call sites
   that reached for .text-info to make a title stand out. Structure, not signal
   — heading face, primary text, no colour. */
.frs-section-title{
  font-family:var(--font-heading);
  font-size:var(--fs-h4);
  font-weight:var(--fw-semibold);
  color:var(--text-primary);
  letter-spacing:var(--ls-heading);
}
.text-danger{color:var(--signal-down) !important}
.text-warning{color:var(--amber-400) !important}
.text-muted{color:var(--text-muted) !important}
.text-secondary{color:var(--text-body) !important}

/* .text-dark and .text-white are contrast-forcing utilities rather than
   semantic colour, and the previous adapter deliberately left them alone. That
   was wrong for THIS app: the default theme is dark (tokens/colors.css sets the
   dark palette on bare :root), so Bootstrap's .text-dark — #212529 !important —
   was painting near-black text onto near-black surfaces. It appears on every
   destination in the rail. Both are now routed to the token that expresses what
   the markup meant: "the highest-contrast text on this surface". */
.text-dark{color:var(--text-primary) !important}
.text-white{color:var(--text-primary) !important}
.text-white-50{color:var(--text-muted) !important}
.text-light{color:var(--text-body) !important}

.border-primary,.border-info,.border-success{border-color:var(--teal-700) !important}
.border-danger{border-color:var(--verm-700) !important}
.border-warning{border-color:var(--amber-700) !important}
.border-dark,.border-secondary{border-color:var(--border-strong) !important}
.border-light{border-color:var(--border-subtle) !important}

/* .bg-light/.bg-white/.bg-dark are used as a plain "give this a surface"
   utility rather than a deliberate light-on-dark choice, so they retarget onto
   the token surfaces. */
.bg-light,.bg-white{background-color:var(--surface-raised) !important;color:var(--text-primary)}
.bg-dark{background-color:var(--surface-inset) !important;color:var(--text-primary)}

/* =============================================================================
   8. Alerts
   ============================================================================= */
.alert{border-radius:var(--radius-md);border-width:1px;border-style:solid}
.alert-primary,.alert-info,.alert-success{background:var(--action-subtle);border-color:var(--teal-700);color:var(--text-primary)}
.alert-warning{background:var(--signal-warn-bg);border-color:var(--amber-700);color:var(--text-primary)}
.alert-danger{background:var(--signal-down-bg);border-color:var(--verm-700);color:var(--text-primary)}

/* =============================================================================
   9. Modals and Wt dialogs
   ============================================================================= */
.modal-content,
.Wt-dialog{
  background:var(--surface-raised);border:1px solid var(--border-strong);
  border-radius:var(--radius-card);box-shadow:var(--shadow-overlay);
  color:var(--text-body);
}
.modal-header,.Wt-dialog .titlebar{border-bottom:1px solid var(--border-subtle)}
.modal-footer{border-top:1px solid var(--border-subtle)}
.modal-title,.Wt-dialog .titlebar{font-family:var(--font-heading);color:var(--text-primary)}
.modal-body{overflow:auto}
.modal-backdrop{background:var(--stone-1000)}
.modal-backdrop.show{opacity:.72}

/* =============================================================================
   10. Tooltips and dropdowns
   ============================================================================= */
/* The tooltip is a dark overlay in BOTH themes - that is the design - so its text
   colour cannot come from a theme-dependent token. It used to be --text-primary,
   which resolves to --stone-950 (#0B1116) in light theme against a --stone-1000
   (#070C10) box: near-black on near-black, about 1.05:1, invisible. Dark theme hid
   the fault because --text-primary is light there. Pinned to a literal light stone
   so both themes get the same ~18:1 the dark theme always had.
   Sized at --fs-body: these carry whole explanatory sentences, not labels, and the
   16px floor applies to prose the reader is expected to actually read. */
.tooltip .tooltip-inner{
  background:var(--stone-1000);border:1px solid var(--stone-600);
  color:var(--stone-50);border-radius:var(--radius-md);
  box-shadow:var(--shadow-overlay,0 8px 24px rgba(0,0,0,.45));
  font-size:var(--fs-body);
  line-height:1.5;
  max-width:340px;
  padding:var(--space-2) var(--space-3);
  text-align:left;
}
.tooltip.show{opacity:1}
.tooltip .tooltip-arrow::before{border-top-color:var(--stone-1000);border-bottom-color:var(--stone-1000)}
.dropdown-menu{
  background:var(--surface-raised);border:1px solid var(--border-strong);
  border-radius:var(--radius-md);box-shadow:var(--shadow-overlay);
}
.dropdown-item{color:var(--text-body)}
.dropdown-item:hover,.dropdown-item:focus{background:var(--action-subtle);color:var(--text-primary)}

/* =============================================================================
   11. Tabs, pills, accordion
   ============================================================================= */
.nav-tabs{border-bottom-color:var(--border-subtle)}
.nav-tabs .nav-link{color:var(--text-muted);border:none;border-bottom:2px solid transparent;border-radius:0}
.nav-tabs .nav-link:hover{color:var(--text-primary)}
.nav-tabs .nav-link.active{color:var(--text-accent);background:transparent;border-bottom-color:var(--action)}
.nav-pills .nav-link.active{background:var(--action);color:var(--action-fg)}
.nav.nav-pills .nav-link .material-symbols-rounded{font-size:1.1em;vertical-align:middle}

/* Bootstrap's own accordion CSS is light-mode (white button, dark text, blue
   focus ring), so an accordion rendered as a light box inside a dark surface.
   This is the standalone single-item accordion ProjectCardV2 uses; DECISIONS.md
   #7's multi-section Raw Data Accordion is a separate component build. */
.accordion-item{background:var(--surface-card);border:1px solid var(--border-subtle);border-radius:var(--radius-card)}
.accordion-button{background:transparent;color:var(--text-primary);font-family:var(--font-heading);font-weight:var(--fw-semibold);box-shadow:none}
.accordion-button:not(.collapsed){background:var(--surface-raised);color:var(--text-primary);box-shadow:none}
.accordion-button:focus{box-shadow:var(--glow-focus)}
.accordion-button::after{filter:grayscale(1) opacity(.6)}
:root:not([data-theme="light"]) .accordion-button::after{filter:invert(1) grayscale(1) opacity(.6)}
.accordion-body{background:transparent;color:var(--text-body)}

/* The templates supply their own chevron pair (Accordian-Template.xml and the
   five screens that inline the same markup): two <i> glyphs toggled by
   aria-expanded, which was a Material Dashboard pattern. Reimplemented here so
   those templates keep working, and Bootstrap's own ::after chevron is
   suppressed on exactly those buttons so only one arrow shows. */
.accordion div button[aria-expanded=true] .collapse-close,
.accordion div button[aria-expanded=false] .collapse-open{display:none}
.accordion div button[aria-expanded=true] .collapse-open,
.accordion div button[aria-expanded=false] .collapse-close{display:block}
.accordion-button:has(.collapse-close)::after,
.accordion-button:has(.collapse-open)::after{display:none}

/* The login/splash nav sits over the brand backdrop with no bar of its own
   (MainLandingLoginScreen.xml). Material Dashboard forced #fff here; the
   backdrop is a dark token surface, so --text-primary is the same intent
   expressed once. */
.navbar.navbar-transparent{
  background:transparent !important;
  border-bottom:0;
  box-shadow:none !important;
}
.navbar.navbar-transparent .nav-link,
.navbar.navbar-transparent .nav-link i,
.navbar.navbar-transparent .navbar-brand{color:var(--text-primary)}
.navbar.navbar-transparent .nav-link:hover,
.navbar.navbar-transparent .nav-link:focus{color:var(--text-accent)}
.navbar.navbar-transparent .navbar-collapse{border-radius:var(--radius-lg)}
@media (max-width:991.98px){
  .navbar.navbar-transparent .navbar-collapse.collapsing,
  .navbar.navbar-transparent .navbar-collapse.show{
    background:var(--surface-raised);
    padding-top:var(--space-2);
    padding-bottom:var(--space-2);
    box-shadow:var(--shadow-overlay);
  }
}

/* Progress. Bootstrap draws the track; the bar takes the action colour rather
   than Material Dashboard's #43a047 green with a cyan glow. */
.progress{background:var(--surface-inset);border-radius:var(--radius-pill);height:1.25rem}
.progress-bar{background:var(--action);color:var(--action-fg);border-radius:var(--radius-pill);font-family:var(--font-figure);font-size:var(--fs-caption)}

/* =============================================================================
   12. Material Dashboard leftovers
   Every class in this section is one the templates actually reference and that
   Bootstrap does not ship. They are reimplemented here, on tokens, so that
   material-dashboard.min.css could be removed. Nothing else from that file was
   in use.

   These are legacy vocabulary, not design-system vocabulary: as screens are
   rebuilt against components/*.d.ts, their call sites should move to real
   component classes and the corresponding rule here should be deleted.
   ============================================================================= */

/* --- type scale: the templates' .text-xs/-sm/-md/-lg predate --fs-* --------- */
.text-xs{font-size:var(--fs-caption) !important;line-height:1.4}
.text-sm{font-size:var(--fs-body-sm) !important;line-height:1.45}
.text-md{font-size:var(--fs-body) !important;line-height:var(--lh-normal)}
.text-lg{font-size:var(--fs-body-lg) !important;line-height:var(--lh-normal)}
.text-bold,.font-weight-bold{font-weight:var(--fw-semibold) !important}
.font-weight-bolder{font-weight:var(--fw-bold) !important}
/* Material Dashboard's gradient text: a decorative flourish with no place on a
   surface that audits money. Flattened to the accent colour. */
.text-gradient{background:none;-webkit-text-fill-color:currentColor;color:var(--text-accent)}

/* --- surfaces -------------------------------------------------------------- */
/* .bg-gray-200 is used almost exclusively as `card bg-gray-200`, i.e. it means
   "the card surface". .bg-gray-100 is its one-step-lighter sibling. */
.bg-gray-100{background-color:var(--surface-raised) !important}
.bg-gray-200{background-color:var(--surface-card) !important}
/* The gradient fills were Creative Tim's pink/blue/near-black ramps. Flattened
   to token surfaces — readme.md §3 admits no gradient fills in the product. */
.bg-gradient-primary,.bg-gradient-info{background-image:none;background-color:var(--action-subtle) !important;color:var(--text-accent)}
.bg-gradient-dark,.bg-gradient-secondary{background-image:none;background-color:var(--surface-inset) !important;color:var(--text-primary)}

/* --- radius, elevation, stacking ------------------------------------------- */
.border-radius-sm{border-radius:var(--radius-sm)}
.border-radius-md{border-radius:var(--radius-md)}
.border-radius-lg{border-radius:var(--radius-lg)}
.border-radius-xl{border-radius:var(--radius-xl)}
/* .shadow-md has never been defined anywhere in this app — it is on the main
   content card in Insights-Main-Page-And-Footer.xml and silently did nothing.
   Defined now, and .shadow-primary (a pink glow) is neutralised: on a dark
   canvas the design system separates surfaces with borders, not shadow. */
.shadow-md{box-shadow:var(--shadow-2) !important}
.shadow-primary{box-shadow:none !important;border:1px solid var(--border-subtle)}
.z-index-2{z-index:2 !important}
.z-index-3{z-index:3 !important}
.z-index-sticky{z-index:1020}

/* --- spacing and sizing utilities Bootstrap's scale doesn't cover ----------- */
.my-6{margin-top:4rem !important;margin-bottom:4rem !important}
.mb-9{margin-bottom:10rem !important}
.mt-n2{margin-top:-.5rem !important}
.mt-n4{margin-top:-1.5rem !important}
.mt-n6{margin-top:-4rem !important}
.mb-n1{margin-bottom:-.25rem !important}
.top-1{top:1% !important}
.height-100{height:100px !important}
.opacity-5{opacity:.5 !important}
.opacity-8{opacity:.8 !important}

/* --- avatars --------------------------------------------------------------- */
.avatar{
  color:var(--text-primary);
  background:var(--surface-inset);
  display:inline-flex;align-items:center;justify-content:center;
  font-size:var(--fs-body);
  border-radius:var(--radius-pill);
  height:48px;width:48px;
  transition:var(--transition-control);
}
.avatar img{width:100%;border-radius:var(--radius-pill)}
.avatar-sm{width:36px !important;height:36px !important;font-size:var(--fs-body-sm)}
.avatar-xs{width:24px !important;height:24px !important;font-size:var(--fs-caption)}
.avatar-stats{width:60px !important;height:24px !important;font-size:var(--fs-caption)}
.navbar-vertical .navbar-nav .nav-link .avatar{width:1.875rem;height:1.875rem}

/* --- rules ----------------------------------------------------------------- */
/* Material Dashboard drew every <hr class="horizontal"> as a black-to-
   transparent gradient, which is invisible on a dark canvas. One token line. */
hr.horizontal{
  background-color:transparent;
  background-image:none;
  border:0;
  border-top:1px solid var(--border-subtle);
  opacity:1;
}
hr.horizontal.vertical{transform:rotate(90deg)}

/* --- page header ----------------------------------------------------------- */
.page-header{
  padding:0;position:relative;overflow:hidden;display:flex;align-items:center;
  background-size:cover;background-position:50%;
}

/* =============================================================================
   13. Icons
   ============================================================================= */
/* Google's Material Symbols, still loaded from fonts.googleapis.com in
   Insights.cpp. Every icon call site in the app is still one of these; the
   Lucide migration below replaces them screen by screen, and the Google Fonts
   link can be dropped the moment the last one goes. */
.material-symbols-rounded{
  font-family:"Material Symbols Rounded";
  font-size:20px;
  display:inline-block;line-height:1;
  text-transform:none;letter-spacing:normal;word-wrap:normal;white-space:nowrap;
  direction:ltr;
  font-variation-settings:"FILL" 0,"wght" 400,"GRAD" 0,"opsz" 24;
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
  text-rendering:optimizeLegibility;
  font-feature-settings:"liga";
}

/* Lucide, self-hosted at vendor/lucide/icons/ (98 curated icons). Rendered as a
   CSS mask so the glyph takes currentColor — readme.md §Iconography, adopted as
   a first-class pattern in DECISIONS.md #15.2.
   Usage: <span class="frs-icon" style="--icon:url('InsightsAssets/vendor/lucide/icons/check.svg')"></span> */
.frs-icon{
  display:inline-block;
  width:var(--icon-size,16px);height:var(--icon-size,16px);
  background-color:currentColor;
  -webkit-mask-image:var(--icon);mask-image:var(--icon);
  -webkit-mask-size:contain;mask-size:contain;
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-position:center;mask-position:center;
  flex:none;
}

/* =============================================================================
   14. Scrollbars — the default browser scrollbar reads badly on a dark canvas
   ============================================================================= */
*{scrollbar-color:var(--border-strong) transparent}
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:var(--radius-pill)}
::-webkit-scrollbar-thumb:hover{background:var(--text-faint)}

/* =============================================================================
   15. Wt item views — WTableView, WTreeView, WTreeTable
   These are not HTML tables, so §5 does not reach them. They are the densest
   surfaces in the product (the tag tables, the adstock column tree, the
   scenario/geography trees) and until now nothing in this app styled them at
   all.

   `Insights.cpp` calls `WBootstrap5Theme::setColorMode("dark")`, which puts
   data-bs-theme="dark" on <html> and switches Wt to its own dark variants —
   that alone fixes the light #ddd cell grid, the #f9f9f9 odd-row stripe and the
   light sort-arrow GIFs. What Wt still hardcodes even in dark mode is a neutral
   #43474b border and a #4d94d4 selected row, neither of which is ours. The
   selection colour matters most: blue-on-black is the one place in this product
   where a colour would be doing a job teal already does.

   Selectors here mirror Wt's own specificity (it qualifies with .Wt-tableview /
   .Wt-treeview and with [data-bs-theme=dark]) so they land without !important.
   ============================================================================= */
[data-bs-theme=dark] .Wt-itemview,
.Wt-itemview{
  background:transparent;
  border:1px solid var(--border-subtle);
  border-radius:var(--radius-md);
  color:var(--text-body);
  font-size:var(--fs-body-sm);
}
[data-bs-theme=dark] .Wt-tv-contents,
.Wt-tableview .Wt-tv-contents{background-color:transparent}

/* Cell rules. Vertical rules are dropped entirely — a column boundary in a
   dense table is carried by alignment, not by a line (readme.md §3). Only the
   horizontal rule survives, at the subtle border token. */
[data-bs-theme=dark] .Wt-tableview .Wt-plaintable td,
[data-bs-theme=dark] .Wt-tableview .Wt-plaintable th,
[data-bs-theme=dark] .Wt-tableview .Wt-tv-br,
[data-bs-theme=dark] .Wt-tableview .Wt-tv-contents .Wt-tv-c{
  border-bottom-color:var(--border-subtle);
  border-inline-end-color:transparent;
}
[data-bs-theme=dark] .Wt-treeview .Wt-header,
[data-bs-theme=dark] .Wt-treeview .Wt-tv-br,
[data-bs-theme=dark] .Wt-treeview ul .Wt-tv-c{border-color:var(--border-subtle)}

/* Header row: the same eyebrow treatment §5 gives a <thead>. */
[data-bs-theme=dark] .Wt-tableview .Wt-header,
[data-bs-theme=dark] .Wt-treeview .Wt-header,
.Wt-itemview .Wt-header{
  border-bottom:1px solid var(--border);
  background:transparent;
}
.Wt-itemview .Wt-header .Wt-label{
  color:var(--text-muted);
  font-family:var(--font-body);
  font-size:var(--fs-label);
  font-weight:var(--fw-semibold);
  text-transform:uppercase;
  letter-spacing:var(--ls-label);
}

/* Striping, hover and selection. Wt's dark stripe is #272b2f (a warm grey that
   does not belong to this palette) and its selection is #4d94d4. */
[data-bs-theme=dark] .Wt-tableview .Wt-tv-contents.Wt-striped a.Wt-tv-c:nth-child(odd),
[data-bs-theme=dark] .Wt-tableview .Wt-tv-contents.Wt-striped div.Wt-tv-c:nth-child(odd),
.Wt-tableview .Wt-tv-contents.Wt-striped a.Wt-tv-c:nth-child(odd),
.Wt-tableview .Wt-tv-contents.Wt-striped div.Wt-tv-c:nth-child(odd){
  background-color:var(--row-stripe);
}
[data-bs-theme=dark] .Wt-itemview .active,
[data-bs-theme=dark] .Wt-tableview .Wt-tv-contents.Wt-striped a.active:nth-child(odd),
[data-bs-theme=dark] .Wt-tableview .Wt-tv-contents.Wt-striped div.active:nth-child(odd),
.Wt-itemview .active{
  background-color:var(--action-subtle-hover);
  color:var(--text-primary);
  text-shadow:none;
}
[data-bs-theme=dark] .Wt-itemview .Wt-tv-rh:hover,
.Wt-itemview .Wt-tv-rh:hover{background-color:var(--border-focus)}
.Wt-itemview .Wt-drop-site{background-color:var(--action-subtle);outline:1px dashed var(--border-focus)}

/* Sort indicators. Wt serves these as GIFs (light and dark pairs), which means
   a bitmap arrow that cannot take currentColor and does not match the chevron
   drawn in §1. Replaced with the same CSS triangle in all three states. */
.Wt-itemview .Wt-tv-sh-none,
.Wt-itemview .Wt-tv-sh-up,
.Wt-itemview .Wt-tv-sh-down,
[data-bs-theme=dark] .Wt-itemview .Wt-tv-sh-none,
[data-bs-theme=dark] .Wt-itemview .Wt-tv-sh-up,
[data-bs-theme=dark] .Wt-itemview .Wt-tv-sh-down{
  background-image:none;
  position:relative;
}
.Wt-itemview .Wt-tv-sh::after{
  content:"";
  position:absolute;
  inset-inline-end:4px;
  top:9px;
  border-inline-start:4px solid transparent;
  border-inline-end:4px solid transparent;
}
.Wt-itemview .Wt-tv-sh-none::after{border-top:4px solid var(--text-faint);opacity:.5}
.Wt-itemview .Wt-tv-sh-down::after{border-top:4px solid var(--text-accent)}
.Wt-itemview .Wt-tv-sh-up::after{border-bottom:4px solid var(--text-accent)}

/* Lazy-load placeholder.
   WTableView reserves the height of the rows it has not fetched yet with a
   .Wt-spacer, and Wt's own CSS fills that with resources/loading.png — a tile
   of the word "Loading" repeated on a diagonal. It was invisible before this
   file existed only because Wt's default row backgrounds are opaque and painted
   over it; the rules above make .Wt-tv-contents transparent so the design
   system's surface shows through, and the tile came through with it. So it
   reads as "Loading" stamped across live data in every table in the product,
   which is what it looked like: not a lost lazy-load, just a watermark that
   stopped being covered up.

   The tile goes rather than the transparency. Wt fetches the next block in one
   round trip, so what the placeholder is actually reporting is a gap of a few
   hundred milliseconds during a scroll — and a repeating word is the loudest
   possible way to report it. An empty reserved area says the same thing
   quietly. */
.Wt-itemview .Wt-spacer,
[data-bs-theme=dark] .Wt-itemview .Wt-spacer{background-image:none}

/* Dates and numbers inside an item view are figures — mono, tabular. This is
   the single largest concentration of figures in the product (987 tag rows on
   the scenario screen alone) and they were rendering in the body face. */
.Wt-itemview .Wt-tv-c:not(:first-child):not(.frs-tag-exists):not(.frs-tag-absent),
.Wt-tableview .Wt-plaintable td:not(:first-child):not(.frs-tag-exists):not(.frs-tag-absent){
  font-family:var(--font-figure);
  font-variant-numeric:tabular-nums;
  font-size:var(--fs-caption);
}

/* ---- Contribution to sales -------------------------------------------------
   Bars run either side of a centre line. A variable that costs sales is a
   different statement from one that earns them, and length alone cannot say so —
   so the sign gets a side and a colour, not a minus sign the eye skips.
   The two closing rows (baseline, interaction) are not variables, and are ruled
   off so they cannot be read as drivers anyone could go and change. */
/* The "Inspect input data" dialog: one block per column, name over its figures.
   Plain on purpose - it is a reference list you read once, not a card you work in. */
.frs-inspect{
  /* One tile size, whatever the block holds: the container is a flowing flex row,
     so without a basis an alpha column with one figure sits next to a numeric one
     with five at a third of the width. */
  flex:0 0 320px;
  padding:var(--space-3);
  margin:0 var(--space-3) var(--space-3) 0;
  align-self:flex-start;
  background:var(--surface-inset);
  border:var(--border-width) solid var(--border-subtle);
  border-radius:var(--radius-md);
  font-size:var(--fs-body-sm);
  color:var(--text-body);
}
.frs-inspect__head{
  display:flex;
  align-items:baseline;
  gap:var(--space-2);
  margin-bottom:var(--space-2);
}
.frs-inspect__name{
  font-weight:var(--fw-semibold);
  color:var(--text-primary);
}
.frs-inspect__kind{
  font-size:var(--fs-caption);
  letter-spacing:var(--ls-label);
  text-transform:uppercase;
  color:var(--text-muted);
}
/* Label left, figure right, on one line each: the block is read down the right
   edge, comparing figures, not across. */
.frs-inspect__stat{
  display:flex;
  justify-content:space-between;
  gap:var(--space-3);
  padding:1px 0;
}
.frs-inspect__stat span:last-child{
  font-family:var(--font-mono);
  font-variant-numeric:tabular-nums;
  color:var(--text-primary);
}
.frs-inspect__warn{
  margin-top:var(--space-2);
  padding-top:var(--space-2);
  border-top:1px solid var(--border-subtle);
  color:var(--signal-warn);
  font-size:var(--fs-body-sm);
  line-height:var(--lh-normal);
}

.frs-contrib{display:flex;flex-direction:column;gap:var(--space-2)}
.frs-contrib__subtitle{
  font-size:var(--fs-body-sm);
  color:var(--text-muted);
}
.frs-contrib__row{
  display:grid;
  grid-template-columns:minmax(120px,240px) 1fr 92px 78px;
  gap:var(--space-3);
  align-items:center;
  font-size:var(--fs-body);
}
.frs-contrib__name{
  color:var(--text-body);
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}
.frs-contrib__name--adv{color:var(--text-accent);font-weight:var(--fw-semibold)}
.frs-contrib__track{
  position:relative;
  display:block;
  height:16px;
  background:var(--surface-inset);
  border-radius:var(--radius-sm);
  overflow:hidden;
}
/* The centre line is the zero, and it is drawn rather than implied. */
.frs-contrib__track::before{
  content:"";
  position:absolute;
  left:50%;
  top:0;
  bottom:0;
  width:1px;
  background:var(--border-strong);
}
.frs-contrib__fill{
  position:absolute;
  top:0;
  bottom:0;
  display:block;
  border-radius:var(--radius-sm);
}
.frs-contrib__fill--up{background:var(--signal-up)}
.frs-contrib__fill--down{background:var(--signal-down)}
.frs-contrib__value{
  font-family:var(--font-mono);
  font-variant-numeric:tabular-nums;
  text-align:right;
  font-weight:var(--fw-medium);
  color:var(--text-body);
}
.frs-contrib__value--up{color:var(--signal-up)}
.frs-contrib__value--down{color:var(--signal-down)}
/* The measured figure. Quieter than the share it is paired with, because the eye
   scans the bars first and comes to the money for one row at a time. */
.frs-contrib__amount{
  font-family:var(--font-mono);
  font-variant-numeric:tabular-nums;
  text-align:right;
  color:var(--text-muted);
}
.frs-contrib__amount--down{color:var(--signal-down)}
/* Labels the two number columns once. A money column under a subtitle that says
   "per period" would otherwise be read as a per-period figure, and it is not. */
.frs-contrib__row--head{
  font-size:var(--fs-caption);
  letter-spacing:var(--ls-label);
  text-transform:uppercase;
  color:var(--text-muted);
}
.frs-contrib__row--head .frs-contrib__track{background:none}
.frs-contrib__row--head .frs-contrib__track::before{background:none}
.frs-contrib__row--head .frs-contrib__amount,
.frs-contrib__row--head .frs-contrib__value{
  font-family:inherit;
  font-weight:var(--fw-medium);
  color:var(--text-muted);
}
.frs-contrib__row--aside{
  color:var(--text-muted);
  font-size:var(--fs-body-sm);
}
.frs-contrib__row--aside .frs-contrib__name{color:var(--text-muted)}
.frs-contrib__row--aside .frs-contrib__amount{color:var(--text-muted)}
.frs-contrib__row--aside .frs-contrib__track{background:none}
.frs-contrib__row--aside .frs-contrib__track::before{background:none}
/* The sum of the variable rows above - bold rather than muted, unlike the aside
   rows below it, because it is still a claim about the variables, not a footnote
   about the model. No bar: a summed share is not a value on the same -100%..100%
   scale the per-variable bars are drawn to. */
.frs-contrib__row--total{font-weight:var(--fw-semibold)}
.frs-contrib__row--total .frs-contrib__track{background:none}
.frs-contrib__row--total .frs-contrib__track::before{background:none}
/* Explicit, not :first-of-type. Every row in this list is a div, so the selector
   only ever matched if the first div in the container happened to be an aside -
   which it never is, and is less than ever now the column captions lead. */
.frs-contrib__row--rule{
  margin-top:var(--space-3);
  padding-top:var(--space-3);
  border-top:1px solid var(--border-subtle);
}
/* No measure cap on the modelling screen's prose. A 78ch limit is the usual
   readability advice, but here it made every explanatory block stop at roughly half
   the width of the chart it explains, so the panel read as two different documents
   stacked on top of each other. Matching the chart's width won that trade (Mark,
   2026-09-10). The panel itself is what bounds the line length now, and the reports
   page keeps its own cap - this is a decision about these panels, not a rule change. */
.frs-contrib__note{
  display:block;
  margin-top:var(--space-4);
  font-size:var(--fs-body);
  color:var(--text-muted);
  line-height:var(--lh-relaxed);
}
.frs-contrib__note p{margin:0 0 var(--space-3) 0}
.frs-contrib__note p:last-child{margin-bottom:0}
/* Amber, and in the panel head rather than buried in the note: the layout here is
   finished and the arithmetic is not, and a reader has to be told which is which
   before they read a number off it. */
.frs-contrib__provisional{
  display:inline-flex;
  align-items:center;
  gap:var(--space-1);
  padding:2px var(--space-2);
  border:1px solid var(--signal-warn);
  border-radius:var(--radius-pill);
  background:var(--signal-warn-bg);
  color:var(--signal-warn);
  font-size:var(--fs-label);
  font-weight:var(--fw-semibold);
  letter-spacing:var(--ls-label);
  text-transform:uppercase;
  white-space:nowrap;
}
.frs-contrib__provisional i{font-size:15px;line-height:1}

/* --- The forecast interval --------------------------------------------------
   The modeller's view of how wrong the model tends to be. It borrows .frs-stats
   wholesale from the fit tiles above it, and deliberately: coverage and band width
   are the same KIND of object as R-squared and MAPE - measured facts about one fit
   - and giving them their own visual language would have implied otherwise.
   Only the three things the tiles cannot carry are styled here. */
.frs-interval__verdict{
  display:inline-flex;
  align-items:center;
  gap:var(--space-1);
  padding:2px var(--space-2);
  border:1px solid currentColor;
  border-radius:var(--radius-pill);
  font-size:var(--fs-label);
  font-weight:var(--fw-semibold);
  letter-spacing:var(--ls-label);
  text-transform:uppercase;
  white-space:nowrap;
}
.frs-interval__verdict i{font-size:15px;line-height:1}
/* The interval's own colour is set inline, from the measurement. The border tracks
   it through currentColor rather than a second inline property, so a verdict can
   never end up with a border that disagrees with its word. */
.frs-interval__subtitle{
  display:block;
  margin-bottom:var(--space-3);
  font-size:var(--fs-body-sm);
  color:var(--text-muted);
  line-height:var(--lh-normal);
}
.frs-interval__note{
  display:block;
  margin-top:var(--space-2);
  font-size:var(--fs-body);
  color:var(--text-muted);
  line-height:var(--lh-relaxed);
}
/* The tiles are the last thing in this panel rather than the first as they are in
   the fit card, so the margin that separates them from a chart below is dead space
   here. */
.frs-panel--interval .frs-stats{margin-bottom:0}

/* ==========================================================================
   RUN SETTINGS ROWS
   A settings panel that is a summary first. Each line names a setting and shows
   what the next run will use; the ones you can change are buttons that open the
   one dialog that changes them. Fact rows and editable rows are deliberately
   almost the same object - the column reads as a list of what this run IS, and
   only the tune glyph and the hover state say which lines answer back.
   ========================================================================== */
.frs-set{
  display:flex;
  flex-direction:column;
}
.frs-set__row{
  display:flex;
  align-items:baseline;
  gap:var(--space-3);
  width:100%;
  padding:var(--space-2) 0;
  border:0;
  border-bottom:1px solid var(--border-subtle);
  background:none;
  border-radius:0;
  text-align:left;
  font-size:var(--fs-body);
  color:var(--text-body);
  box-shadow:none;
}
.frs-set__row:last-child{border-bottom:0}
.frs-set__label{
  color:var(--text-muted);
  white-space:nowrap;
}
.frs-set__value{
  margin-left:auto;
  font-family:var(--font-figure);
  font-variant-numeric:tabular-nums;
  color:var(--text-body);
  text-align:right;
}
/* Only the pressable rows react. A fact row that lights up under the cursor is
   a promise the panel cannot keep. */
.frs-set__row--edit{cursor:pointer;transition:background var(--dur-fast) var(--ease-standard)}
.frs-set__row--edit:hover,
.frs-set__row--edit:focus-visible{background:var(--surface-inset)}
.frs-set__row--edit:hover .frs-set__edit{opacity:1}
.frs-set__row--fact{cursor:default}
.frs-set__edit{
  font-size:18px;
  line-height:1;
  color:var(--text-muted);
  opacity:.45;
  transition:opacity var(--dur-fast) var(--ease-standard);
}
.frs-set__row--edit:disabled{opacity:.5;cursor:not-allowed}
.frs-set__actions{
  display:flex;
  flex-direction:column;
  gap:var(--space-2);
  margin-top:var(--space-3);
  padding-top:var(--space-3);
  border-top:1px solid var(--border-subtle);
}
.frs-set__actions .btn{width:100%}
/* The read-outs several older code paths still write to. Present, addressable,
   and never painted. */
.frs-set__offstage{display:none}
.frs-set__dialog{
  display:flex;
  flex-direction:column;
  gap:var(--space-3);
  max-width:52ch;
  font-size:var(--fs-body);
}
.frs-log__row--head .frs-log__cell--icon i{font-size:18px;line-height:1;vertical-align:middle}

/* -----------------------------------------------------------------------------
   Strategies — the approved-plan history (V5 plan section 7)

   ITS OWN GRID, NOT frs-log's. The experiment log's row is a 13-track template
   authored for that screen's columns; reusing it here put every cell in the
   wrong track and pushed the page into horizontal overflow. Two screens with
   different columns need two templates - the shared part is the type, spacing
   and hover vocabulary below, which is what the log classes were really for.
   ----------------------------------------------------------------------------- */
/* The table's fixed tracks total more than the content column at narrow widths,
   so it scrolls INSIDE ITS OWN BOX rather than widening the page. A grid that
   overflows its container pushes the whole layout, and the symptom then shows up
   somewhere else entirely - a clipped top bar, a shifted rail - which is a long
   way from the cause. Measured after this was added: no page-level overflow at
   1488 or below. */
.frs-strategy__scroll{overflow-x:auto;max-width:100%}

.frs-strategy__row{
  display:grid;
  grid-template-columns:
    minmax(180px,1fr)  /* name - the 1fr track, so it absorbs spare width */
    96px               /* status badge */
    170px              /* period */
    150px              /* predicted revenue */
    90px               /* lift */
    96px               /* origin - Optimizer / Imported / Manual */
    140px;             /* approved by */
  align-items:center;
  gap:var(--space-2);
  padding:var(--space-2) var(--space-2);
  border-bottom:1px solid var(--border-subtle);
}
.frs-strategy__row--head{
  border-bottom:1px solid var(--border);
  font-size:var(--fs-caption);
  letter-spacing:var(--ls-label);
  text-transform:uppercase;
  color:var(--text-muted);
}
/* Rows open a detail panel, so they carry the pointer and the hover step. Colour
   is the whole hover vocabulary here - no transform, per the system's rule that a
   hover-lift reads as a consumer affordance on a surface that audits money. */
.frs-strategy__row:not(.frs-strategy__row--head){cursor:pointer}
.frs-strategy__row:not(.frs-strategy__row--head):hover{background:var(--surface-inset)}
.frs-strategy__row--open{background:var(--action-subtle)}

.frs-strategy__cell{
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
  color:var(--text-body);
}
.frs-strategy__cell--num{
  font-family:var(--font-figure);
  font-variant-numeric:tabular-nums;
  text-align:end;
}
.frs-strategy__cell--muted{color:var(--text-muted)}

/* The per-channel summary inside an open strategy. Impressions lead and spend
   follows, deliberately - V5 plan 4.7: the model relates impressions to sales and
   spend is the efficiency lens over it, so a table that leads with dollars
   re-teaches the CPM framing this product exists to argue against. */
.frs-strategy__channel-row{
  display:grid;
  grid-template-columns:minmax(140px,1fr) 200px 160px 110px;
  align-items:center;
  gap:var(--space-2);
  padding:var(--space-1) var(--space-2);
  border-bottom:1px solid var(--border-subtle);
}
.frs-strategy__channel-row--head{
  border-bottom:1px solid var(--border);
  font-size:var(--fs-caption);
  letter-spacing:var(--ls-label);
  text-transform:uppercase;
  color:var(--text-muted);
}

/* The period tile on the Project Home scoreboard carries a DATE RANGE, not a
   figure. At the 27px metric size it wrapped to two lines and broke the tile
   row's baseline, so it steps down - it is context for the numbers beside it,
   not one of them. */
.frs-stat__value--period{font-size:var(--fs-body);line-height:1.3;white-space:nowrap}

/* The scoreboard's own track width. The shared .frs-stats grid mins out at 150px,
   which is fine for the metrics it was built for and far too narrow here: at
   1280 - the narrowest width this product is QA'd at - "$10,128,758,544" broke
   across two lines AFTER a thousands separator, so the tile read "$10,128,758"
   over ",544". A wrapped money figure is worse than a smaller one; this is a
   product that sells on precision to people who read numbers for a living.
   230px holds the widest figure the scoreboard can carry at the metric size. */
.frs-stats--scoreboard{grid-template-columns:repeat(auto-fit,minmax(230px,1fr))}
.frs-stats--scoreboard .frs-stat__value{white-space:nowrap}

/* ==========================================================================
   REPORT 2 - SATURATION AND EFFICIENT RANGE (V5 plan 8.2 / REPORTS.md 5.2)
   ITS OWN GRID, NOT frs-log__*. That one is a 13-track row authored for the
   experiment log; this report has a different column set, so it gets its own
   prefix rather than bending that grid to fit - see the V5 plan handoff,
   2026-09-08.
   NO GRAPHICAL RANGE COLUMN. A track drawing the two bounds plus a dot for
   current spend and a diamond for proposed shipped, then was removed the same
   day (Mark): the state badge already says where those two numbers sit
   relative to the bounds, so the track repeated Current/Proposed as two dots
   without adding information. See SaturationMapView.h. */
.frs-satmap__verdict{
  display:block;
  margin-bottom:var(--space-3);
  font-size:var(--fs-body-sm);
  color:var(--text-muted);
  max-width:78ch;
  line-height:var(--lh-relaxed);
}
.frs-satmap__scroll{
  overflow-x:auto;
  /* Ten rows before it scrolls - this row has no wrapping chip list like the
     experiment log's, so a flat pixel budget is safe here in a way it was not
     there. */
  --frs-satmap-rows:10;
  --frs-satmap-row:44px;
  --frs-satmap-head:34px;
  max-height:calc(var(--frs-satmap-head) + var(--frs-satmap-rows) * var(--frs-satmap-row));
  overflow-y:auto;
}
.frs-satmap__row{
  display:grid;
  /* PRIORITY-RANKED READING ORDER, LEFT TO RIGHT: what it is, what state it is
     in, what it costs today and at the proposed plan, then the two figures
     REPORTS.md 2.1 says must never be shown one without the other. */
  grid-template-columns:
    minmax(170px,1fr)  /* tag */
    150px              /* state */
    110px 110px        /* current spend, proposed spend */
    90px               /* average ROI */
    130px;             /* marginal ROI interval */
  gap:var(--space-3);
  align-items:center;
  min-width:760px;
  padding:var(--space-2) var(--space-1);
  font-size:var(--fs-body-sm);
  border-bottom:1px solid var(--border-subtle);
}
.frs-satmap__row:last-child{border-bottom:0}
.frs-satmap__row--head{
  font-size:var(--fs-caption);
  letter-spacing:var(--ls-label);
  text-transform:uppercase;
  color:var(--text-muted);
  border-bottom:1px solid var(--border);
}
.frs-satmap__tag{
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
  color:var(--text-body);
  /* SAME PIXEL SIZE AS .frs-satmap__figure IS NOT THE SAME VISUAL SIZE (Mark, 2026-09-08). Both
     were already 14.5px (var(--fs-body-sm)) - measured, not assumed - but Source Sans at 14.5px
     reads larger than IBM Plex Mono at 14.5px because the two typefaces have different x-heights.
     Stepping the tag down to fs-label is what actually matches the two columns' visual weight;
     matching the numeral would have left this exactly as it was. */
  font-size:var(--fs-label);
}
.frs-satmap__figure{
  font-family:var(--font-mono);
  font-variant-numeric:tabular-nums;
  text-align:right;
  color:var(--text-body);
  /* A wrapped money or ROI figure reads as two different numbers stacked, which is worse than one
     that overflows its column - the row's own overflow-x scroll (frs-satmap__scroll) is what
     catches a marginal-ROI interval too wide for 112px, e.g. "-84.57-108.56" on a near-zero-spend
     tag where the ratio swings wildly. */
  white-space:nowrap;
}
.frs-satmap__figure--muted{color:var(--text-faint)}
/* THE BADGE. Same shape as .frs-interval__verdict and .frs-contrib__provisional -
   an outlined pill whose colour is set inline from the state, per row, because
   four different colours share one rule rather than four. */
.frs-satmap__badge{
  display:inline-flex;
  align-items:center;
  gap:var(--space-1);
  padding:2px var(--space-2);
  border:1px solid currentColor;
  border-radius:var(--radius-pill);
  font-size:var(--fs-label);
  font-weight:var(--fw-semibold);
  letter-spacing:var(--ls-label);
  text-transform:uppercase;
  white-space:nowrap;
}
.frs-satmap__badge i{font-size:15px;line-height:1}

/* THE SHARED CONTROLS BOUNDARY (Mark, 2026-09-08). Run and Geography sat directly above the
   due-to bridge's title with nothing marking that they also drive the saturation report below it,
   so they read as belonging to "Due to" alone. This wraps the controls and both report bodies in
   one visible boundary, so changing either picker is legible as affecting everything inside it -
   not a generic `.frs-panel`, which is a card style used elsewhere for a single self-contained
   widget; this is specifically two reports declaring a shared dependency. */
.frs-reports__shared{
  border:1px solid var(--border-subtle);
  border-radius:var(--radius-card);
  padding:var(--space-4);
  margin-bottom:var(--space-4);
}

/* REPORT PROSE, AT BODY SIZE - NOT frs-log__hint (Mark, 2026-09-08: "almost impossible to
   read... happens a lot"). Every report built 2026-09-08 reused frs-log__hint - the app's own
   token for an actual hint, `--fs-caption` at 12.5px - for blurbs, captions, disclosures and
   refusal messages: content a reader has to read to understand the report, not an aside. That is
   the type-scale-floor rule's density exception used where no density applies.
   NO max-width, UNLIKE `.frs-interval__note` this borrows font-size/color/line-height from. That
   one is free-standing paragraph prose, capped at 78ch for a comfortable reading line. These sit
   directly under a full-width chart as its caption (Mark, 2026-09-08: "span the width of the
   charts proceeding them") - a capped width there reads as an unrelated paragraph rather than a
   note about the chart above it. */
.frs-reports__note{
  display:block;
  font-size:var(--fs-body);
  color:var(--text-muted);
  line-height:var(--lh-relaxed);
}

/* A NARROW TWO-COLUMN LIST, NOT space-between ACROSS THE PANEL (Mark, 2026-09-08: "bring these
   two columns together... spaced very, very far apart"). Channel performance's Average ROI list
   is a plain name/value pair repeated a handful of times - flexing it across the full chart width
   above put the value at the far right edge, correct for a chart axis, wrong for six short rows
   nobody reads that wide. Fixed columns keep every row's value aligned under the one above it. */
.frs-channelperf__roi-row{
  display:grid;
  grid-template-columns:minmax(80px,160px) 70px;
  gap:var(--space-3);
  align-items:center;
  padding:var(--space-1) 0;
}

/* ==========================================================================
   REPORT 6 - EXECUTIVE SUMMARY (REPORTS.md 3.1)
   Modelled on templates/one-pager/OnePager.dc.html's board-deck layout, built
   from this app's own atoms (frs-stat, frs-hint, frs-reports__note) rather
   than that file's raw inline styles. */
.frs-execsummary__recommendation{
  border-top:3px solid var(--signal-up);
  background:var(--surface-inset);
  border-radius:var(--radius-md);
  padding:var(--space-4);
  margin-bottom:var(--space-4);
}
.frs-execsummary__moves-row{
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:var(--space-4);
  margin-bottom:var(--space-4);
}
.frs-execsummary__move{
  display:flex;
  align-items:center;
  gap:var(--space-2);
  padding:var(--space-2) var(--space-3);
  background:var(--surface-inset);
  border:1px solid var(--border-subtle);
  border-radius:var(--radius-sm);
  margin-bottom:var(--space-2);
  font-size:var(--fs-body);
}
.frs-execsummary__move--out{border-left:3px solid var(--signal-down)}
.frs-execsummary__move--into{border-left:3px solid var(--signal-up)}
.frs-execsummary__move-name{flex:1}
.frs-execsummary__move-amount{
  font-family:var(--font-mono);
  font-variant-numeric:tabular-nums;
}
.frs-execsummary__move--out .frs-execsummary__move-amount{color:var(--signal-down)}
.frs-execsummary__move--into .frs-execsummary__move-amount{color:var(--signal-up)}
.frs-execsummary__bottom-row{
  display:grid;
  grid-template-columns:1.4fr 1fr;
  gap:var(--space-4);
  align-items:start;
}
.frs-execsummary__market-row{
  display:grid;
  grid-template-columns:minmax(100px,1fr) 90px 90px 80px;
  gap:var(--space-3);
  padding:var(--space-1) 0;
  border-bottom:1px solid var(--border-subtle);
  font-size:var(--fs-body-sm);
}
.frs-execsummary__market-row--head{
  font-size:var(--fs-caption);
  letter-spacing:var(--ls-label);
  text-transform:uppercase;
  color:var(--text-muted);
  border-bottom:1px solid var(--border);
}
.frs-execsummary__fact-row{
  display:flex;
  justify-content:space-between;
  align-items:center;
  gap:var(--space-3);
  padding:var(--space-1) 0;
  border-bottom:1px solid var(--border-subtle);
}

/* ==========================================================================
   REPORT 7 - MODEL PROVENANCE (REPORTS.md 3.9)
   ITS OWN GRID, NOT frs-log__* - that one is the 13-track experiment log row
   for ONE model's version history; this is one row per model, across every
   trophied model in the project, with a different column set entirely. */
.frs-modelprov__scroll{
  overflow-x:auto;
  --frs-modelprov-rows:8;
  --frs-modelprov-row:44px;
  --frs-modelprov-head:34px;
  max-height:calc(var(--frs-modelprov-head) + var(--frs-modelprov-rows) * var(--frs-modelprov-row));
  overflow-y:auto;
  margin-bottom:var(--space-3);
}
.frs-modelprov__row{
  display:grid;
  grid-template-columns:
    minmax(180px,1fr)  /* model */
    140px              /* R2 train -> holdout */
    150px              /* out-of-fold CV */
    150px              /* forecast coverage */
    170px              /* trained by, when */
    80px;              /* versions */
  gap:var(--space-3);
  align-items:center;
  min-width:820px;
  padding:var(--space-2) var(--space-1);
  font-size:var(--fs-body-sm);
  border-bottom:1px solid var(--border-subtle);
}
.frs-modelprov__row:last-child{border-bottom:0}
.frs-modelprov__row--head{
  font-size:var(--fs-caption);
  letter-spacing:var(--ls-label);
  text-transform:uppercase;
  color:var(--text-muted);
  border-bottom:1px solid var(--border);
}

/* REPORT 9 - plan & optimisation. Its own grid, not frs-satmap__row bent to fit: one more column
   (channel) than the saturation map needs, and a different reading order (current -> proposed ->
   how much it moved -> what it earned). Cell classes are reused from frs-satmap and frs-contrib
   rather than redefined - only the row's column layout is new. */
.frs-planopt__scroll{
  overflow-x:auto;
  --frs-planopt-rows:10;
  --frs-planopt-row:44px;
  --frs-planopt-head:34px;
  max-height:calc(var(--frs-planopt-head) + var(--frs-planopt-rows) * var(--frs-planopt-row));
  overflow-y:auto;
}
.frs-planopt__row{
  display:grid;
  grid-template-columns:
    minmax(170px,1fr)  /* tag */
    120px              /* channel */
    100px 100px        /* current spend, proposed spend */
    90px               /* change */
    90px               /* average ROI */
    130px;             /* marginal ROI interval */
  gap:var(--space-3);
  align-items:center;
  min-width:880px;
  padding:var(--space-2) var(--space-1);
  font-size:var(--fs-body-sm);
  border-bottom:1px solid var(--border-subtle);
}
.frs-planopt__row:last-child{border-bottom:0}
.frs-planopt__row--head{
  font-size:var(--fs-caption);
  letter-spacing:var(--ls-label);
  text-transform:uppercase;
  color:var(--text-muted);
  border-bottom:1px solid var(--border);
}

/* ---------------------------------------------------------------------------
   Plan vs actual (report: "what did you tell us to do, and what happened").
   Its own grid rather than bending .frs-planopt__row to fit: nine columns and a
   different meaning per column, and the sixth pattern point in plan 16.17 says a
   new table gets its own grid.
   --------------------------------------------------------------------------- */
.frs-pva__scroll{
  overflow-x:auto;
  --frs-pva-rows:12;
  --frs-pva-row:44px;
  --frs-pva-head:34px;
  max-height:calc(var(--frs-pva-head) + var(--frs-pva-rows) * var(--frs-pva-row));
  overflow-y:auto;
}
.frs-pva__row{
  display:grid;
  grid-template-columns:
    minmax(160px,1.4fr) /* campaign */
    100px               /* channel */
    minmax(110px,1fr)   /* region */
    132px 132px         /* impressions planned, actual */
    92px                /* impressions variance */
    124px 124px         /* spend planned, actual */
    72px;               /* weeks delivered / planned */
  /* 14px of gutter, not 10: the headings ("Impr. planned", "Spend planned") are the widest thing
     in these columns, not the figures, and at 10px they touched their neighbour. Sized against the
     HEADINGS for that reason - a grid measured against its data alone collides in its own header. */
  gap:0 14px;
  align-items:center;
  min-width:1120px;
  padding:6px 8px;
  border-bottom:1px solid var(--frs-border, rgba(255,255,255,.08));
}
.frs-pva__row--head{
  position:sticky;
  top:0;
  z-index:1;
  font-weight:600;
  background:var(--frs-panel, #16222c);
}
/* Revenue sits outside the scroller: it is context, not part of the comparison,
   and giving it the same grid would invite reading it as another variance row. */
.frs-pva__revrow{
  display:grid;
  grid-template-columns:minmax(140px,1fr) 140px 70px;
  gap:0 10px;
  align-items:center;
  max-width:520px;
  padding:5px 8px;
  border-bottom:1px solid var(--frs-border, rgba(255,255,255,.08));
}
