﻿@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');

/* =================================================================================================
   THE PRIMARY BLUE

   The site's blue is ChartPalette.BlueAccent (#12509E), NOT Bootstrap's #0D6EFD. It is darkened from
   Bootstrap's own primary (user, Sep 2026) because that blue read light and childish, and it scored
   exactly 4.50:1 on white, the WCAG AA floor for body text with no margin at all. #12509E is 7.87:1,
   which clears AAA.

   WHY THIS BLOCK HAS TO EXIST. That blue has TWO independent sources and moving only one leaves the
   site rendering two blues beside each other, most visibly on the landing page, where twelve
   .btn-primary CTAs are Bootstrap's and the links around them are ours:

     1. ChartPalette.BlueAccent  -> --color-blue-accent, consumed at ~26 places across app.css,
        calculator.css and landing.css. Emitted into :root by _Layout.cshtml for server-rendered pages
        and by DF.charts.setPalette at WASM startup for the client.
     2. Bootstrap's own --bs-primary / --bs-link-color, and the .btn-primary rule, which does NOT read
        --bs-primary: it hardcodes --bs-btn-bg and six sibling literals inside its own rule.

   OVERRIDDEN HERE RATHER THAN EDITED IN THE VENDORED FILE, deliberately. Editing bootstrap.min.css
   would put the site's identity inside a file whose whole point is that it can be replaced wholesale,
   so the next Bootstrap upgrade would silently revert it with nothing to catch that. app.css is linked
   AFTER bootstrap.min.css in both _Layout.cshtml and index.html, so these win on cascade order at equal
   specificity. Verified in both shells; if either ever reorders its <link> tags, this block goes dead
   silently, which is what TestPrimaryBlueOverride pins.

   THE SHADES ARE BOOTSTRAP'S OWN DERIVATION, not invented: reverse-engineered from its #0D6EFD set,
   hover-bg is shade 15%, hover/active-border shade 20%/25%, active-bg shade 20%, and the focus-shadow
   rgb is tint 15%. Keeping its ramp means hover and active feel exactly as they did.
   ================================================================================================= */
:root {
    --bs-blue:             #12509E;
    --bs-primary:          #12509E;
    --bs-primary-rgb:      18, 80, 158;
    --bs-link-color:       #12509E;
    --bs-link-color-rgb:   18, 80, 158;
    --bs-link-hover-color: #0E407E;
}

.btn-primary {
    --bs-btn-bg:                 #12509E;
    --bs-btn-border-color:       #12509E;
    --bs-btn-hover-bg:           #0F4486;
    --bs-btn-hover-border-color: #0E407E;
    --bs-btn-active-bg:          #0E407E;
    --bs-btn-active-border-color:#0E3C77;
    --bs-btn-disabled-bg:        #12509E;
    --bs-btn-disabled-border-color:#12509E;
    --bs-btn-focus-shadow-rgb:   54, 106, 173;
}

/* A SOFT LIFT ON HOVER, on EVERY button at once (user, 2026-09-25): every Bootstrap .btn in every colour
   (primary blue, the grey secondary and outline-secondary, the red danger and outline-danger, success,
   warning, light, dark), which is every coloured button the app and the public pages render, plus the
   Google button, which is not a .btn but sits beside two that are. One rule, so no button can hover
   differently from its siblings, whatever its colour: the colour darkening each variant already does is
   its own, and this adds the same depth to all of them.
   .btn-link is excluded: it is drawn as a text link, and a shadow under a link reads as a stray box.
   A drop shadow rather than a ring, because Bootstrap already draws a RING for keyboard focus and a hover
   ring would make the two indistinguishable; and it stands aside while the button has keyboard focus, so
   the focus ring is never replaced by it (both are box-shadow). Never on a disabled button. Only where a
   pointer can hover, or a tap would leave it stuck on (the identity badge's lesson). */
@media (hover: hover) {
    .btn:not(.btn-link):hover:not(:disabled):not(.disabled):not(:focus-visible),
    .btn-google-signin:hover:not(:disabled):not(:focus-visible) {
        box-shadow: 0 4px 14px rgba(10, 16, 23, 0.30);
    }
}

.btn-outline-primary {
    --bs-btn-color:              #12509E;
    --bs-btn-border-color:       #12509E;
    --bs-btn-hover-bg:           #12509E;
    --bs-btn-hover-border-color: #12509E;
    --bs-btn-active-bg:          #12509E;
    --bs-btn-active-border-color:#12509E;
    --bs-btn-disabled-color:     #12509E;
    --bs-btn-disabled-border-color:#12509E;
    --bs-btn-focus-shadow-rgb:   54, 106, 173;
}

/* Bootstrap's FORM CONTROLS carry the blue as bare literals rather than as custom properties, so the
   :root block above does not reach them. Left alone, a focused input glows in the old bright blue next
   to a #12509E button, which is the "two blues" this whole section exists to prevent, just one control
   further in. #88A8CE is the focus border at Bootstrap's own tint 50%, the same derivation that gave
   its #86B7FE from #0D6EFD. These render on the admin pages and in the app shell rather than on the
   public surface, which is why they are here rather than in calculator.css. */
.form-control:focus,
.form-select:focus,
.form-check-input:focus {
    border-color: #88A8CE;
    box-shadow: 0 0 0 .25rem rgba(18, 80, 158, .25);
}

.form-check-input:checked,
.form-check-input[type=checkbox]:indeterminate {
    background-color: #12509E;
    border-color: #12509E;
}

.nav-link:focus-visible {
    box-shadow: 0 0 0 .25rem rgba(18, 80, 158, .25);
}

.btn-close:focus {
    box-shadow: 0 0 0 .25rem rgba(18, 80, 158, .25);
}

.nav-pills  { --bs-nav-pills-link-active-bg: #12509E; }
.list-group { --bs-list-group-active-bg: #12509E; --bs-list-group-active-border-color: #12509E; }

/* Disabling font boosting in Chrome on Android */

html * {
    font-family: Tahoma, Arial, sans-serif;
    text-size-adjust: none;
    max-height: 1000000px;
    -webkit-text-size-adjust: none;
    -moz-text-size-adjust: none;
    -ms-text-size-adjust: none;
}

html, body {
    /* Root font anchor: 10.8px is 90% of the historical 12px base. Every em/rem
       size in the app cascades from here, so this single value scales the whole
       site's font sizing uniformly. */
    font-size: 10.8px;
    padding-top: 3px;
    line-height: 1.2;
}

/* THE one tooltip body size, for BOTH tooltip mechanisms: the `data-description`
   ::after popup (pills, toggles) and the rich `.control-tooltip` element behind a
   leaf control's (i) icon. Declared in rem, so it is an absolute size rather than a
   multiple of whatever it hangs off.

   It has to be rem. A tooltip is a POPUP, not part of its trigger, so it should not
   inherit the trigger's scale -- but both mechanisms used to size themselves `1.2em`,
   which is not a size at all, it is "20% bigger than my trigger". Every scaled
   trigger therefore produced its own tooltip size: 1.2rem off a plain control label,
   1.44rem off an inline toggle, 1.56rem off a module header pill, 1.68rem off a
   user-mode pill, 1.8rem off a segmented toggle. Users saw tooltips that were never
   quite the same size, and each new scaled control silently minted another variant.

   1.2rem is the value the MOST NUMEROUS surface already rendered at: the (i) popup on
   every leaf control, which is the tooltip a user meets constantly, against a handful of
   pills. It is also the right order for the job -- this is multi-paragraph body copy in a
   300-to-500px card, where ~13px sets a comfortable measure and the 1.56rem the pills
   happened to carry reads heading-sized. So the many hold still and the few come down to
   meet them. Retuning is this one line.

   --tooltip-line-height is the same story one property along: .control-tooltip pinned 1.4
   while the other bodies silently inherited the 1.2 off <body>, so unifying only the font
   size would still leave the popups reading differently. 1.4 is the right one to keep: the
   1.2 is a global set for dense control rows, and these are multi-paragraph prose cards.
   Both are declared here so a tooltip's type is described in ONE place. */
:root {
    --tooltip-font-size: 1.2rem;
    --tooltip-line-height: 1.4;
}

/* THE hover treatment for a clickable CARD: a pale-blue fill plus a soft ring just
   outside the border. It is one gesture said twice, so both halves belong together --
   the fill is `var(--color-pill-action-light)` at the call site, the ring is this token
   (offsets + spread + colour bundled, since that is the part re-typed wrong).

   Consumers: the compact-layout module pills, the sweep-suggestion cards, the three
   catalogue pickers (modules via `.module-picker-group .add-module-button`, goals +
   What-Ifs via `.add-accent-button`), and the wizard existing-instance tiles. All are
   small cards in a gapped grid, so they share the 0.15rem spread, and retuning the
   softness is this one line. The big module surfaces (`.module-view`, `.module-edit`,
   the chart card) carry the same colour at 0.25rem and stay untokenised. 0.25rem is NOT
   theirs alone though: `.label-link-active:hover` uses that spread with its own
   hand-typed `#CED9FE` / `#f3bbc0`, a different colour for a different job (a form
   control's affordance), and sits outside this family entirely.

   `.wizard-existing-tile` is the one consumer whose FILL is conditional. It is the only
   card here that carries state colour -- `.completed` is green and `.started` amber, in
   both the fill and the border -- so a hover fill would paint over the state the user
   opened the menu to read. It takes the ring unconditionally, since a ring sits outside
   the border and composes with every variant, and the fill only where there is no state
   fill to lose. That is one rule with a state exemption rather than two treatments, and
   it leaves a plain tile behaving exactly like the other four cards.

   Where it must NOT spread: a SELECTABLE card, where a fill already carries "chosen".
   `.tier-card-active` (the user-mode and identity-tier cards) fills with this very
   `--color-pill-action-light` when selected, so a hover fill there would be
   indistinguishable from having picked the card. `.pdf-export-option` is the same shape
   one step along: it already tints on hover (`--color-surface-light`) and fills a neutral
   `#f0f0f0` when selected, so hover and chosen are told apart by tone, and a third,
   bluer tint would muddy precisely that. Both keep what they have.

   It replaced a `border-color: #000` on the pickers and the wizard tiles, which read
   harsh against their pale resting borders -- and on the wizard tiles it was actively
   destructive, overriding the green / amber border so a completed tile lost half its
   state colour simply by being pointed at. */
:root {
    --hover-card-ring: 0 0 0 0.15rem var(--color-pill-action-shadow);

    /* The resting look of a catalogue tile that CANNOT be chosen -- a single-instance module
       already added, or a goal / What-If that does not apply: a faint dotted card. Defined here
       so the three pickers named above read as one system when disabled too. The module picker
       used to fade to 0.4 with a 1px border while the accent (goals / What-Ifs) tiles used
       0.6 / 2px; both now pull the one value from here. */
    --picker-tile-disabled-opacity: 0.4;
    --picker-tile-disabled-border-width: 2px;
    /* The module picker icon is already faded to 0.4 at idle, so a disabled tile fades it FURTHER, to
       this value, or the icon would not visibly grey when the module is added (the name still would).
       One token so the rest and hover rules cannot drift apart. */
    --picker-module-icon-disabled-opacity: 0.15;
}

p { 
    padding: 5px; 
}

.control-cell { 
    padding: 2px; 
}

.item-info-label {
    font-weight: bold;
    font-size: 1.1em;
    letter-spacing: 0.5px;
    letter-spacing: 1px;
}

.control-cell.item-info {
    border-style: solid;
    border-width: 1.5px;
    border-radius: 0.5em;
    margin-bottom: 5px;
}

.item-info-text {
    font-size:1.2em;
    text-align: center;
}

.item-info-label.warning {
    color: var(--color-red);
}

.control-cell.item-info.warning {
    background-color: var(--color-pill-error-light);
    border-color: var(--color-red);
}

.item-info-label.suggestion {
    color: var(--color-green-dark);
}

.control-cell.item-info.suggestion {
    background-color: var(--color-green-pale);
    border-color: var(--color-green-dark);
}

.item-info-label.information {
    color: var(--color-black);
}

.control-cell.item-info.information {
    background-color: var(--color-pill-information-light);
    border-color: var(--color-black);
}

.item-info-label.note {
    color: var(--color-pill-note-text);
}

.control-cell.item-info.note {
    background-color: var(--color-pill-note-light);
    border-color: var(--color-pill-note-border);
    color: var(--color-pill-note-text);
}

.item-info-label-header {
    text-align: center;
    margin-bottom: 10px;
    margin-top: 5px;
}

img.item-info-icon {
    width: 30px;
    margin-right: 10px;
}

/* Generic clickable action affordance for any .item-info-* style box (suggestion,
   warning, information, …). Sits on its own centered row at the bottom of the box.
   Renamed from .item-suggestion-action because the same treatment is wanted
   elsewhere; the Suggestion pill is just one consumer. */
.item-info-action-row {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-wrap: wrap;
    gap: 14px;
    margin-top: 6px;
}

a.item-info-action {
    color: #000000;
    font-size: 1.25em;
    font-weight: 600;
    text-decoration: underline;
    text-decoration-style: dotted;
    text-underline-offset: 0.3em;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 8px;
}

a.item-info-action:hover {
    text-decoration-style: solid;
}

/* Pipe divider between the two equally-weighted actions ("Apply the changes | Ignore").
   The dismiss link (`.item-info-dismiss`) is deliberately styled identically to the
   primary — no dimming — so both read as peers; only this pipe separates them. Muted
   grey + normal weight so the separator does not compete with the links; sized to match
   so it aligns on the row's centre line. Non-interactive. */
.item-info-action-divider {
    color: #999999;
    font-size: 1.25em;
    font-weight: 400;
    user-select: none;
}

.item-info-action-icon {
    width: 1em;
    height: 1em;
    flex-shrink: 0;
}

/* ─────────────────────────────────────────────────────────────────────────
   Banner boxes — compact, one-line attention prompts (sibling of .item-info
   but without the row/col Bootstrap chrome). Same color-pair pattern as
   .item-info: a darker shade for the border + title text, a lighter shade
   for the background. Add new variants by defining `.banner-box.<name>`
   (background + border) and `.banner-box.<name> .banner-box-title` (title
   color).
   ──────────────────────────────────────────────────────────────────────── */
.banner-box {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 30px;
    padding: 15px;
    border-style: solid;
    border-width: 1.5px;
    border-radius: 0.5em;
    margin: 10px 2px;
    font-size: 1.3em;
}

.banner-box-icon {
    width: 40px;
    height: 40px;
    flex-shrink: 0;
}

.banner-box-title {
    font-weight: bold;
    font-size: 1.3em;
    letter-spacing: 1px;
}

.banner-box-text {
    flex: 1;
    min-width: 0;
    font-size: 1.3em;
}

.banner-box-actions {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    font-size: 1.2em;
}

a.banner-box-action {
    color: #000;
    font-weight: 600;
    text-decoration: underline;
    text-decoration-style: dotted;
    text-underline-offset: 0.45em;
    cursor: pointer;
}

a.banner-box-action:hover {
    text-decoration-style: solid;
}

.banner-box-separator {
    color: #888;
}

/* Wizard variant — stripped to just the two action links, left-aligned, with no box chrome (no
   border / background / icon / title / body). The .banner-box base still supplies the flex row +
   padding + margin so the prompt keeps its place; the links sit at the start, where the page's other chrome
   (the tab strip, the switchers) begins. `justify-content: flex-start` restates the flex default and is kept
   deliberately rather than dropped: it is what stops a future justify-content on .banner-box (added for the
   variants that DO have a box) silently re-aligning this one.
   The 15px inset the base supplies lands the links on the module cards' own text column rather than
   on their border, which is the line the prompt is talking about. */
.banner-box.wizard {
    background-color: transparent;
    border: none;
    justify-content: flex-start;
    color: var(--color-black);
}

.banner-box.wizard .banner-box-separator {
    color: var(--color-black);
}

/* Warning variant — yellow (caution, not error). Colors live in ChartPalette.cs. */
.banner-box.warning {
    background-color: var(--color-banner-warning-light);
    border-color: var(--color-banner-warning-dark);
}

.banner-box.warning .banner-box-title {
    color: var(--color-banner-warning-dark);
}

/* Read-only variant — violet (locked / AI-owned, not a caution). Shown by
   DesignComponent when the active plan is AI-owned (read-only). Distinct
   from the yellow warning tone so "this is locked, not something is wrong"
   reads immediately. Follows the house 2-colour pattern: one dark shade
   (#5127a8) for both border and title, one light shade for the background. */
.banner-box.readonly {
    background-color: #f3eefc;
    border-color: #5127a8;
}

.banner-box.readonly .banner-box-title {
    color: #5127a8;
}

/* Success variant — green (a confirmation, not a caution). Follows the house 2-colour pattern:
   one dark shade (#1e7a34) for border + title, one light shade (#e6f4ea) for the background.
   Shown transiently by ImportConfirmationBanner after a showcase import. */
.banner-box.success {
    background-color: #e6f4ea;
    border-color: #1e7a34;
}

.banner-box.success .banner-box-title {
    color: #1e7a34;
}

/* ─────────────────────────────────────────────────────────────────────────
   Read-only plan skin (an AI-owned plan, or a shared project being visited).

   Emitted as `.plan-readonly` on each read-only plan region (module,
   goal, what-if card; sweeps content). The plan stays fully readable at
   FULL COLOUR — only its editing is removed. This is deliberately NOT the
   greyed `.disabled` skin: a toggled-off control must still look different
   from a read-only-but-active one, so read-only never adds `.disabled` and
   never changes colour. The server (RawEditEventsApplier Gate A, and the
   ingest ownership gate for a visit) is the real wall; this layer is UX only.

   No read-only surface DRAWS a control any more: module, goal and what-if
   cards and the Sweeps editor render every value as text, every on/off switch
   or optional checkbox as nothing, and anything switched off not at all (see
   DF.Claude/Features/ReadOnlyPlanRendering.md). The pointer-events rule that
   used to make drawn controls inert is gone with them: it blocked the mouse and
   not the keyboard. What remains is the rule below, which hides the edit
   affordances (display: none also takes them out of the tab order). */

/* Edit affordances only make sense while editing: hide them outright. Covers
   the rename / move / delete / clone icon buttons (.icon-button), the
   expandable "Add ..." links (.module-block-link), and the menu-items add
   tiles (.menu-tiles-row + its caption). Design-level add-module / add-goal /
   add-what-if catalogues and the Sweeps "New sweep" button are suppressed at
   their render site instead (DesignComponent / WhatIfsPanel / SweepsComponent).
   `!important` is intentional: these affordances carry inline display / style
   in some states (compact-pill, toggled), and this must win over them. */
.plan-readonly .icon-button,
.plan-readonly .module-block-link,
.plan-readonly .menu-tiles-row,
.plan-readonly .menu-tiles-caption {
    display: none !important;
}

/* ─────────────────────────────────────────────────────────────────────────
   Wizard existing-instance tile — top section of the menu, one tile per
   module instance already in the plan.
   ──────────────────────────────────────────────────────────────────────── */
.wizard-existing-tile {
    padding: 16px 8px;
    background-color: #fff;
    border: 1px solid #cbd5e0;
    border-radius: 8px;
    cursor: pointer;
    text-align: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    font-size: 0.95em;
    color: #333;
    position: relative;
    min-width: 120px;
    transition: background-color 0.15s ease, box-shadow 0.15s ease, color 0.15s ease;
}

/* The shared clickable-card treatment (see --hover-card-ring at the top of this file),
   split in two because this is the one card in the family carrying STATE colour.

   The ring is unconditional: it sits OUTSIDE the border, so it composes with the plain,
   `.completed` (green) and `.started` (amber) variants alike without touching any of
   them. The border is deliberately left alone -- the old `border-color: #000` here
   overrode the green / amber border, so a completed tile lost half its state colour
   simply by being pointed at. */
.wizard-existing-tile:hover {
    color: #000;
    box-shadow: var(--hover-card-ring);
}

/* The fill, only where there is no state fill to paint over. A plain tile is white and
   has nothing to lose, so it takes the full family treatment and reads exactly like a
   picker card; a state tile keeps its own green / amber and leans on the ring and the
   text darkening instead.

   Without this the plain tile was left with the ring alone against #fff, roughly 1.24:1
   and close to invisible -- the ring is a companion to the fill in every other consumer,
   never a substitute for it. */
.wizard-existing-tile:not(.completed):not(.started):hover {
    background-color: var(--color-pill-action-light);
}

.wizard-existing-tile.completed {
    border-color: green;
    border-width: 2px;
    background-color: #E5FFDA;
}

/* Started but not finished (in progress, or left via "Exit now"): amber, distinct from the green "done". */
.wizard-existing-tile.started {
    border-color: #e0a500;
    border-width: 2px;
    background-color: #FFF6E5;
}

/* ─────────────────────────────────────────────────────────────────────────
   Wizard breadcrumb pill — clickable summary of an answered step. Same
   "gray idle, black on hover" treatment as the .hovering-effects
   breadcrumbs in the Results details header, just packaged as a pill.
   ──────────────────────────────────────────────────────────────────────── */
.wizard-breadcrumb-pill {
    display: inline-block;
    padding: 4px 10px;
    border: 1px solid #999999;
    border-radius: 0.5em;
    color: var(--color-text-secondary);
    background-color: #f6f6f6;
    font-size: 1.1em;
    cursor: pointer;
    transition: color 80ms ease, border-color 80ms ease;
}

.wizard-breadcrumb-pill:hover {
    color: #000000;
    border-color: #000000;
}

/* Bigger, easier-to-hit wizard nav / gate buttons (Back, Exit now, Next, Finish, Yes, No). The buttons
   already carry Bootstrap's .btn-lg; this scopes an extra bump to the wizard so its primary actions are a
   comfortable tap target without touching button sizing anywhere else. Specificity (0,2,0) beats .btn-lg.

   THE NEWER IDIOM IS `.profile-page .btn-lg`, which does the same job by setting Bootstrap's own
   --bs-btn-* custom properties instead of re-declaring these properties; prefer it for anything new.
   It needs no specificity argument (the variables cascade and Bootstrap's rule consumes them) and it
   keeps .btn-lg's border-radius, which this rule silently inherits only because it does not restate it.
   Left as-is deliberately: converting it moves wizard button metrics for no behavioural gain, which is
   CLAUDE.md rule 24's trade in miniature. */
.wizard-session .btn {
    font-size: 1.2rem;
    padding: 0.9rem 2rem;
    min-width: 130px;
}

/* The live consequence line (WizardLoop.md §A): a quiet grey parenthetical aside below the input, the same
   text size as the helper paragraph below it. Not the bold blue payoff of an earlier draft. */
.wizard-consequence {
    color: #777777;
    font-size: 1.25em;
    text-align: center;
}

/* The menu loop's tile-grid gate (WizardLoop.md T5): the module-picker tiles (.add-module-button /
   .module-image, reused verbatim) floated inside the wizard question card. Centre them and give a little
   breathing room above (below the heading / helper) and below (before the Done button). The tile look itself
   is inherited from .menu-tiles-row. */
.wizard-menu-tiles {
    justify-content: center;
    margin-top: 16px;
}

/* Fade + slide-in for the suggestion pill — same easing family as
   @keyframes module-enter so a freshly-cached pill arrives the way a freshly-
   added module does. */
@keyframes item-info-suggestion-enter {
    from { opacity: 0; transform: translateY(-6px); }
    to { opacity: 1; transform: translateY(0); }
}

.control-cell.item-info.suggestion {
    animation: item-info-suggestion-enter 0.4s ease-out;
}

/* Two-phase exit: 0–50% fades + slides the pill up (mirror of -enter), then
   50–100% collapses the layout space (max-height + paddings + margins +
   border to 0) so the slot the pill occupied closes immediately rather than
   sitting empty until the listener removes the cache entry — on a slow
   re-pricing that empty gap was visible for hundreds of ms. The 50–100%
   phase only specifies the *target* values; CSS interpolates from the
   natural computed style at 50%, so the rule's `max-height: 500px` ceiling
   works for any pill height up to that. `forwards` keeps the final
   collapsed-and-invisible state painted until the listener removes the
   entry. */
@keyframes item-info-suggestion-exit {
    0% { opacity: 1; transform: translateY(0); }
    50% { opacity: 0; transform: translateY(-6px); }
    100% {
        opacity: 0;
        transform: translateY(-6px);
        max-height: 0;
        padding-top: 0;
        padding-bottom: 0;
        margin-top: 0;
        margin-bottom: 0;
        border-top-width: 0;
        border-bottom-width: 0;
    }
}

.control-cell.item-info.suggestion.dismissing {
    animation: item-info-suggestion-exit 0.4s ease-out forwards;
    /* A pill fading out must not be clickable: the 0.4s exit animation keeps the element (and its
       Apply / Ignore buttons) in the DOM, invisible-but-live for the last 200ms at opacity 0, so
       without this a click during the fade could apply a pill that was already dismissed on an edit. */
    pointer-events: none;
    max-height: 500px;
    overflow: hidden;
}

.control-row {
    /* nothing yet */
}

/* Light-weight visual grouping for flat lists of primitive controls. Emitted
   above a primitive row by ItemsBuilder.OpenControlRow when the item carries
   [GroupSeparatorAbove(caption?)]. The line spans the full row width; an
   optional bold caption sits in the line. Caption's background pulls from
   the inherited --row-stripe custom property set by the enclosing
   .module-block (even / odd) so the caption visually masks the line ends
   regardless of which stripe the composite happens to render against. The
   renderer adds a .disabled class to both elements when any ancestor of the
   primitive is toggled off, mirroring the existing .control-label.disabled
   contract — caption greys out when the enclosing composite is off. */
.group-separator {
    border-top: 1px solid #DDDDDD;
    margin: 22px 0 16px 0;
    padding: 0;
    position: relative;
    height: 0;
}
.group-separator.disabled { border-top-color: #EEEEEE; }
.group-separator-caption {
    position: relative;
    top: -0.7em;
    margin-left: 12px;
    padding: 0 6px;
    background: var(--row-stripe, #FFFFFF);
    color: #444444;
    font-size: 1.1em;
    font-weight: bold;
}
.group-separator-caption.disabled { color: #BBBBBB; }

.control-label {
    display: block;
    width: 100%;
    text-align: center;
    justify-content: right;
    margin-top: auto;
    margin-bottom: auto;
    padding-right: 8px;
    padding-left: 2px;
}

.control-label.is-invalid {
    border-color: #dc3545;
}

.control-label-value {
    text-align: left;
    display: block;
    width: 100%;
    padding: 0.375rem 0.75rem;
    font-size: 1.2em;
    font-weight: 400;
    line-height: 1.5;
    color: #212529;
}

/* Read-only text labels only (Current age, Longevity, Simulation length, ...). Tighter horizontal padding
   so the value sits closer to the left, reading as an addendum to the control above. The compound selector
   keeps it winning over .control-label-value regardless of stylesheet order, and scopes it to read-only
   values without touching the shared class the info cards / suggestion pills / action text use. */
.control-label-value.control-readonly-value {
    padding: 0.375rem 0.2rem;
}

/* A read-only plan draws each control as its value (ReadOnlyPlanRendering.md), one cell per control, and a
   row of several controls (a date's day, month and year; an amount and its period) must read as ONE phrase:
   "26 September 2016", not three values spread across the grid columns their dropdowns filled. So a
   read-only value cell (RendererDesignControlsExtensions.ReadOnlyValueCellClass) sizes to its text instead
   of its column, and may still shrink and wrap (flex-shrink 1, min-width 0) so a long value never
   overflows. The row's label column is untouched, so the first value still starts where it always did. */
.control-row > .control-cell-readonly {
    flex: 0 1 auto;
    width: auto;
    min-width: 0;
}

/* Between two consecutive values the gap is one space: the first value's own 0.2rem padding plus the cell's
   2px, with the second's leading padding removed. */
.control-row > .control-cell-readonly + .control-cell-readonly {
    padding-left: 0;
}

.control-row > .control-cell-readonly + .control-cell-readonly > .control-readonly-value {
    padding-left: 0;
}

/* An EXPLANATION box: read-only prose that describes the control DIRECTLY ABOVE it, rather than a
   computed field standing on its own. Two surfaces carry it -- an ExplanationItem (the Life-expectancy
   longevity line, a deposit breakdown, ...) and the enum-value description addendum under a dropdown.
   The card plus the caret are what say "this is about the thing above", which a plain read-only row
   could not; see DF.Claude/Features/ExplanationBoxes.md for the authoring contract.

   It deliberately does NOT also carry .control-readonly-value: that class exists to pull a bare value
   left with tight padding, and the box owns its own padding. Both compound selectors have the same
   (0,2,0) specificity, so carrying both would make the winner depend on source order. */
.control-label-value.explanation-box {
    /* The fill and the rim are declared ONCE as custom properties because each is used TWICE and the two
       uses must stay equal by construction: the caret's inner triangle is the box's fill (it masks the top
       border) and its outer triangle is the box's rim. A literal repeated in both places is coupled only by
       a comment, and a comment does not fail a build. The disabled skin re-points the same two properties,
       so every caret rule below is written once and never needs a .disabled twin. */
    --explanation-box-fill: #FFFFFF;
    /* Deliberately NOT --color-module-border (#BBBBBB, what the module cards use). The rim is DOTTED here,
       and that is the point: it says at a glance that this row is prose about the control above rather than
       another control the user can type into. A dotted rule reads heavier than a solid one of the same
       weight, so at 2px it needs the lighter tone to carry the same presence, hence the decoupling. */
    --explanation-box-rim: #CCCCCC;

    position: relative;
    margin-top: 9px;      /* the gap the caret occupies -- it is drawn outside the box, above its top edge */
    margin-bottom: 7px;   /* the box is prose, not a control row, so it needs its own breathing space below
                             it; without this it crowds whatever row follows. */
    padding: 5px 10px;
    border: 2px dotted var(--explanation-box-rim);
    border-radius: 10px;
    background-color: var(--explanation-box-fill);
}

/* The caret is TWO stacked triangles, not one. ::before draws it in the border colour; ::after redraws it
   2px lower in the fill colour, which both leaves a 2px rim (so the caret continues the box's own border
   rather than floating unoutlined) and masks the top border where the caret meets it (so no line runs
   across the caret's base). The onboarding hint's caret (.onboarding-hint.up::before) is a single triangle
   because that box is solid black with no border to continue.

   THE OFFSET MUST TRACK THE BORDER WIDTH. It is the difference between the two `top` values below, and it
   is what makes the caret's rim the same thickness as the box's: at 1px against a 2px border the caret
   reads as a thinner outline pinned to a thicker card, and the ::after base no longer covers the whole
   border so a hairline survives across the caret's base. If the border width changes, change both.

   The caret's rim is SOLID while the box's is dotted, which is not an oversight: a CSS triangle is a border
   trick, so it has no dash pattern to inherit, and a 16px pointer wants to read as one shape anyway. */
.control-label-value.explanation-box::before,
.control-label-value.explanation-box::after {
    content: '';
    pointer-events: none;
    position: absolute;
    /* Centred unless the box was handed a measured offset. A control that fills the value cell wants the
       middle; a narrower one (a Size5 integer box, or a dropdown whose width follows its option labels)
       hands over its own midpoint as --explanation-caret. Measured per box in ItemsBuilder.CaretOffsetStyle,
       never declared, because the dropdown case moves when its labels do. */
    left: var(--explanation-caret, 50%);
    transform: translateX(-50%);
    width: 0;
    height: 0;
    border-left: 8px solid transparent;
    border-right: 8px solid transparent;
}

.control-label-value.explanation-box::before {
    top: -9px;
    border-bottom: 9px solid var(--explanation-box-rim);
}

.control-label-value.explanation-box::after {
    top: -7px;   /* 2px below ::before's -9px, matching the 2px border above */
    border-bottom: 9px solid var(--explanation-box-fill);
}

/* Switched-off composite: the card recedes to the page's own surface tone with a lighter rim, so it
   reads as inert rather than as a live callout whose text merely greyed. It re-points the two custom
   properties rather than restating the four colour sites, so the caret cannot keep the live colours the
   box has just dropped -- the failure mode a per-site override invites. */
.control-label-value.explanation-box.disabled {
    --explanation-box-fill: var(--color-surface-light);
    --explanation-box-rim: #DDDDDD;
}

.dropdown-control {
    border: 1px solid #ced4da;
    background-color: white;
}

.dropdown-control.disabled {
    background-color: #e9ecef;
    color: #BBBBBB;
}

.dropdown-icon {
    height: 20px;
    margin-right: 15px;
    margin-bottom: 3px;
    vertical-align: middle;
    min-width: 20px;
}

.smaller-font { font-size: 0.9em; }

label.disabled { color: #BBBBBB; }

select.disabled {
    color: #BBBBBB;
    background-image: none; /* caret disappear */
}

span.disabled { color: #BBBBBB; }

input.disabled { color: #BBBBBB; }

.icon-button:hover { cursor: pointer; }

/* The wordmark's capitals (the D and F of Diligent Finance, and Copyright's C): larger AND bold since
   2026-09-24 (user), so the brand name reads as a mark rather than as a sentence. */
.first-letter {
    font-size: 1.3em;
    font-weight: bold;
}

/* --------------------------------------------------------*/
/* SPECIAL */
/* --------------------------------------------------------*/

/* 
    Remove arrows in number only text boxes
    https://www.w3schools.com/howto/howto_css_hide_arrow_number.asp 
*/

/* Chrome, Safari, Edge, Opera */

input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

/* Firefox */

input[type=number] { -moz-appearance: textfield; }

#add-module-button {
    position: fixed;
    bottom: 5px;
    left: 5px;
    z-index: 9000;
}

/* --------------------------------------------------------*/
/* BOOTSTRAP */
/* --------------------------------------------------------*/

.container {
    max-width: 1600px;
}

.dropdown-menu a:hover { cursor: pointer; }

.dropdown-item a:hover { cursor: pointer; }

/* --------------------------------------------------------*/
/* MODALS */
/* --------------------------------------------------------*/

/* ONE SIZE FOR EVERY MODAL, declared once and read by every shape below.

   A modal asks the reader to make a decision, and this audience skews toward readers with presbyopia, so
   modal prose is set well above the page's base. That reasoning was written for the change-of-sign-in
   dialog alone and applied to it alone, which left the popups beside it on the same page (sign out,
   delete profile, download your data) at the old 1.2em while the one next to them was 1.45em. A reader
   met two sizes in one page for no reason they could see.

   IT IS A VARIABLE RATHER THAN A REPEATED LITERAL because several selectors genuinely need it: modals
   render inside `.modal-body`, and the identical consent markup is ALSO served by the landing door's
   sign-in panel, which has no `.modal-body` anywhere above it. Several DOM shapes, one decision, so the
   decision is named once and no copy can drift from the others.

   IT IS rem RATHER THAN em, AND THAT IS THE LOAD-BEARING HALF. An em is not a size, it is "45% bigger
   than whatever I hang off", so the door and the in-app dialog agreed only by the COINCIDENCE that
   neither of their ancestors happens to scale today. The first scaled container above either one would
   have split them silently. This is the argument --tooltip-font-size makes a few hundred lines above, for
   the same reason: a popup is not part of its trigger and must not inherit its trigger's scale.

   1.7rem, MEASURED rather than picked. Against a 10.8px root that is 18.36px, and the anchor is the
   landing page's own copy: its supporting paragraphs render 16.63px and 18.14px and its hero 23.76px,
   while this dialog rendered 15.66px. The surface asking the single most consequential question on the
   site was set BELOW every paragraph of marketing copy that leads a visitor to it. 1.7rem is 18.36px,
   which just CLEARS that band's 18.14px top rather than landing on it, so the decision now reads at least
   as large as the invitation and never smaller.

   Retuning the whole family is this one line.

   THE ACTION BUTTONS READ IT TOO, and that is not a stray inclusion. A button label is not prose, so the
   first cut of this change deliberately left them out, and that was wrong: it did not leave the
   relationship alone, it INVERTED it. The door's card read explainer 15.12px then button 15.66px, the
   action very slightly louder than the sentence describing it; sizing only the prose would have put the
   button 15% BELOW its own explainer, which is the defect this change exists to remove, newly created on
   the most consequential control on the page. Equal is the honest resting place: an action is never
   quieter than the copy that explains it.

   --modal-prose-line-height is the same story one property along, and the tooltip pair above is again the
   precedent. .lp-signin-error pinned 1.45, .lp-tier-detail pinned 1.5, and everything else silently
   inherited the 1.2 set on <body> for dense control rows. Unifying only the size would have left three
   leadings on one dialog. 1.5 is the right one to keep: 1.2 is a global for tight rows, and this is
   multi-paragraph prose that a reader with presbyopia has to track across. */
:root {
    --modal-prose-size: 1.7rem;
    --modal-prose-line-height: 1.5;
}

.modal-body p {
    margin: 10px;
    font-size: var(--modal-prose-size);
    line-height: var(--modal-prose-line-height);
}

/* A modal that lists things reads as one block of prose, so its list items must be the size of the
   paragraph introducing them. An li is not a p, so without this it stays at the base font and looks
   shrunken next to the sentence above it, which is the same defect .help-page-content fixed for help
   pages (see its comment further down). Set on the li rather than the ul so a nested list does not
   compound.

   THE NESTED RULE BELOW IS INERT WHILE THE TOKEN IS ABSOLUTE, and that is worth saying so nobody reads it
   as load-bearing today: a nested li re-matches this rule and an absolute rem cannot compound anyway. It
   is kept, and pinned, because it is the thing that stops a nested list ballooning the day the token ever
   returns to an em. */
.modal-body li {
    font-size: var(--modal-prose-size);
    line-height: var(--modal-prose-line-height);
}

.modal-body li li {
    font-size: 1em;
}

/* NEVER SMALLER THAN THE PROSE IT WARNS ABOUT. This was a flat 1.3em, which sat above the old 1.2em
   body and would have sat BELOW it once the body moved: the one line on the surface that must not be
   skimmed, rendered smaller than the paragraph explaining it. */
.modal-warning {
    color: var(--color-red);
    font-weight: bold;
    font-size: var(--modal-prose-size);
}

/* A CONSENT dialog: one that asks the user to agree to something consequential before it happens, and
   carries a checkbox that is a decision in its own right. Today that is the change-of-sign-in-method
   dialog (IdentityTransitionModal), which can close a profile and move its projects.

   IT NO LONGER SIZES ITSELF ABOVE the ordinary modal prose, and this paragraph used to say it did. The
   presbyopia argument that justified 1.45em here is not about this dialog, it is about who uses the
   product, so it now sets the size for every modal and this block reads the same variable. What survives
   is the SELECTOR: the consent markup is served on a surface with no `.modal-body` above it, so it still
   needs a rule of its own to be reached at all.

   THE CLASS IS DOUBLED TO BUY SPECIFICITY, and the repetition is deliberate rather than a typo. A bare
   `.modal-consent p` ties with `.modal-body p` at (0,1,1), so which one won would depend on source order,
   which is exactly the kind of thing a later reorganisation breaks silently. `.modal-consent.modal-consent`
   matches precisely the same elements at (0,2,1) and wins that tie outright.

   It replaced a `.modal-body` ancestor, which bought the same specificity and quietly made the rule
   CONDITIONAL ON THE SURFACE. The identical consent markup is also served by the landing door's sign-in
   panel (_SignInPanel.cshtml), which has no `.modal-body` anywhere above it, so the copy this comment
   argues must not be economised on rendered there at BASE size while the in-app dialog got 1.45em: the
   accessibility decision applied on the surface that was walked in a browser and not on the one that was
   not. Naming an ancestor to win a tie makes every future surface a maintenance step nobody is told
   about; doubling the class asks nothing of them. */
.modal-consent.modal-consent p,
.modal-consent.modal-consent li,
.modal-consent.modal-consent .modal-consent-label,
.modal-consent.modal-consent .modal-warning {
    font-size: var(--modal-prose-size);
}

/* The fork rows state two outcomes as a list, so the list has to sit at the prose's size and rhythm. The
   `li` above is named for the same reason `.modal-body li` exists at all: an li is not a p, so without it
   the bullets stay at the base font and look shrunken next to the sentence introducing them, which is
   exactly the defect the checkbox label had in this dialog's first version. */
.modal-consent.modal-consent ul {
    margin: 0.6em 0 0 0;
    padding-left: 1.4em;
}

.modal-consent.modal-consent li {
    margin-bottom: 0.5em;
}

/* CENTRED, unlike an inline toggle appended to a sentence (ProjectShareModal's share checkbox, which
   reads as a property of the prose it sits in). A consent row is its own decision, so it is given its
   own line and its own weight rather than trailing off the end of a paragraph. */
.modal-consent-row {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.6em;
    margin-top: 1.2em;
}

/* The label is clickable along with the box: a 28px target is fine for a mouse and mean for anyone
   whose hands are less steady than the designer's. */
.modal-consent-label {
    cursor: pointer;
    user-select: none;
}

.modal-content {
    border: white 10px solid;
    background-color: #EEEEEE;
    border-radius: 0.5em;
}

.modal-title {
    font-size: 2.4em;
    font-weight: bold;
    font-variant-caps: petite-caps;
    letter-spacing: 1px;
    word-spacing: 2px;
}

/* A DIALOG'S ACTION BUTTON, on BOTH surfaces that show one. Same two-DOM-shapes argument as
   --modal-prose-size a few hundred lines up: an in-app dialog renders its footer inside `.modal-footer`,
   and the landing door serves the identical pair with no `.modal-footer` anywhere above it. One decision,
   named once, so neither copy can drift from the other. They drifted before this rule existed: measured
   2026-09-24, the in-app pair were 38px tall and the door's 56px, for the same question.

   SIZED WITH THE PROSE for the reason --modal-prose-size gives: at 1.2em this was 12.96px against the
   body's 15.66px, and sizing only the prose would have widened that gap from 17% to 29%. The button is
   the thing the reader has to act on.

   44px MINIMUM, the touch target WCAG 2.5.5 asks for, which neither surface met: the in-app pair came
   out 38px from Bootstrap's default padding (btn-md carries no rule of its own), and the door's 56px is a
   landing-page CTA height rather than a dialog's. It was 48px from 2026-09-24 to 2026-09-25, "the floor
   with margin"; the user found the dialogs' buttons oddly tall beside the page's own, which on /Profile
   are 45px at nearly the same type size (btn-lg), so the floor itself is the size now and a dialog's
   buttons match the page they sit over. */
.modal-footer .btn,
.lp-signin-consent-actions .btn {
    font-size: var(--modal-prose-size);
    min-height: 44px;
}

/* ROOM ABOVE A DIALOG'S BUTTONS (user, 2026-09-25): Bootstrap's footer padding left them 8 to 12px under
   the last input or card, close enough to read as part of it. 20px separates the question from the
   answer to it, on both surfaces, for the same two-DOM-shapes reason as the rule above (the door's twin
   is the margin-top on .lp-signin-consent-actions). */
.modal-footer {
    padding-top: 20px;
}

.modal-header button.btn-close {
    font-size: 2em;
    padding: 0 20px 0 0;
    outline: none;
}

/* --------------------------------------------------------*/
/* FORM */
/* --------------------------------------------------------*/

form { 
    justify-content: space-evenly; 
}

div.help-icon {
    background-color: #fcfcfc;
    border-radius: 12px;
    padding: 3px;
    margin-left: 5px;
    border: #aaaaaa solid 1px;
    max-height: 25px;
    opacity: 0.8;
}

div.help-icon.disabled {
    opacity: 0.4;
}

img.help-icon {
    margin: auto;
    height: 1.2em;
}

div.form-group {
    padding: 0;
    margin-top: 7px;
    margin-bottom: 7px;
}

.input-group-text {
    font-size: 0.9em;
    padding: 0 7px 0 7px;
    color: #777;
}

.form-control { 
    font-size: 1.2em; 
}

.form-control:focus {
    color: black;
    font-weight: 600;
}

.form-control.is-invalid {
    color: #DC3545;
    font-weight: 600;
}

.form-control-error {
    color: #DC3545;
    font-size: 1.3em;
    font-weight: bold;
    text-align: left;
    margin-bottom: 10px;
    margin-top: 5px;
}

/* Suggestible money items: the chevron addon next to the textbox is a
   narrow native <select> rendered as chevron-only. The textbox owns the
   value display; the chevron is purely a one-tap shortcut to a list of
   common values. On mobile this gives the iOS wheel / Android bottom
   sheet for free without invoking the keyboard.
   High-specificity selector defeats Bootstrap's default .form-select
   width:100% inside an input-group. */
.input-group > select.form-select-suggestions {
    flex: 0 0 auto;
    width: 2.5rem;
    min-width: 2.5rem;
    padding: 0;
    background-position: center center;
    background-size: 12px 12px;
    color: transparent;
    -webkit-text-fill-color: transparent;
}

.input-group > select.form-select-suggestions option {
    color: black;
    -webkit-text-fill-color: black;
    padding: 0.35rem 1rem;
    padding-inline-start: 1rem;
    padding-inline-end: 1.5rem;
    text-indent: 0.5rem;
}

/* THE AMOUNT SLIDER: the row that appears under a focused numeric field (money, a rate or a count) so its
   value can be set with the mouse alone (DF.Claude/Features/AmountSlider.md).

   An in-flow sibling ROW, not an overlay, and the whole reason is that this app clips overlays in three
   places -- the wizard's overflow-y backdrop, comparison mode's overflow-x columns, and .module-body's
   overflow:hidden on mobile. So there is no position:absolute here, no z-index, and nothing to clip.
   It borrows the shape of the sub-label row above it (an empty col-sm-3 and a col-sm-9 value cell). */
.amount-slider-row {
    /* Sits a little BELOW the control it belongs to, so the two read as a control and its scale rather than
       as two rows crowding each other. */
    margin-top: 5px;
}

.amount-slider {
    display: block;
    width: 100%;
}

/* The track fills its cell, and the cell is given the same col-sm-N as the control above (see
   AmountSliderBinding.Columns), so it lines up exactly with the textbox whatever that control's width. The
   endpoint amounts live on their own line underneath (.amount-slider-scale) rather than flanking the track,
   which is what used to steal the width and leave the slider short of the box. */
.amount-slider-input {
    display: block;
    width: 100%;
    margin: 0;
    /* The native skin is dropped entirely rather than tinted with accent-color, because the track and the
       thumb have to be sized INDEPENDENTLY here: a thin track under a full-size knob. accent-color can only
       recolour the native pair, so it cannot express that.

       THE HEIGHT IS LOAD-BEARING, and leaving it out is what put the knob inside the textbox above. Dropping
       the appearance drops the native metrics with it, so the input's box collapses to about the height of
       the track; the knob is then centred on a 3px line inside a 3px box and overflows its own element by
       most of its radius, upward, into the row above. 18px is the knob's 16 plus a pixel of slack each side,
       so the control cannot paint outside itself whichever way a browser positions the track. */
    height: 18px;
    -webkit-appearance: none;
    appearance: none;
    background: transparent;
    cursor: pointer;
}

/* THE TRACK: 5px, and TWO-TONE -- dark behind the knob, light ahead of it, the near-universal convention.
   It adds no claim the control was not already making (the knob's position is visible either way); it just
   makes that position readable at a glance without reading the number, which is the whole point of offering a
   slider as the primary input. Kept MONOCHROME rather than given a brand fill on purpose: this track's top is
   a derived figure that grows when you reach it, not a goal, so a saturated progress bar would imply a
   "percentage complete" that means nothing here.

   THE SPLIT POINT IS THE KNOB'S CENTRE, not a plain percentage of the width. The knob's centre travels from
   8px (its radius) to 8px short of the far end, so a bare `60%` would sit up to 8px away from the knob at the
   extremes and the fill would visibly lead or lag it. --amount-slider-fill is a unitless 0..1 fraction the
   component writes on the element, and the calc below maps it onto that real travel.

   Both vendor prefixes are required and cannot be merged into one rule: a browser drops the whole selector
   list when it does not recognise one entry, so a combined rule would silently style nothing anywhere. */
.amount-slider-input::-webkit-slider-runnable-track {
    height: 5px;
    border-radius: 2.5px;
    background: linear-gradient(
        to right,
        var(--color-black) 0 calc(8px + ((100% - 16px) * var(--amount-slider-fill, 0))),
        var(--color-module-border) calc(8px + ((100% - 16px) * var(--amount-slider-fill, 0))) 100%);
}

.amount-slider-input::-moz-range-track {
    height: 5px;
    border-radius: 2.5px;
    background: linear-gradient(
        to right,
        var(--color-black) 0 calc(8px + ((100% - 16px) * var(--amount-slider-fill, 0))),
        var(--color-module-border) calc(8px + ((100% - 16px) * var(--amount-slider-fill, 0))) 100%);
}

/* THE KNOB: 16px, the native diameter, kept deliberately. A thin track is easier to read; a small knob is
   harder to grab, and the pointer target is the thing a user actually fights with. */
.amount-slider-input::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 16px;
    height: 16px;
    border: none;
    border-radius: 50%;
    background: var(--color-black);
    /* Centres the knob on the track: (track height - knob height) / 2 = (5 - 16) / 2. WebKit lays the thumb
       out from the track's TOP edge, so without this it hangs below the line. Firefox centres it itself,
       which is why the -moz- rule below carries no margin. This offset only stays inside the control because
       the input above declares a height that can hold the knob. */
    margin-top: -5.5px;
}

.amount-slider-input::-moz-range-thumb {
    width: 16px;
    height: 16px;
    border: none;
    border-radius: 50%;
    background: var(--color-black);
}

.amount-slider-input:focus-visible {
    outline: 2px solid var(--color-blue-accent);
    outline-offset: 4px;
}

/* The two endpoint amounts, one at each end under the track. Two elements rather than one "0 - 2,500"
   string: a dash standing in for "to" is prose punctuation and forbidden in user-visible text
   (CLAUDE.md rule 16). Non-selectable because they are a scale, not a value to copy. */
/* NOT DRAWN ON A COARSE POINTER, and that is the honest reading of what this feature does NOT yet do.
   The design records that the TRIGGER is wrong on touch (focusing a numeric field raises the OS keyboard,
   the very thing the slider exists to replace, so the slider appears behind it), and says plainly that touch
   is unsolved. Rendering it anyway put a 16px knob in an 18px row on a phone, far under a usable touch
   target, so the control was there, unusable, and covered. Suppressing it means the textbox behaves exactly
   as it did before the slider existed. (hover: none) is this stylesheet's existing test for that.

   The C# still opens its session and the probe still runs, which is a few hundred microseconds on a
   deliberate focus and is deliberately not special-cased: a media query is a rendering fact, and teaching
   the component to ask about the pointer would put a second, disagreeing notion of "is touch supported" in
   the code. When touch IS solved this rule is the one thing that has to go. */
@media (hover: none) {
    .amount-slider-row {
        display: none;
    }
}

.amount-slider-scale {
    display: flex;
    justify-content: space-between;
    width: 100%;
    margin-top: 2px;
}

.amount-slider-endpoint {
    font-size: 0.9em;
    /* --color-text-secondary (#666666, 5.74:1), the house grey for de-emphasised text, shared with
       .strategy-strip-legend and .div-graph-description. It was --color-gray-steel, which is #F5F7FA:
       a near-white SURFACE token used as a TEXT colour, so these labels rendered at 1.07:1 and were
       invisible on a white page, on the one control whose job is to say what range you are dragging
       within. The same misuse .strategy-strip-label carried and was fixed for in Sep 2026: the name
       says steel, and ChartPalette's own line defining the token says what it is, a panel and card
       BACKGROUND. --color-text-secondary was added in Sep 2026 so an author of quiet text has a TEXT
       token to reach for; TestCssContrast now fails the build if a background-role token reappears in
       any color: rule here. */
    color: var(--color-text-secondary);
    white-space: nowrap;
    user-select: none;
}

/* The sweep editor's two-knob range: one rail, two native range inputs stacked on it. Each input stays a real
   range control (keyboard, screen reader), laid over the same rail with its own track hidden and ONLY its knob
   taking the pointer, so the two cannot fight for a drag. The rail paints the fill BETWEEN the knobs from two
   0..1 fractions the component writes, mapped onto the knob's real travel exactly as .amount-slider-input maps
   its single fill (8px inset at each end for the 16px knob's radius). */
.range-slider {
    display: block;
    width: 100%;
    min-width: 320px;
}

.range-slider-rail {
    position: relative;
    height: 18px;
}

.range-slider-rail::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 6.5px;
    height: 5px;
    border-radius: 2.5px;
    background: linear-gradient(
        to right,
        var(--color-module-border) 0 calc(8px + ((100% - 16px) * var(--range-slider-lo, 0))),
        var(--color-black) calc(8px + ((100% - 16px) * var(--range-slider-lo, 0))) calc(8px + ((100% - 16px) * var(--range-slider-hi, 1))),
        var(--color-module-border) calc(8px + ((100% - 16px) * var(--range-slider-hi, 1))) 100%);
}

.range-slider-input {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 18px;
    margin: 0;
    -webkit-appearance: none;
    appearance: none;
    background: transparent;
    pointer-events: none;
}

.range-slider-input::-webkit-slider-runnable-track {
    height: 5px;
    background: transparent;
}

.range-slider-input::-moz-range-track {
    height: 5px;
    background: transparent;
}

.range-slider-input::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 16px;
    height: 16px;
    border: none;
    border-radius: 50%;
    background: var(--color-black);
    margin-top: -5.5px;
    pointer-events: auto;
    cursor: pointer;
}

.range-slider-input::-moz-range-thumb {
    width: 16px;
    height: 16px;
    border: none;
    border-radius: 50%;
    background: var(--color-black);
    pointer-events: auto;
    cursor: pointer;
}

/* The ring goes on the THUMB, not the input: both inputs span the whole rail, so an outline on the input drew
   the same box for either knob and a keyboard user could not see which one the arrows would move. */
.range-slider-input:focus-visible {
    outline: none;
}

.range-slider-input:focus-visible::-webkit-slider-thumb {
    box-shadow: 0 0 0 3px var(--color-blue-accent);
}

.range-slider-input:focus-visible::-moz-range-thumb {
    box-shadow: 0 0 0 3px var(--color-blue-accent);
}

.range-slider-input:disabled::-webkit-slider-thumb {
    background: var(--color-gray-steel);
    cursor: default;
}

.range-slider-input:disabled::-moz-range-thumb {
    background: var(--color-gray-steel);
    cursor: default;
}

/* Same reason as .amount-slider-row above: a 16px knob is not a touch target and touch is unsolved for the
   sliders as a whole. The Start and End boxes stand alone on a coarse pointer, as they did before. */
@media (hover: none) {
    .range-slider {
        display: none;
    }
}

/* --------------------------------------------------------*/
/* LAYOUT */
/* --------------------------------------------------------*/

/* The narrow side column of an accent-style card header. Despite the name it is shared by six
   columns across three families: the module header's three (identity stack, action icons, move
   arrows), the Goal and What-If cards' action columns, and the Goal / What-If help affordance.
   Its justify-content packs all of them, with exactly ONE named exception below
   (.accent-tooltip-column, the single-icon help column). Adding a second exception puts the
   columns back into competition -- see TestCardSideColumnAlignment, which enforces base-plus-one
   over every rule naming either class, and the .module-identity-column note further down for the
   bug that motivated the guard. */
.module-side-column {
    display: flex;
    flex-direction: column;
    flex-wrap: wrap;
    align-content: center;
    justify-content: flex-start;
    align-items: center;
    gap: 8px;
}

/* The one side column holding a single centred icon rather than a stack: the Goal / What-If
   help affordance (AccentTooltip.RenderColumn). It was an inline justify-content on the element,
   which put a second packing declaration somewhere no stylesheet guard could see it; as a named
   modifier it is visible to the scan and says what it is. Compound selector (0,2,0) so it beats
   .module-side-column whatever the source order.

   Note this is a deliberate weakening of what an inline style gave: inline beat every author
   rule, (0,2,0) does not. A future higher-specificity rule that never names the class (say
   .some-card .col-2 { justify-content: ... }) would win here AND be invisible to the guard,
   which only inspects selectors mentioning .module-side-column. Nothing does today. */
.module-side-column.accent-tooltip-column {
    justify-content: center;
}

/* --------------------------------------------------------*/
/* RADIO BUTTONS */
/* --------------------------------------------------------*/

/* Vertical radio — used by EnumeratorItemRadioVertical for uneven / >2 option sets.
   Each option is one row; the subtitle (when present) sits below the label in a
   lighter typeface so the user reads "Label" first and "what this means" second. */
.radio-control-vertical {
    display: flex;
    flex-direction: column;
    gap: 0.4em;
    /* Outer breathing room top and bottom: two of these controls can stack in one
       composite (e.g. VAT Scheme above Registration timing), and without it the
       3 + 3 options read as one contiguous block. Scoped to this control only. */
    margin: 0.5em 0;
}

.radio-vertical-option {
    display: flex;
    align-items: flex-start;
    gap: 0.5em;
}

.radio-vertical-option.disabled { color: #BBBBBB; }

.radio-vertical-label {
    display: flex;
    align-items: flex-start;
    gap: 0.5em;
    cursor: pointer;
    margin: 0;
}

.radio-vertical-option.disabled .radio-vertical-label { cursor: default; }

.radio-vertical-input {
    margin-top: 0.25em; /* baseline-align with the label-line text */
    flex-shrink: 0;
    accent-color: var(--color-black); /* native radio: monochrome dot, ink not blue */
}

.radio-vertical-text {
    display: flex;
    flex-direction: column;
    line-height: 1.25;
}

.radio-vertical-label-line {
    font-size: 1.2em;
}

/* Selected option: a bold label, so a stacked set is scannable at a glance. The eye finds the
   answer without reading every row and working out which circle is filled, which is the whole
   point of stacking them. Three details are deliberate:
     - WEIGHT only, never colour, so this composes with the disabled greying rather than fighting
       it: a switched-off control keeps its selection legible while still reading as un-editable,
       exactly as the segmented toggle's selected pill does.
     - the SUBTITLE is left alone. It is explanatory prose in a lighter typeface, read second by
       design, and bolding it would restate emphasis the label already carries.
     - :has() keys off the native radio's checked state, mirroring .segmented-toggle-option below,
       so all three radio families carry selection the same way and the reader learns one habit. */
.radio-vertical-option:has(.radio-vertical-input:checked) .radio-vertical-label-line {
    font-weight: 600;
}

/* Endorsement pill on the recommended option, inline after the label text. An outlined light pill
   rather than the solid dark of .suggestions-pill-top: that one is a card-level endorsement with a
   whole card to sit on, and at its own weight inside a form row it dominated the control it was
   meant to annotate. The outline keeps it legible as a distinct object while leaving the option's
   own label the loudest thing on the row. Sized in em so it tracks the 1.2em label line. */
.radio-vertical-badge {
    display: inline-block;
    margin-left: 0.5em;
    padding: 2px 7px;
    border-radius: 10px;
    background-color: #FFFFFF;
    color: #333;
    font-size: 0.7em;
    font-weight: 600;
    text-transform: uppercase;
    vertical-align: middle;
    /* The pill is two words at most and must never break across lines mid-endorsement. */
    white-space: nowrap;
    border: 1px solid #666;
    letter-spacing: 1.3px;
}

.radio-vertical-subtitle {
    font-size: 0.95em;
    color: #777777;
    margin-top: 0.1em;
}

.radio-vertical-option.disabled .radio-vertical-subtitle { color: #BBBBBB; }

/* A live-looking pill inside a greyed control (an unticked optional composite, or one frozen by
   the automated strategy) still endorses something inert, so fade the text and the outline with
   the row rather than leaving a crisp badge on a dead control.
   The FILL goes with them: fading only the text and border left a crisp WHITE chip standing out
   against the greyed row, the brightest thing on a dead control and the opposite of this rule's
   purpose.
   No cursor rule here, deliberately. Two were added and both were DEAD: the pill inherits
   `cursor: pointer` from `.radio-vertical-label` and `default` from its `.disabled` variant, so
   nothing needed overriding. The stated justification was also wrong, that
   `div[data-description]:hover { cursor: help }` would otherwise win, when that rule selects a
   DIV and the pill is a SPAN, so it never applied to it at all. Do not re-add them on that
   reasoning. */
.radio-vertical-option.disabled .radio-vertical-badge {
    color: #BBBBBB;
    border-color: #BBBBBB;
    background-color: transparent;
}
/* Inline (horizontal) radio — used by EnumeratorItemToggle for compact 2 to 3 option
   selectors with short, equal-width labels. Each option is a real radio circle plus its
   label, laid out on one line. The whole option is a <label>, so hovering the circle OR
   the text shows a hand cursor and clicking either selects the option (the horizontal
   counterpart of the .radio-control-vertical block above). An optional per-option hover
   tooltip rides the house data-description popup on the label text (wired further down). */
.inline-toggle {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4em 1.4em; /* row-gap when wrapping; column-gap between options on a line */
    min-height: 2.5em; /* match the other control rows' height */
}

.inline-toggle-option {
    display: inline-flex;
    align-items: center;
    gap: 0.4em; /* radio circle to its label */
    cursor: pointer;
    margin: 0;
}

.inline-toggle-option.disabled { cursor: default; color: #BBBBBB; }

.inline-toggle-input {
    margin: 0;
    flex-shrink: 0;
    cursor: pointer;
    accent-color: var(--color-black); /* native radio: monochrome dot, ink not blue */
}

.inline-toggle-option.disabled .inline-toggle-input { cursor: default; }

.inline-toggle-text {
    font-size: 1.2em; /* match the vertical radio's label line */
}

/* Selected option: a bold label, the horizontal counterpart of the vertical radio's rule above
   and of the segmented toggle's below.
   A bolder label is a shade wider, so the options sitting after the selected one shift when the
   user picks a different one. On a wide viewport that is a pixel or two; this control is
   `flex-wrap: wrap`, though, so where the options already nearly fill the row the extra width can
   instead push the last one onto a second line, and that is a jump rather than a wobble. The two
   longest shipped labels sit on one control (Loan's "I already have this loan" / "I'm planning
   this loan"), so the case is reachable, and a desktop check does not show it.
   Accepted rather than engineered around: the segmented toggle has always carried the same
   trade, and the alternative is reserving the bold width up front (a hidden bold copy behind
   ::after), which needs the label duplicated into a data attribute by the renderer. That is a lot
   of machinery, and it would buy a stable line only by making every option permanently as wide as
   its bold form. Revisit if a control is reported rewrapping in real use.
   The per-option data-description popup is unaffected: its shared ::after resets font-weight to
   normal precisely so a styled trigger cannot restyle its own tooltip. */
.inline-toggle-option:has(.inline-toggle-input:checked) .inline-toggle-text {
    font-weight: 600;
}

.inline-toggle-option.disabled .inline-toggle-text { color: #BBBBBB; }

/* --------------------------------------------------------*/
/* SEGMENTED TOGGLE (summary / breakdown view switch) */
/* --------------------------------------------------------*/

/* The special "view switch" look for the shared summary/detail control: two icon+label
   segments, the selected one filled in ink. Rendered by AddDesignSegmentedToggle; the
   native radios inside are hidden but still drive selection + arrow-key navigation. */
.segmented-toggle {
    display: inline-flex;
    align-items: stretch;
    gap: 3px; /* space between the two pills */
    min-height: 2.5em; /* match the other control rows' height */
    padding: 3px; /* inset the pills so the selected one reads as a pill in a track */
    border: 1px solid var(--color-gray-steel);
    border-radius: 10px; /* concentric with the 7px pill + 3px padding */
    background: var(--color-surface-light);
}

.segmented-toggle-option {
    display: inline-flex;
    align-items: center;
    gap: 0.45em; /* icon to its label */
    padding: 0.25em 0.9em;
    margin: 0;
    border-radius: 7px;
    font-size: 1.2em;
    color: #AAAAAA; /* unselected: muted grey label (selected overrides to ink below) */
    cursor: pointer;
    user-select: none;
    transition: background-color 0.12s ease;
}

.segmented-toggle-option:hover {
    background: var(--color-row-stripe-even);
}

/* Hide the native radio (it drives selection + keyboard nav); the label is the visible button. */
.segmented-toggle-input {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

.segmented-toggle-icon {
    width: 1.2em;
    height: 1.2em;
    flex-shrink: 0;
    opacity: 0.2; /* unselected: very faint (selected goes to full-strength black) */
}

.segmented-toggle-text {
    font-size: 1.15em;
}

/* Selected pill: NO fill (a filled pill reads as too heavy). The selection is carried by a bold
   label + a full-strength black icon; an unselected pill is normal-weight with a muted icon. :has()
   keys off the hidden radio's checked state. Cursor stays default — the selected pill is a no-op. */
.segmented-toggle-option:has(.segmented-toggle-input:checked),
.segmented-toggle-option:has(.segmented-toggle-input:checked):hover {
    background: transparent;
    cursor: default;
}

.segmented-toggle-option:has(.segmented-toggle-input:checked) .segmented-toggle-text {
    color: var(--color-black); /* selected: ink label (unselected is #AAAAAA) */
    font-weight: 600;
}

.segmented-toggle-option:has(.segmented-toggle-input:checked) .segmented-toggle-icon {
    opacity: 1; /* full-strength black (unselected pills are muted at 0.2) */
}

/* Keyboard focus: the native radio is hidden, so reflect its :focus-visible onto the pill as a
   visible ring — otherwise a keyboard user arrowing between Summary / Breakdown sees no focus. */
.segmented-toggle-option:has(.segmented-toggle-input:focus-visible) {
    outline: 2px solid var(--color-black);
    outline-offset: -2px;
}

/* Disabled: the control greys to the house #BBBBBB, exactly like every other switched-off
   control (label.disabled / select.disabled / span.disabled / input.disabled /
   .inline-toggle-option.disabled). Three things the enabled skin does have to be undone
   explicitly, because none of them is inherited:
     - the TRACK (border + fill) is dropped, so a toggled-off composite does not carry a grey
       box around two grey words. The inline toggle has no track at all, so this matches it.
     - the SELECTED pill's ink label is overridden. Its enabled rule is :has()-based and sits at
       4 class-level specificity (.segmented-toggle-option + :has(), which per Selectors 4 takes
       its most specific argument .segmented-toggle-input:checked = 2, + .segmented-toggle-text),
       so the override below carries the .segmented-toggle.disabled ancestor to reach 6 and
       outrank it outright rather than leaning on source order.
     - the SELECTED pill's full-strength icon is muted to sit inside the greyed band.
   No container `opacity` here: the colours are stated exactly, and dimming the whole group on
   top of #BBBBBB would wash the label out to near-invisible. The selected pill keeps its bold
   weight and a stronger icon, so which view is showing stays legible while the control reads
   as un-editable. */
.segmented-toggle.disabled {
    border-color: transparent;
    background: transparent;
}

.segmented-toggle.disabled .segmented-toggle-option { cursor: default; }
.segmented-toggle.disabled .segmented-toggle-option:hover { background: transparent; }

.segmented-toggle.disabled .segmented-toggle-text,
.segmented-toggle.disabled .segmented-toggle-option:has(.segmented-toggle-input:checked) .segmented-toggle-text {
    color: #BBBBBB;
}

.segmented-toggle.disabled .segmented-toggle-option:has(.segmented-toggle-input:checked) .segmented-toggle-icon {
    /* Down from full strength to essentially the 0.2 the unselected pills carry, so the icon
       stops carrying selection at all and the bold label is the sole signal. The 0.05 above
       unselected is a chosen value, not a differential meant to be read. */
    opacity: 0.25;
}

/* --------------------------------------------------------*/
/* CHECK BOXES */
/* --------------------------------------------------------*/

/* Monochrome controls: the on/off activation switch shared by modules, goals, and
   what-ifs (.checkbox-module-toggle, ink track below), the simple checkboxes
   (custom-painted further down), and the radios (radio section) all use ink instead
   of the Bootstrap blue, so colour stays reserved for the signals that should own it
   (links, primary actions, warnings, the active state). */
.checkbox-module-toggle {
    font-size: 1.8em;
    cursor: pointer;
    padding: 0;
}

.checkbox-module-toggle:checked { background-color: var(--color-black); }

/* Custom-painted (appearance:none) so the border colour is controllable per state,
   which native checkboxes don't allow:
     OFF  transparent box, light-gray border; hover turns the border black (the affordance).
     ON   charcoal box (#222222) + white tick, muted at rest; hover lifts it to the full charcoal.
   Sizes are per-variant; border, tick, and hover behaviour are shared. */
.checkbox-simple,
.checkbox-simple-small,
.checkbox-simple-large {
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    box-sizing: border-box;
    vertical-align: middle;
    background-color: transparent;
    background-repeat: no-repeat;
    background-position: center;
    background-size: contain;
    border: 2px solid #CCCCCC;
    border-radius: 4px;
    cursor: pointer;
    transition: border-color 0.12s ease, background-color 0.12s ease, opacity 0.12s ease;
}

.checkbox-simple       { width: 24px; height: 24px; margin-top: 4px; margin-bottom: 6px; }
.checkbox-simple-small { width: 18px; height: 18px; } /* bigger spoils the text boxes */
.checkbox-simple-large { width: 28px; height: 28px; } /* IdentityTransitionModal (isLarge: true) and the landing door's consent step */

/* ON: charcoal fill (#222222, a touch softer than pure black) + matching border +
   white check glyph (Bootstrap's check path), muted at rest via opacity so hover
   lifts it to the full charcoal. */
.checkbox-simple:checked,
.checkbox-simple-small:checked,
.checkbox-simple-large:checked {
    background-color: #222222;
    border-color: #222222;
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e");
    opacity: 0.8;
}

/* Interactive hover (suppressed when not actionable: .disabled / :disabled / the
   parent-not-set .no-hover-effect marker). Any box lifts from its muted rest to full
   opacity; an unchecked box additionally takes an ink border (the affordance). A
   checked box keeps its #222222 border, so hover deepens it to a uniform, full-opacity
   charcoal. */
.checkbox-simple:hover:not(.disabled):not(.no-hover-effect):not(:disabled),
.checkbox-simple-small:hover:not(.disabled):not(.no-hover-effect):not(:disabled),
.checkbox-simple-large:hover:not(.disabled):not(.no-hover-effect):not(:disabled) {
    opacity: 1;
}

.checkbox-simple:hover:not(:checked):not(.disabled):not(.no-hover-effect):not(:disabled),
.checkbox-simple-small:hover:not(:checked):not(.disabled):not(.no-hover-effect):not(:disabled),
.checkbox-simple-large:hover:not(:checked):not(.disabled):not(.no-hover-effect):not(:disabled) {
    border-color: var(--color-black);
}

/* Disabled, or inside a disabled module/item, or with the parent toggle off:
   grayed out and non-interactive. appearance:none discards the browser's native
   disabled shading, so we restore it here. Covers the .disabled class, the disabled
   attribute (:disabled), and the parent-not-set .no-hover-effect marker; placed last
   so a checked-and-disabled box mutes past its :checked opacity. */
.checkbox-simple:disabled, .checkbox-simple.disabled, .checkbox-simple.no-hover-effect,
.checkbox-simple-small:disabled, .checkbox-simple-small.disabled, .checkbox-simple-small.no-hover-effect,
.checkbox-simple-large:disabled, .checkbox-simple-large.disabled, .checkbox-simple-large.no-hover-effect {
    opacity: 0.2;
    cursor: auto;
}

/* --------------------------------------------------------*/
/* CHEVRONS */
/* --------------------------------------------------------*/

.chevron { margin-right: 10px; }

.chevron::before {
    border-style: solid;
    border-width: 0.16em 0.16em 0 0;
    content: '';
    display: inline-block;
    height: 0.75em;
    width: 0.75em;
    left: 0.15em;
    right: 0.55em;
    top: 0.3em;
    position: relative;
    transform: rotate(-45deg);
    vertical-align: top;
}

.chevron.right:before {
    left: 0;
    transform: rotate(45deg);
}

.chevron.bottom:before {
    top: 0.2em;
    transform: rotate(135deg);
}

.chevron.left:before {
    left: 0.25em;
    transform: rotate(-135deg);
}

/* --------------------------------------------------------*/
/* ARROWS */
/* --------------------------------------------------------*/

.arrow {
    height: 0;
    width: 0;
    border-style: solid;
}

.arror.left {
    border-color: transparent black;
    border-width: 0.8em 0.8em 0.8em 0;
}

.arrow.right {
    border-color: transparent black;
    border-width: 0.8em 0 0.8em 0.8em;
}

.arrow.down {
    border-color: black transparent;
    border-width: 0.8em 0.8em 0 0.8em;
}

.arrow.down.red { border-color: darkred transparent; }

.arrow.up {
    border-color: black transparent;
    border-width: 0 0.8em 0.8em 0.8em;
}

.arrow.up.green { border-color: darkgreen transparent; }

.inline-arrow {
    display: inline-block;
    margin-left: 6px;
    vertical-align: middle;
}

/* --------------------------------------------------------*/
/* RESULTS SUMMARY BOX */
/* --------------------------------------------------------*/

.result-summary {
    color: #333333;
    padding: 8px;
    border-style: solid;
    border-width: 1px;
    border-color: #E5E7E9;
}

.result-summary.green { background-color: var(--color-success-row-light); }

.result-summary.red { background-color: var(--color-failure-row-light); }

/* --------------------------------------------------------*/
/* GENERAL CONTROL */
/* --------------------------------------------------------*/

button.btn { font-weight: 500; }

.label-link {
    border-radius: 0.5rem;
    background-color: white;
    border: 1px solid #ccc;
}

.label-link-active:hover {
    cursor: pointer;
    box-shadow: 0 0 0 0.25rem #CED9FE;
    transition: box-shadow 0.2s;
}

.is-invalid.label-link-active:hover {
    cursor: pointer;
    box-shadow: 0 0 0 0.25rem #f3bbc0;
    transition: box-shadow 0.2s;
}

/* --------------------------------------------------------*/
/* MENU */
/* --------------------------------------------------------*/

.nav-link {
    color: #888888;
    font-size: 1.4em;
}

.nav-tabs {
    background-color: white;
    opacity: 0.85;
}

.nav-tabs .nav-link.active {
    color: black;
    background-color: #F9F9F9;
    opacity: 1;
}

.nav-tabs .nav-link.active:hover { 
    cursor: default; 
}

a.active {
    font-weight: bold;
}

/* --------------------------------------------------------*/
/* USER MODE TOGGLE */
/* --------------------------------------------------------*/

.user-mode-toggle-bar {
    display: flex;
    justify-content: center;
    padding: 6px 0 10px 0;
    margin-top: 15px;
    font-size: 1.2em;
}

/* WRAPS rather than overflowing, and the rationale sits above the selector so the declaration block
   reads as one. A segmented control that cannot wrap runs off BOTH edges once its pills outgrow the
   container, and the left-hand overflow cannot be scrolled to at all. The measurement, the in-app twin
   and the detector gap it exposed are in DF.Claude/Features/BrowserWalks.md; only what a reader of THIS
   file needs is repeated here, so the two cannot drift into two accounts of one defect.

   A NO-OP wherever a toggle already fits, which is what makes a change to the shared control safe: with
   the pills on one line there is nothing to wrap and nothing to centre.

   CORNER RADII, and this is the part wrapping FALSIFIES. The first/last pills carry matching corner
   radii so their hover/active backgrounds align to this box's rounded outline -- true of an UNWRAPPED
   row only. `:first-child` / `:last-child` are per-CONTAINER, not per-line, so on a wrapped row the pill
   at the end of a line has square corners, and with no padding and (deliberately) no `overflow: hidden`
   an active pill's black background would paint over the curve. `justify-content: center` is what holds
   it off rather than a fix: measured across 300px to 430px on /uk/showcases, the smallest side gap on a
   wrapped line was 1.8px at 315px, so it comes close. There is no complete fix that keeps the tooltips:
   clipping is what would align it, and clipping is exactly what must not happen here.

   No `overflow` in any spelling: the pills' `button[data-description]::after` tooltip is
   `position: absolute; top: 45px; left: -20px; min-width: 300px`, so it escapes this box on three sides
   and any clipping hides it. `top: 45px` against a ~26px row also means a line-1 tooltip lands over
   line 2 on a wrapped toggle; only the two-pill toggles carry `data-description` today, so it cannot
   happen yet, but wrapping is now possible on every surface that shares this control.

   A single pill WIDER than the container is still not handled: a flex item's `min-width: auto` floors at
   min-content, so wrapping cannot rescue it. Clean at 320px with today's labels; a longer localised one
   reopens it. Disclosed rather than half-fixed, because `min-width: 0` without a wrapping rule for the
   label trades a clipped row for a spilled one. Guarded by TestSegmentedToggleWrapping. */
.user-mode-toggle {
    display: inline-flex;
    flex-wrap: wrap;
    justify-content: center;
    border: 1px solid #ddd;
    border-radius: 20px;
}

.user-mode-pill:first-child {
    border-top-left-radius: 20px;
    border-bottom-left-radius: 20px;
}

.user-mode-pill:last-child {
    border-top-right-radius: 20px;
    border-bottom-right-radius: 20px;
}

.user-mode-pill {
    border: none;
    background: transparent;
    padding: 3px 14px;
    font-size: 1.4em;
    color: #888;
    cursor: pointer;
    transition: background-color 0.15s, color 0.15s;
}

.user-mode-pill:not(.user-mode-pill-active):hover:not(:disabled) {
    background-color: var(--bs-primary);
    color: white;
}

/* A pill whose value would show nothing is DISABLED by both galleries (`Showcases.razor`'s
   `disabled=` and `showcases.js`'s `pill.disabled =`), and until Sep 2026 nothing rendered that state:
   same `#888`, same `cursor: pointer` as a live pill, so the only way to discover a combination was
   empty was to press it and watch nothing happen. Reported from the running app.

   `:not(.user-mode-pill-active)` because a pill can be BOTH selected and empty (pick a type, then narrow
   the other axis under it). Selected is the stronger fact about a control, so the active styling wins and
   the reader still sees what they chose; only an unselected dead end greys out.

   Raw hex rather than a palette var to match its immediate neighbours (`#888`, `#ddd`, `#000` in this same
   block), which is the local convention; the `var(--color-...)` tokens are used by the badge rules further
   down. */
.user-mode-pill:disabled:not(.user-mode-pill-active) {
    color: #ccc;
    cursor: not-allowed;
}

/* Chained classes (0,2,0) beat Bootstrap's `button:not(:disabled)` rule
   at (0,1,1) so the active pill's `cursor: default` actually wins. */
.user-mode-pill.user-mode-pill-active {
    background-color: #000;
    color: white;
    cursor: default;
}

.user-mode-pill.user-mode-pill-active:hover {
    background-color: #000;
    color: white;
}

/* Icon pills of the module-layout toggle (ModuleLayoutToggleComponent). The
   active pill's background is black and the hover background is blue, so the
   black glyph inverts to white on both. */
.layout-toggle-icon {
    height: 13px;
    display: block;
    opacity: 0.55;
}

.user-mode-pill-active .layout-toggle-icon {
    filter: invert(1);
    opacity: 1;
}

.user-mode-pill:not(.user-mode-pill-active):hover .layout-toggle-icon {
    filter: invert(1);
    opacity: 1;
}

/* --------------------------------------------------------*/
/* TIER CARDS: the progressive ladders on /Profile. Shared by the USER MODE selector
   (Lite / Standard / Expert) and the IDENTITY selector (Anonymous / Key / Google), which is
   why the stem is neither mode- nor plan-: it was .mode-plan* until Sep 2026 and both halves
   of that name were wrong. */
/* --------------------------------------------------------*/

.tier-cards {
    display: grid;
    /* Equal-width columns side by side when the panel is wide enough; auto-fit
       drops to 2 then 1 column as it narrows, so the cards stack on small screens.
       Grid's default stretch also gives the columns equal height. */
    grid-template-columns: repeat(auto-fit, minmax(min(170px, 100%), 1fr));
    gap: 20px;

    /* NO PADDING HERE. All four card sets on /Profile share one padding rule, further down beside
       .profile-action-cards, because setting it in two places is exactly how the page ended up with two
       different spacings: this class holds the mode and identity grids, that one holds the two action
       sections, and an edit to either reaches half the page. Search for PROFILE CARD-SET SPACING. */
}

/* Sign-in variant (IdentitySelectorComponent): the active card carries the tier's maintenance
   controls while the other two are short, so stretching all three to the tallest looks lopsided.
   align-items: start lets each card take its own content height. The user-mode cards keep the grid's
   default stretch (equal height) since their content is comparable. */
.tier-cards-signin {
    align-items: start;
}

.tier-card {
    border: 2px solid #e5e5e5;
    border-radius: 12px;
    padding: 20px;
    background-color: #fafafa;
    /* Unselected cards clearly de-emphasised against the active card's
       near-black (#222) body text; hover lifts them to full black. */
    color: #999;
    transition: border-color 0.08s, background-color 0.08s, color 0.08s;
}

.tier-card:not(.tier-card-active) {
    cursor: pointer;
}

.tier-card:not(.tier-card-active):hover {
    border-color: #999;
    color: #000;
}

.tier-card:not(.tier-card-active):hover .tier-card-title {
    color: #000;
}

.tier-card:focus-visible {
    outline: 2px solid #555;
    outline-offset: 1px;
}

.tier-card-active {
    border-color: #000;
    background-color: var(--color-pill-action-light);
    color: #222;
    cursor: default;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}

/* THE ACTIVE ANONYMOUS CARD TAKES THE BADGE'S AMBER (user, Sep 2026), so the chrome and the card that
   confirms it say "this browser only" in one colour rather than two.

   ACTIVE ONLY, and that is the same rule the badge follows rather than a separate preference: a
   non-current card is an OPTION. The Anonymous card seen by a Google user is an offer to step down, and
   tinting it there would warn someone about a risk they do not have, which is the exact defect the badge's
   establishment gate exists to prevent, one surface along.

   The check mark, the word and the tier glyph all turn together for free: they share this element, and
   .tier-card-icon is a mask filled with currentColor. */
.tier-card-active[data-tier="Anonymous"] .tier-card-title {
    color: var(--color-identity-at-risk);
}

.tier-card-title {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 11px;
    font-size: 2.4em;
    font-weight: 700;
    margin-top: 5px;
    margin-bottom: 10px;
    text-align: center;
}

/* The progressive card glyph to the right of the title (one filled slot for
   Lite, two for Standard, three for Expert). Masked rather than drawn as an <img>
   so it takes the title's currentColor: #999 on the unselected cards, #000 on
   hover and on the active card. The per-card SVG comes from the --tier-card-icon
   custom property set inline on the element, by whichever of the two selectors rendered it. */
.tier-card-icon {
    width: 44px;
    height: 31px;
    flex: none;
    background-color: currentColor;
    -webkit-mask-image: var(--tier-card-icon);
    mask-image: var(--tier-card-icon);
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-position: center;
    mask-position: center;
    -webkit-mask-size: contain;
    mask-size: contain;
}

.tier-card-subtitle {
    margin-bottom: 20px;
    text-align: center;
    font-weight: 600;
    font-size: 1.5em;
}

.tier-card-active .tier-card-title {
    color: #000;
}

.tier-card-features {
    list-style: none;
    margin: 0;
    padding-left: 4px;
    font-size: 1.3em;
    line-height: 1.4;
    /* Left-align the bullets explicitly so they read the same in both selectors: the identity
       selector sits inside .profile-below-modes (text-align: center), which would otherwise
       centre these; the mode selector is outside it and already renders them left. */
    text-align: left;
}

.tier-card-features li::before {
    content: "\2714";
    margin-right: 7px;
}

/* Was the mode value inside the Profile TAB. The tab is gone (replaced by .identity-badge below), and this
   rule is kept because the same class is used elsewhere; grep before deleting it. */
.user-mode-badge {
    font-size: 0.6em;
    color: #aaa;
    font-weight: normal;
    margin-left: 2px;
    vertical-align: middle;
}

/* --------------------------------------------------------*/
/* THE CHROME BAR AND ITS IDENTITY BADGE */
/* --------------------------------------------------------*/

/* The tab strip and the identity badge on one row. A flex row rather than an absolutely-positioned badge,
   so a narrow viewport wraps rather than overlapping the badge onto the tabs. The margin-bottom that used
   to sit on the ul lives here now, so the Real/Nominal band's negative-margin trick still meets the menu. */
.chrome-bar {
    display: flex;

    /* CENTRE, not flex-start, and the reason is that the badge is the TALLER of the two. It carries two
       stacked lines and a 3.12em glyph (2.6em of its 1.2em baseline), so it outgrew the tab strip; with flex-start both boxes began at
       the top and the badge simply hung below, putting its silhouette lower than the tab icons even though
       nothing was aligned to the bottom. Centring makes the shorter box (the strip) sit in the middle of
       the taller one, which is what reads as "on the same line". */
    align-items: center;
    margin-bottom: 15px;
}

.chrome-bar-tabs {
    flex-grow: 1;
    margin-bottom: 0;
}

/* The badge: silhouette, then TIER, then mode, on one row separated by a middle dot. A real anchor, so it
   needs the link colours neutralised, and it must not look like the tabs beside it. */
.identity-badge {
    /* THE BASELINE, and it is not cosmetic. The tab strip carries an inline font-size: 1.2em on its own
       <ul>, and this badge is that ul's SIBLING rather than one of its <li>s, so it inherited none of it:
       every em below was resolving against the body instead, which is why the tier line rendered SMALLER
       than the tabs it sits beside. Setting it here restores the strip's scale and makes the children's
       ems mean what they read as. */
    font-size: 1.2em;
    display: flex;
    align-items: center;
    gap: 5px;
    /* NO BORDER, so it reads as part of the menu rather than as a control pinned beside it. The tabs have
       no visible border at rest either (.nav-tabs' own borders are transparent until a tab is active), so
       the badge sitting in a box was the one element on the bar drawing a frame around itself.

       The padding stays: it is the click target, not decoration, and the badge is now the only way to
       /Profile. border-radius went with the border, since it had nothing left to round. */
    padding: 6px 12px;
    margin-left: 10px;
    margin-right: 10px;
    text-decoration: none;
    color: inherit;
    white-space: nowrap;
}

/* HOVER MATCHES THE MENU ITEMS, and it is copied from them rather than invented: a .nav-link goes from
   #888888 to Bootstrap's --bs-link-hover-color (#0a58ca), and the tab's border lightens. Set on the CHILD
   lines rather than on the anchor, because each of them carries its own colour and would override an
   inherited one. */
.identity-badge:hover {
    text-decoration: none;
}

/* THE RISK TINT DOES NOT SURVIVE THE HOVER (user, Sep 2026), so all three tiers go blue together and the
   badge behaves like every other link on the page. This selector list is that decision: it names the tier
   alongside the glyph and the mode, rather than excluding it.

   I argued the other way twice while building this, on "a warning a pointer can switch off is not a
   warning". That was overweighted, and the reason is worth keeping so it is not re-argued: hover is
   transient, mouse-only and unreachable on a touch device, so the AT-REST state is the one that carries the
   message, and it is unchanged. What the exception actually bought was an inconsistency the user could see
   and the warning could not spend.

   NOTE this is the BADGE's hover, not a tier CARD's. The active Anonymous card on /Profile has no hover
   state at all (.tier-card:not(.tier-card-active):hover excludes it, and it carries cursor: default), so
   it stays amber throughout -- correctly, since it is not a link and nothing invites a click. */
/* ONLY WHERE A POINTER CAN HOVER (user, 2026-09-24). On a touch screen a tap leaves :hover stuck on the
   element until the next tap elsewhere, so tapping the Anonymous badge to reach /Profile left its silhouette
   BLUE on the page it opened, reading as though the risk tint had gone. (hover: hover) keeps the rule above
   for a mouse and takes it away from touch, where there was never a real hover to answer. */
@media (hover: hover) {
    .identity-badge:hover .identity-badge-icon,
    .identity-badge:hover .identity-badge-mode,
    .identity-badge:hover .identity-badge-tier {
        color: #0a58ca;
    }
}

.identity-badge:focus-visible {
    outline: 2px solid #1b3a63;
    outline-offset: 2px;
}

/* SPANS BOTH LINES, so it is sized to the pair rather than to one: two lines at 1.3em and line-height 1.3
   stand about 3.4em tall, and the glyph's ink sits inside its em box, so 2.6em fills the height without
   pushing the badge taller than the lines already make it. */
.identity-badge-icon {
    font-size: 2.6em;
    color: #555;
}

/* TWO STACKED LINES, tier over mode, each followed by its own glyph (user, Sep 2026). This reverses the one-row
   "Anonymous . Expert" layout, which was chosen to keep the badge shorter than the tab strip; the bar's
   align-items: center is what absorbs the extra height now, and a line per fact is what lets each carry the
   glyph its card on /Profile already uses. The middle-dot separator went with the single row, since nothing
   sits side by side any more to need dividing. */
.identity-badge-lines {
    display: flex;
    flex-direction: column;
    /* FLEX-START, so each glyph sits straight after its own word rather than in a shared right-hand column
       (user, Sep 2026): a column pushed the Expert glyph a word's width away from "Expert". */
    align-items: flex-start;
    line-height: 1.3;
    margin-left: 10px;
}

/* Both lines are a word and then its glyph in a row, the glyph TRAILING (user, Sep 2026): leading, it put
   three glyphs in a cluster at the left edge with the silhouette. Shared here so the two cannot drift in size or alignment:
   1.3em of the badge's 1.2em baseline, about 1.56em rendered (user, Sep 2026; it was 1.6em, which made
   "Anonymous" alone wider than two of the tabs beside it). */
.identity-badge-tier,
.identity-badge-mode {
    display: flex;
    align-items: center;
    gap: 0.35em;
    font-size: 1.3em;
    color: #000000;
}

/* The tier is the point of the badge, so it keeps the weight; the mode is context and sits at normal
   weight beneath it (user, Sep 2026). The Anonymous amber below still wins over both, because it is
   meaning rather than styling. BLACK rather than a blue, on both: the whole badge is already a link, and a
   second shade of link blue on one line said nothing. */
.identity-badge-tier {
    font-weight: 600;
}

.identity-badge-mode {
    font-weight: normal;
}

/* A line's trailing glyph, masked from the tier or mode SVG so it takes the line's currentColor: black at
   rest, blue on hover, amber on an Anonymous tier line, all for free. The same idiom as .tier-card-icon on
   /Profile, in em rather than px so it tracks the line it follows. 1.4em (user, Sep 2026), a touch larger
   than the cap height, so the line detail reads at the badge's size. */
.identity-badge-glyph {
    width: 1.4em;
    height: 1.4em;
    flex: none;
    background-color: currentColor;
    -webkit-mask-image: var(--identity-badge-glyph);
    mask-image: var(--identity-badge-glyph);
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-position: center;
    mask-position: center;
    -webkit-mask-size: contain;
    mask-size: contain;
}

/* Anonymous ONLY. Two of the three tier values mean the work is safe and one does not, so the treatment is
   on the value rather than on the badge: tinting the whole block, or the mode line, would say something
   false about the other two. */
.identity-badge-tier-at-risk {
    color: var(--color-identity-at-risk);
}

/* THE GLYPH CARRIES IT AT EVERY WIDTH (user, Sep 2026), not only below 576px where the words are hidden.
   It started inside that media query, on the argument that tinting the glyph as well as the word would be
   saying it twice; one signal in one place at both sizes is worth more than that economy, and it makes the
   narrow layout a subset of the wide one rather than a different design. */
.identity-badge-at-risk .identity-badge-icon {
    color: var(--color-identity-at-risk);
}

/* --------------------------------------------------------*/
/* ITEMS */
/* --------------------------------------------------------*/

/* A control label that opens a help page. Same idiom as every other link affordance in the app: black,
   detached dotted underline, solid on hover, and the COLOUR NEVER MOVES. It used to be a solid black
   border-bottom that turned the text and the rule blue on hover, which was the last blue-on-hover link
   left on the design page.

   A border-bottom rather than a text-decoration is also why it needed its own look at all: the border
   spans the whole inline box, so it could not sit under the word the way every sibling's underline does. */
.help-page-link,
.legal-acceptance-prompt a,
.analytics-notice a {
    text-decoration: underline;
    text-decoration-style: dotted;
    text-underline-offset: 0.3em;
    cursor: pointer;
}

.help-page-link:hover,
.legal-acceptance-prompt a:hover,
.analytics-notice a:hover {
    text-decoration-style: solid;
}

/* The acceptance prompt's two links take the house underline above but deliberately KEEP the link blue, unlike
   .help-page-link: they are the two documents the prompt asks the user to open, so they must read as links at a
   glance. */

/* Two short lines (the Terms to accept, the Privacy notice that explains), each its own paragraph and clearly apart. */
.legal-acceptance-prompt p {
    margin-bottom: 1em;
}

/* --------------------------------------------------------*/
/* MODULES */
/* --------------------------------------------------------*/

.curveMiniChart {
    margin-top: 5px;
    margin-bottom: 15px;
}

/* The Settings profile EDITORS (life-stage + asset-allocation) want more air around the preview
   chart than the compact per-module picker previews: the top margin detaches it from the
   "Add ... point" link above (the link itself only carries 6px below it), and the bottom margin
   sets it off from the card's end. Two-class specificity (0,2,0) beats the base .curveMiniChart, and
   the modifier is applied only at the two editor call sites so the picker previews keep the tighter
   rhythm. */
.curveMiniChart.profile-editor-curve {
    margin-top: 18px;
    margin-bottom: 28px;
}

@keyframes module-enter {
    from { opacity: 0; transform: translateY(-8px); }
    to { opacity: 1; transform: translateY(0); }
}

@keyframes module-leave {
    from { opacity: 1; }
    to { opacity: 0; }
}

.module {
    border-radius: 15px;
    border-style: solid;
    border-width: 1px;
    /* Shared with .module-chart-section-clickable:hover, which takes this exact frame so a
       clickable chart card looks like an expanded module. Read through the variable at both
       sites so they cannot drift. */
    border-color: var(--color-module-border);
    margin-top: 10px;
    padding: 1.2em;
    animation: module-enter 0.3s ease-out;
}

.module-leaving {
    animation: module-leave 0.25s ease-out forwards;
    pointer-events: none;
}

.module-view {
    color: #444444;
    background-color: var(--color-surface-light);
    border-width: 1px;
}

.module-view.disabled {
    color: #BBBBBB;
    border-style: dashed;
    background-color: #FCFCFC;
}

.module-view:hover {
    background-color: var(--color-pill-action-light);
    box-shadow: 0 0 0 0.25rem var(--color-pill-action-shadow);
    transition: box-shadow 0.2s;
}

.module-view.disabled:hover {
    box-shadow: 0 0 0 0.25rem #EEEEEE;
    background-color: #FCFCFC;
}

.module-edit {
    background-color: var(--color-pill-action-light);
    box-shadow: 0 0 0 0.25rem var(--color-pill-action-shadow);
}

.module-edit.disabled {
    box-shadow: 0 0 0 0.25rem #EEEEEE;
    background-color: #FCFCFC;
    color: #BBBBBB;
}

.module-description {
    margin-top: 7px;
    margin-bottom: 2px;
    font-size: 1.3em;
    text-align: center;
    word-break: break-all;
    font-weight: bold;
}

.module-explanation {
    padding-top: 10px;
    padding-bottom: 10px;
    /* PROPORTIONAL, capped at the 80px it always was (user, 2026-09-24, from a phone). A fixed 80px each
       side left 45px for text in the 205px a 360px screen gives this column, one word per line. 10% of
       the column is 80px or more from about an 800px column, so a desktop card is unchanged, and on a
       phone the sides come down to 20.5px (measured at a 360px screen). */
    padding-inline: min(80px, 10%);
    font-size: 1.4em;
    text-align: center;
    color: #777777;
}

.module-chart-section {
    margin: 15px;
    text-align: center;
    min-height: 150px;
    border: 1px solid #dddddd;
    background-color: #fefefe;
    border-radius: 1em;
    /* On the base rule, not on :hover, so the frame fades in AND out. Declaring it inside
       :hover (as .module-view:hover does) animates only the enter and snaps the exit. */
    transition: border-color 0.2s, box-shadow 0.2s;
}

/* Three columns, not a flex row with a spacer: the title is centred on the BAND, so it stays put
   whatever the navigation links on the right are doing. Centring it on the space the links leave
   over would drift it as the labels change (and under translation).

   Column 1 is deliberately EMPTY: it is the mirror of the link column, and being a grid track it
   needs no element. The literal alternative, an empty div, is 0px wide and centres nothing; making
   that work needs a visibility:hidden duplicate of the whole link stack, kept in sync by hand.

   All three tracks are fr, so the two sides are EQUAL by construction and the middle is bounded at
   3/5 of the band. The middle being bounded is the load-bearing part: it was `auto` at first, which
   sizes to max-content, so a group description (Property's longest is 224 characters) took the
   whole band and starved both side tracks to zero. Evicting the description to its own full-width
   row "fixed" that and broke the three-section layout instead, separating a description from the
   title it belongs to. Bounding the track keeps both: title and description share the middle
   section and the description simply wraps.

   The 0 minimum on each track is what keeps the sides exactly equal even when the band is narrow;
   the price is that the links can overflow their track rather than widen it, which is why the
   narrow-viewport and comparison-mode rules below drop their labels to icons. Do not give column 3
   a max-content minimum to "fix" that: it buys overflow safety by making the tracks unequal, which
   un-centres the title, the exact thing this grid exists to guarantee.

   align-items is START, not center: the link stack is taller than a one-line title, so centring
   floated the title in the middle of a link-height row and read as an ugly gap above the heading.
   The links opt back into centring themselves, since they span both rows. */
.module-group {
    margin: 10px;
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 3fr) minmax(0, 1fr);
    /* Two EXPLICIT rows, title then description, and they have to be explicit: the link stack
       spans them with `grid-row: 1 / -1`, and -1 counts from the end of the EXPLICIT grid. With
       rows left implicit that resolves to line 1, so the span collapses to a single row, the links
       go back to setting row 1's height, and the description is stranded below the whole stack
       instead of sitting under its title. Row 2 is auto, so it is simply 0 for a group with no
       description. */
    grid-template-rows: auto auto auto;
    align-items: start;
    column-gap: 14px;
    text-align: center;
    font-size: 1.4em;
    padding: 10px 20px;
    border: #AAA solid 1px;
    color: black;
    border-radius: 7px;
    background-color: var(--color-pill-action-shadow);
    text-transform: uppercase;
    letter-spacing: 2px;
    font-weight: bold;
}

.module-group-title {
    grid-column: 2;
    grid-row: 1;
    min-width: 0;
}

/* Stacked on the right of the band: Top, Collapse, Bottom.

   Rows stretch to the widest of the three and each icon sits in a fixed-width cell, so both the
   icons AND the label starts line up. The labels differ in width ("Top" against "Collapse"), so
   one edge has to be ragged; this puts it on the far right, in open space, rather than between
   the title and the icons where there is a border to read it against. */
.module-group-links {
    grid-column: 3;
    /* Spans every row so the stack sits centred beside the whole title-plus-description-plus-commands
       block, rather than defining row 1's height and pushing the description away from its title.
       -1 counts from the end of the EXPLICIT grid, so this depends on .module-group declaring its
       rows; see the note there. */
    grid-row: 1 / -1;
    align-self: center;
    justify-self: end;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    /* The three labels are 11.5px on a 1.35 line-height, so at the 1px this shipped with they read
       as one block of text rather than three separate controls. Enough air to separate them without
       the stack growing tall enough to drive the band's height. */
    gap: 5px;
}

/* The quiet sibling of .module-block-link (the "Add ..." links): same hover idiom, underline
   plus pointer, but grey and glyphless, because these are secondary navigation rather than a
   primary action.
   Everything the band imposes is reset: the links must not inherit its uppercase, bold, 1.4em or
   2px letter-spacing, or they compete with the content they sit above.

   A NEUTRAL grey (--color-text-secondary, #666666), not a blue-tinted one: the strip reads as plain secondary text on the
   band rather than as a coloured control. It also clears the accessibility floor its blue-tinted
   predecessor (#6B7783) sat under: measured against the band's --color-pill-action-shadow (#DBE9F4)
   #666666 is ~4.7:1, over the 4.5:1 text floor, where #6B7783 was 3.7:1. Hover takes it to
   near-black, and the disabled state greys further to #BBBBBB. The command strip below
   (.module-group-command) shares this exact grey so the two sets of secondary controls on one band
   read as one family. */
.module-group-link {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 4px;
    padding: 0;
    border: 0;
    background: none;
    font-family: inherit;
    font-size: 11.5px;
    font-weight: normal;
    line-height: 1.35;
    letter-spacing: 0;
    text-transform: none;
    color: var(--color-text-secondary);
    cursor: pointer;
}

.module-group-link .fa {
    width: 1.15em;
    text-align: center;
    flex: none;
    opacity: 0.7;
}

.module-group-link:hover {
    color: #111111;
}

/* The underline belongs to the WORD, not the whole control. On the button itself it would run
   under the icon too, which reads as a stray rule beside the glyph rather than as a link. The
   offset pushes it clear of the descenders; without it the rule sits tight against the "p" in
   "Top" and "Collapse". */
.module-group-link:hover .module-group-link-text {
    text-decoration: underline;
    text-underline-offset: 3px;
}

.module-group-link:hover .fa {
    opacity: 1;
}

.module-group-link:focus-visible {
    outline: 2px solid #007bff;
    outline-offset: 2px;
    border-radius: 2px;
}

/* Phones: the band is already tight, and three stacked labels beside a centred title crowd it.
   The icons alone carry the meaning (up / collapse / down is legible without words) and the
   tooltip keeps the label available. */
@media (max-width: 700px) {
    .module-group-links {
        align-items: flex-end;
    }

    .module-group-link-text {
        display: none;
    }
}

/* Comparison mode is the same squeeze without the narrow viewport: two half-width columns on a
   wide screen, so the media query above never fires for it. Keyed off the side wrapper rather
   than converted to a container query, because .comparison-side is an explicit signal the layout
   already sets, where a container query would need a new containment context on the band. */
.comparison-side .module-group-links {
    align-items: flex-end;
}

.comparison-side .module-group-link-text {
    display: none;
}

/* The group command strip: directly beneath the description and in the SAME middle track, so it
   reads as belonging to the group titled above it rather than to the module or the page. That
   placement is what makes N strips on one screen (the "All" tab) unambiguous with no explanatory
   copy, the same way N "Add ..." links are unambiguous.

   grid-column / grid-row are explicit: every child of .module-group is placed, and auto-placement
   would drop this into the empty left mirror track. */
.module-group-commands {
    grid-column: 2;
    grid-row: 3;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    /* Wider than the 14px this shipped with: the commands are icon-led now, so a tight gap runs the
       glyph of one into the label of the last and the row reads as one long control rather than
       three. Comfortably clear of the ~8px word-spacing inside a label. */
    gap: 26px;
    margin-top: 10px;
}

/* Styled to read as the navigation stack on the right of the same band (.module-group-link): grey
   and underline-free at rest, darkening and underlining on hover. Both sets are secondary controls
   on a bold uppercase band, so they should look alike, and every property the band imposes has to
   be reset here or the commands compete with the content below.

   Underline-free at rest also keeps these OUT of the dotted-link family sweep
   (TestAddLinkRendering), which is correct rather than convenient: that rule governs links which
   announce themselves with a dotted underline, and these deliberately do not. */
.module-group-command {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 0;
    border: 0;
    background: none;
    font-family: inherit;
    font-size: 11.5px;
    font-weight: normal;
    line-height: 1.35;
    letter-spacing: 0;
    text-transform: none;
    text-decoration: none;
    color: var(--color-text-secondary);
    cursor: pointer;
}

.module-group-command .fa {
    /* Fixed cell and a knocked-back glyph, exactly as .module-group-link does, so the three icons
       read as one family and none of them out-weighs its own label. */
    width: 1.15em;
    text-align: center;
    flex: none;
    opacity: 0.7;
}

.module-group-command:hover {
    color: #111111;
}

/* The underline belongs to the WORD, not the whole control: on the button it would run under the
   glyph too and read as a stray rule beside it. The same split, and the same reason, as
   .module-group-link:hover .module-group-link-text. */
.module-group-command:hover .module-group-command-text {
    text-decoration: underline;
    text-underline-offset: 3px;
}

.module-group-command:hover .fa {
    opacity: 1;
}

/* A no-op command stays present (so the strip never changes width mid-edit) but reads and behaves
   as unavailable. pointer-events: none is the per-composite reset link's own idiom for its greyed
   state and does two jobs here: no hover, and no click. The element carries `disabled` +
   aria-disabled too, because a gate that exists only as a colour is not a gate to the keyboard or
   to a screen reader. */
.module-group-command.disabled {
    color: #BBBBBB;
    cursor: default;
    pointer-events: none;
}

/* In the MIDDLE section with the title, directly beneath it, because that is the section they
   belong to together. Safe here only because the middle track is bounded (see .module-group): with
   the track at `auto` this description's max-content width starved both side tracks to zero. */
.module-group-description {
    grid-column: 2;
    grid-row: 2;
    margin-top: 8px;
    font-size: 0.9em;
    font-weight: normal;
    text-transform: none;
    letter-spacing: normal;
    color: #555;
}

/* Households Couple Mode: one framed box per person (Me, then Partner), rendered only when the
   plan is for two. Side-by-side is inherited rather than declared here: .module-group-items is
   already a wrapping flex row and .module-block is max-width:480px/width:100%, so the two boxes sit
   next to each other when the viewport allows and stack when it does not. Me is declared first, so
   Me is the left one.

   The two owner classes carry no colour yet. The design calls for a sex-driven tint, which needs
   Gender on the SHARED BaseSettingsPersonalData; today only the UK subclass declares it (Italy calls
   its own "Sesso"), so tinting would either reach into one country from shared rendering or force
   that hoist into this slice. The hooks are emitted now so the tint lands as pure CSS later. */
/* Forces the wrapping .module-group-items row to break BEFORE the pair, so Me and Partner share a
   row instead of inheriting the wrap position of whatever card precedes them. Emitted only by Me,
   only in Couple Mode. A zero-height 100%-basis flex item is the standard way to force a wrap
   without adding a visible element. */
.person-row-break {
    flex-basis: 100%;
    height: 0;
    margin: 0;
    padding: 0;
}

.person-section-header {
    font-weight: 600;
    font-size: 0.95em;
    color: #444;
    padding-bottom: 6px;
    margin-bottom: 8px;
    border-bottom: 1px solid #E3E3E3;
}

.person-section > .module-block {
    /* The person box is the frame; its children should not add a second inset margin at the top. */
    margin-top: 0;
}

/* The wrapping flex container that lays out a group's tier-1 composite cards
   (.module-block). The cards carry vertical margins but no horizontal margin,
   so without a gap the cards packed side by side in a row sit edge-to-edge
   ("too attached"). The gap also applies between wrapped rows. */
.module-group-items {
    gap: 5px;
}

.module-group-menu {
    text-align: center;
    margin-top: 15px;
    margin-bottom: 15px;
}

.module-group-menu-item {
    padding: 10px;
    margin: 2px;
    border-radius: 4px;
    font-size: 1.4em;
    background-color: var(--color-pill-action-shadow);
    text-transform: uppercase;
    border: #AAA solid 1px;
}

.module-group-menu-item-on {
    color: black;
    font-weight: bold;
    border-color: #555;
    border-width: 2px;
    display: inline-block;
    cursor: default;
    background-color: #b0c4de;
}

.module-group-menu-item-off {
    font-weight: normal;
    color: #999;
}

.module-group-menu-item-off:hover {
    font-weight: normal;
    color: black;
    border-color: black;
}

/* The chart card navigates to the module's slice of /Results, so on hover it wears the frame
   an EXPANDED module wears: .module's own border (both read --color-module-border, so they
   cannot drift) plus the glow .module-edit carries. Borrowing an existing treatment rather
   than inventing one keeps the page to a single vocabulary for "this is live". The old
   #F1F1F1 glow was near-invisible against the module's alice-blue background, which is the
   part that needed fixing; the black chart titles and their underline are deliberately
   unchanged, they already read as the link.

   :focus-visible rides along because the card is keyboard-reachable (role=button +
   tabindex=0), so the frame doubles as its focus ring.

   Only the -clickable modifier gets any of this. The same card also renders as the
   pre-first-result loading placeholder, which is NOT clickable, and used to light up and
   offer a hand cursor for a click that did nothing. */
.module-chart-section-clickable:hover,
.module-chart-section-clickable:focus-visible {
    cursor: pointer;
    border-color: var(--color-module-border);
    box-shadow: 0 0 0 0.25rem var(--color-pill-action-shadow);
    outline: none; /* the frame above IS the focus indicator; a UA outline on top double-rings it */
}

.module-chart-section-clickable:hover .module-chart-title,
.module-chart-section-clickable:focus-visible .module-chart-title {
    color: black;
    text-decoration-color: black;
}

/* The chart card's title carries the same hover-underline affordance as a composite card's title
   (.module-block-header-text), and it had the same defect: this is display: inline-block, so a
   border-bottom draws once at the bottom of the box however many lines the text wraps to, and
   "Balance sheet changes" at 1.4em bold wraps in a half-width comparison-mode card. text-decoration
   is drawn per line box, so it follows every line. See .module-block-header-text for the leading
   arithmetic; both are pinned by TestCompositeHeaderUnderline. */
.module-chart-title {
    text-align: center;
    margin-bottom: 10px;
    font-size: 1.4em;
    font-weight: bold;
    display: inline-block;
    text-decoration-line: underline;
    text-decoration-style: solid;
    text-decoration-color: transparent;
    text-decoration-thickness: 2px;
    text-underline-offset: 0.3em;
    line-height: 1.4;
}

.module-chart-parent { margin: auto; }

.module-chart-wrapper { padding: 20px 20px 0 20px; }

.module-chart { width: 100%; }

.module-description-summary {
    font-size: 1.5em;
    text-align: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.module-summary {
    color: #555555;
    margin: 5px;
    font-weight: 400;
    font-size: 1.3em;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    row-gap: 6px;
}

/* Secondary pill row sitting below the main four-pill identity bar (ΔTNW =
   External + Transfers + BS changes). Carries derived metrics — currently the
   annualised return (IRR) pill, future TWR / income-yield / similar rate pills.
   The top margin separates this row visually from the identity bar above so
   the two read as related-but-distinct strips. */
.module-summary-additional-pills {
    margin-top: 25px;
}

/* Reusable invisible wrapper for any row of pills (.module-summary boxes,
   tax-bridge pills, or future pill ports from /Design). Provides consistent
   top / bottom breathing room above + below a pill row regardless of which
   page hosts it. Stack multiple .module-summary children inside one
   .pills-row when several pill bars belong to the same logical section.
   Otherwise wrap each pill bar in its own .pills-row for separate spacing. */
.pills-row {
    margin-top: 40px;
    margin-bottom: 8px;
}

/* Description rendered as a colspan'd row at the top of <thead> — the
   <td> itself is transparent and contributes width=0 to the table's
   preferred width (via the inner div's `width: 0; min-width: 100%`
   trick), so a long description never inflates the table's grid width.
   The visual styling lives on the inner div, which fills the cell
   (= sum of column widths) at render time and wraps text inside.

   Selectors are scoped to .details-table to win specificity over
   .details-table td (padding 0 8px) and any tr / odd / hover rules. */
/* Selectors include .fluid-table (the table also carries that class via
   TableBuilder.OpenTable) so specificity (0,3,1) beats the
   .fluid-table td { font-size: 1.2em } and
   .fluid-table td:first-child { font-weight: bold; font-size: 1.3em }
   rules at line ~3660 (specificity 0,2,1). Source order alone wasn't
   enough — those rules come AFTER ours in the file. */
.details-table.fluid-table td.table-description {
    padding: 0;
    border: none;
    background-color: transparent;
    font-weight: normal;
    font-size: 1em;
}

.details-table.fluid-table td.table-description > div.table-description-inner {
    /* The width: 0 + min-width: 100% pair makes the cell's preferred
       width 0 (so the table grid stays at its data-row width) while
       the rendered div fills 100% of the cell at layout time and
       wraps text inside the data-row width. */
    width: 0;
    min-width: 100%;
    box-sizing: border-box;

    background-color: #f6f6f6;
    border-top: 2px black solid;
    color: var(--color-text-secondary);
    font-size: 1.3em;
    font-weight: normal;
    text-align: center;
    padding: 10px;
}

/* Title block above the optional caption. Black bold heading; the
   caption (the plain text node sibling) inherits the inner div's
   #666 + normal-weight styling. */
.details-table.fluid-table td.table-description > div.table-description-inner > div.table-description-title {
    color: black;
    font-weight: bold;
}

/* Card-stack mode (<768px) hides the entire <thead> via
   `.fluid-table thead { display: none }` so column-header cells don't
   appear above the data-label-pair stack. The description row still
   needs to be visible — override thead display, hide every other thead
   row (column headers), then bring the description row back as a block
   that fills its parent without inheriting .fluid-table tr's card-style
   border / padding / margin. */
@media (max-width: 768px) {
    /* Selectors duplicate .fluid-table from .details-table to win
       specificity (0,2,1) against `.fluid-table thead { display: none }`
       (0,1,1) which lives later in the file. Same trick for the deeper
       selectors. */
    .details-table.fluid-table thead {
        display: block;
    }
    .details-table.fluid-table thead tr {
        display: none;
    }
    .details-table.fluid-table thead tr.table-description-row {
        display: block;
        margin: 0;
        padding: 0;
        border: none;
    }
    .details-table.fluid-table thead tr.table-description-row td.table-description {
        display: block;
        width: 100%;
        padding: 0;
    }
}

.module-summary-icon {
    height: 28px;
    opacity: 0.7;
    grid-row: 1 / span 2;
    grid-column: 1;
    align-self: center;
    justify-self: center;
}

.module-summary-box {
    margin: 0 5px;
    padding: 4px 10px;
    border-radius: 10px;
    background-color: #f2f2f2;
    border: 1px solid #CCC;
    display: grid;
    grid-template-columns: auto 1fr;
    grid-template-rows: auto auto;
    column-gap: 8px;
    row-gap: 1px;
    align-items: center;
}

/* The label sits directly above the number in the same grid column, and that column is
   sized by whichever of the two is wider. A long amount ("-GBP 1,234,567") widens it well
   past the label, so a start-aligned label drifted left of the number it describes. The
   number is already centred, so centring the label keeps the pair as one centred stack at
   every width; when the label is the wider of the two, both were already flush anyway. */
.module-summary-description {
    font-size: 0.85em;
    grid-row: 1;
    grid-column: 2;
    align-self: end;
    text-align: center;
}

.module-summary-number {
    color: black;
    font-weight: bold;
    font-size: 1.1em;
    white-space: nowrap;
    text-align: center;
    grid-row: 2;
    grid-column: 2;
    align-self: start;
}

/* Pill text colours pull from ChartPalette via CSS custom properties set on :root
   by setPalette at app startup (index.html). Single source of truth in
   DF.UI/Charting/ChartPalette.cs. */
.module-summary-number.blue { color: var(--color-blue); }
.module-summary-number.darkblue { color: var(--color-dark-blue); }
.module-summary-number.gray { color: var(--color-gray-dark); }

.module-summary-box-disabled {
    opacity: 0.45;
}

.module-summary-number-disabled {
    color: #888;
}

/* Re-pricing in flight: dots replace the number within each pill so the box
   layout stays stable. Reuses the summary-dot-pulse animation. */
.module-summary-number-pricing {
    /* `flex`, not `inline-flex`: this is always a direct grid child of .module-summary-box, so it is
       blockified and the two compute identically. Spelling it `flex` stops it reading as shrink-to-fit.
       It stretches across the value column like the label above it, and justify-content is what centres
       the dots inside it -- text-align cannot, because flex items are not text. Without it the dots sit
       flush left under a centred label, so the pill's contents jump sideways on every re-price. */
    display: flex;
    justify-content: center;
    gap: 3px;
    align-items: center;
    min-height: 1.1em;
}

.module-summary-pricing-dot {
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background-color: #888;
    animation: summary-dot-pulse 1s ease-in-out infinite;
}

.module-summary-pricing-dot:nth-child(2) { animation-delay: 0.2s; }

.module-summary-pricing-dot:nth-child(3) { animation-delay: 0.4s; }

@keyframes summary-fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

.module-summary-box { animation: summary-fade-in 0.3s ease-out; }

.module-summary-operator {
    align-self: center;
    color: #AAA;
    font-size: 1.3em;
    padding: 0 2px;
    user-select: none;
}

/* ---- Results year tiles -------------------------------------------------
   The Summary's year navigator: a wrapped grid of one clickable tile per year,
   in place of the year-by-year table that used to close the page. Sixty tiles
   wrap into about six rows, where sixty table rows did not, which is what buys
   the navigator a place above the analysis rather than under it.
   See DF.Claude/Features/ResultsYearTiles.md.

   auto-fill + minmax rather than a fixed column count: the grid is rendered at
   full page width, inside a comparison half, and on a phone, and each wants a
   different count off the same rule. */
.year-tiles {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(96px, 100%), 1fr));
    gap: 6px;
    /* Constrained to the PLOT, the same 70% .div-graph-description uses and for the same reason:
       every chart on this page draws its chartArea at 70% of the canvas (charting.js), roughly centred
       by Google's own axis margins. The navigator is stacked among those charts, so a full-width grid
       reads as a wider object than everything around it.

       The 520px floor is the difference from the description, and it is load-bearing rather than
       defensive. A description is prose: narrowing it costs nothing. A grid is a WRAP, so narrowing it
       costs columns and buys rows -- measured at 390px, a bare 70% dropped the grid from 3 columns to
       2 and took it from 19 rows to 28. The floor lets the cap do its job on the wide page it was
       asked about, and stand down where the page is already narrow and there is no wider neighbour to
       look wider than. max() rather than a breakpoint so the two regimes meet continuously instead of
       jumping at a threshold.

       The auto side margins do the centring; keep them if this is ever retuned, or the grid pins to
       the left edge instead. */
    max-width: max(70%, 520px);
    margin: 8px auto 16px;
}

/* The grid's one-line header, naming the quantity every tile face shows. It is a caption, not a
   label on a control, so it takes ordinary prose casing from the pill term it reuses. Shares the
   grid's measure, floor included, so the two stay one block at every width. */
/* .year-tiles-caption carries NO style of its own any more: the grid's title sits below the grid in
   .div-graph-title, like every other titled block on this page, and the class survives only as the hook
   TestYearTilesComponent finds it by. */

.year-tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    padding: 6px;
    /* 1.5px, not 1: at 1px the frame competes with the pale end of the tint and a lightly-tinted
       tile reads as barely-drawn. The half-pixel is deliberate and survives: the border is also the
       whole hover affordance, so it only ever changes COLOUR and the grid never reflows. */
    border: 1.5px solid var(--color-module-border);
    border-radius: 8px;
    /* No shadow, in either state. The tile is one of sixty in a dense grid, so a halo on all of them
       reads as sixty floating cards competing with the tint, and a halo on just the hovered one was
       tried and dropped as clutter. The pointer cue is carried by the border colour alone. */
    /* The heat tint. The component sets --year-tile-tint inline per tile (an rgba derived from
       ChartPalette.MoneyIn / MoneyOut at ChartPalette.YearTile* alpha). An untinted tile falls through
       to the light neutral grey (ChartPalette.PillNeutralLight, the default chip's fill): a Results year
       printing £0, and EVERY Tax Summary tile, whose button never carries a tint because its bands do.
       "Nothing happened" stays distinct from "a little happened" because the smallest tint is a HUE
       (blue or red at YearTileMinTint), which the neutral grey is not.

       The DATA arrives as a variable and the PAINTING happens here, so every background-color
       declaration for this element lives in one file. (That split was originally forced by a hover
       fill an inline background-color would have outranked; the hover no longer fills, so the split
       is now kept for the ordinary reason rather than a mechanical one.) */
    background-color: var(--year-tile-tint, var(--color-pill-neutral-light));
    cursor: pointer;
    font: inherit;
    /* A FIXED face colour, never the amount's sign colour. A negative used to inherit
       the table's red text, which on a dark-red tile at YearTileMaxTint is dark red on
       dark red, on the most important tile in the grid. Black clears WCAG AA on both
       hues at that alpha (8.7:1 and 5.9:1); the sign is carried by the explicit +/-
       and by the hue. See ChartPalette.YearTileMaxTint for the measurements. */
    color: var(--color-black);
    text-align: center;
    /* A grid item defaults to min-width: auto, which refuses to shrink below its content and would
       push the whole column wider than its track. Pair with .year-tile-amount's overflow-wrap. */
    min-width: 0;
    /* Only the border colour transitions, since it is the only thing hover changes. The background
       deliberately does NOT: it carries DATA, so easing it would animate all sixty tiles at once on a
       Real / Nominal flip, turning a data change into a slow repaint of the whole grid. */
    transition: border-color 0.15s ease;
}

/* Hover, and this tile deliberately does NOT take the shared clickable-card treatment
   (--hover-card-ring + a --color-pill-action-light fill) the catalogue pickers and sweep cards wear.

   The reason is the one the wizard tile already established: a fill paints over colour that MEANS
   something. There the meaning is completed / started state; here it is the heat tint, i.e. how big
   that year was, which is the grid's entire content. Filling the hovered tile blanks the one datum
   the user is pointing at, and worse, it makes a hovered tile momentarily indistinguishable from a
   pale one. The wizard tile is exempted for exactly this and is the precedent, not an exception.

   The shared ring ALONE was tried and is not acceptable either: pale blue on a light card measures
   about 1.24:1, which is a hover the user cannot see, and TestHoverCardRing exists to catch precisely
   that. So the affordance is carried by the border instead, which has contrast to spare: the resting
   frame is --color-module-border (#BBBBBB) and hover takes it to black, a change no fill is needed to
   sell, and no shadow either: a halo was tried at two strengths and dropped, see the rule below.

   Pinned by TestHoverCardRing.TheYearTile_HoversWithoutAFill_ButStillCarriesAVisibleAffordance. */
.year-tile:hover {
    /* The border IS the whole affordance: grey --color-module-border to black, at the same 1.5px, so
       nothing moves and nothing reflows. No shadow in either state (a halo was tried and dropped: on
       a dense grid of sixty small tiles it read as clutter, and once it was strong enough to see it
       needed a z-index to stop siblings clipping it, which is a lot of machinery for a pointer cue a
       border colour already carries). No fill either, for the reason above. */
    border-color: var(--color-black);
}

/* Keyboard access, which the <tr onclick> rows this replaces did not have at all. */
.year-tile:focus-visible {
    outline: 2px solid var(--color-blue-accent);
    outline-offset: 1px;
}

.year-tile-year {
    font-size: 1.15em;
    font-weight: bold;
}

/* One band of a MULTI-line tile (the Tax Summary's Reportable and Embedded lines). Each band carries its
   own --year-tile-tint, on its own scale, and the button carries none; a one-line tile keeps the tint on
   the button, so the Results grid's DOM is unchanged. The band stretches to the tile's width so its paint
   reads as a stripe of the tile rather than a highlight behind the text. */
.year-tile-line {
    align-self: stretch;
    background-color: var(--year-tile-tint, transparent);
    border-radius: 4px;
    padding: 3px 4px;
}

/* The legend under a multi-line grid: one swatch per band colour, after the caption, so a face that
   carries amounts only never leaves a line unlabelled. The swatch paints the band's colour at its cap. */
.year-tiles-legend-entry {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    margin-left: 12px;
}

.year-tiles-legend-swatch {
    display: inline-block;
    width: 12px;
    height: 12px;
    border: 1.5px solid var(--color-module-border);
    border-radius: 3px;
    background-color: var(--year-tile-tint, transparent);
}

.year-tile-amount {
    font-size: 1.05em;
    /* NOT `white-space: nowrap`, and not `overflow: hidden` either. A currency amount has no space
       or hyphen in it, so normal wrapping already keeps it on one line; `anywhere` only engages when
       the amount genuinely cannot fit, and then it wraps INSIDE the tile instead of spilling past the
       border. `overflow: hidden` would be worse than the spill it fixes: a clipped `+£12,345,678`
       reads as a real, smaller, wrong number, which the Second Commandment forbids outright.

       Measured at the 96px column floor with this font: 12 glyphs (`+£12,345,678`) occupy ~82px
       inside 96px, and the wrap engages around 15 (a billion-pound year in nominal terms). So this
       is the guard for the case the browser pass could not construct, not the everyday path. */
    overflow-wrap: anywhere;
}

@keyframes summary-dot-pulse {
    0%, 100% { opacity: 0.25; }
    50% { opacity: 0.9; }
}

.module-summary-loading {
    display: flex;
    justify-content: center;
    align-items: center;
    margin: 5px;
    gap: 4px;
    height: 41px;
}

.module-summary-loading-dot {
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background-color: #AAA;
    animation: summary-dot-pulse 1s ease-in-out infinite;
}

.module-summary-loading-dot:nth-child(2) { animation-delay: 0.2s; }

.module-summary-loading-dot:nth-child(3) { animation-delay: 0.4s; }

.module-summary-number.red { color: var(--color-red); }

.module-body:hover {
    cursor: pointer;
    color: black;
}

.module.disabled .module-body:hover {
    color: #BBBBBB;
}

.module.disabled .module-explanation {
    color: #BBBBBB;
}

.module-name {
    padding-bottom: 1px;
}

/* Hover "clickable" cue on the title. text-decoration (not border-bottom) so the
   underline hugs the actual glyphs on every line: a long title that wraps to two
   lines gets each line underlined under its text, instead of a single full-width
   border under the last (often short) line. Also avoids the 2px hover layout shift
   a border-bottom caused. */
.module-body:hover .module-name {
    cursor: pointer;
    text-decoration: underline;
    text-decoration-thickness: 2px;
    text-underline-offset: 0.3em;
}

/* The SAME cue on the clickable-card family (the --hover-card-ring consumers at the top of this file).
   Those tiles carried the fill and ring but no text cue, while the module header above and the showcase
   gallery card both underlined, so pointing at a goal tile and pointing at a module header said different
   things about the same kind of gesture.

   The name only, never the whole tile: text-decoration propagates down the flex line, so putting this on
   `.add-accent-title` would underline the grey GOAL / WHAT-IF kind label stacked above the name too. The
   same mistake is already recorded for `.module-block-header-title` and the `.module-block-link` pair.

   Scoped to `.module-picker-group` for the module tile, and behind the same `:not(...-disabled)` as each
   family's fill, so the underline and the fill can never travel apart: a disabled tile is inert and must
   not claim otherwise.

   0.3em is the site's offset for this cue (15 of the 24 sites in this file, counted on this branch), and the reason the rules
   below loosen a line-height comes with it: see the clearance formula on `.module-block-header-text`.

   THE COMPACT PILL DEPARTS FROM THE OTHER THREE TWICE, and both are deliberate.

   It hangs off the GLYPH and the TITLE rather than the whole card, because those are the two elements
   carrying the expand handler (ModuleComponent.RenderCompactPill, both with StopPropagation) and both
   already say so with `cursor: pointer`. The third child is the rename / delete / clone / on-off stack,
   which does something else entirely, so a whole-pill selector would promise expansion from a button that
   does not expand. The pill's FILL is whole-pill and stays that way: a fill says "you are on this card",
   an underline says "this opens", and only the second is a lie over the action stack.

   And it takes NO `:not(.disabled)`, unlike the picker tiles above. Their `-disabled` means the tile
   cannot be chosen, so its hover must be inert. `.module-compact-pill.disabled` means the MODULE is not
   set, which is a different word wearing the same spelling: the pill is still expandable, and expanding it
   is how you switch the module on. Excluding it would remove the cue from the one pill a user most needs
   to open. */
.module-picker-group .add-module-button:not(.add-module-button-disabled):hover .add-module-description,
.add-accent-button:not(.add-accent-button-disabled):hover .add-accent-name,
.sweep-suggestion-card:hover .sweep-suggestion-title,
.module-compact-pill-glyph:hover ~ .module-compact-pill-title .module-compact-pill-name,
.module-compact-pill-title:hover .module-compact-pill-name {
    text-decoration: underline;
    text-decoration-thickness: 2px;
    text-underline-offset: 0.3em;
}

/* `.module-body:hover .module-name` (two rules up, no longer the one immediately above: the shared
   family rule now sits between them) also matches a .disabled title, since it still carries
   .module-name, and supplies the underline; this only swaps the colour to the muted grey. */
.module-body:hover .module-name.disabled {
    text-decoration-color: #DDDDDD;
}

.module-title {
    padding-bottom: 5px;
    margin-top: 3px;
    margin-bottom: 10px;
    display: flex;
    justify-content: center;
}

.module-title .module-name {
    font-weight: bold;
    font-size: 2em;
}

/* Collapse/expand arrow beside the module name (was an inline style in
   ModuleComponent.BuildHeader). */
.module-collapse-chevron {
    font-size: 1.8em;
    padding: 3px;
    margin-left: 18px;
}

/* The module's identity stack: category name, icon, ownership pill. It deliberately
   declares NO justify-content of its own, so its main-axis packing is whatever
   .module-side-column says -- the class its two siblings (the action icons and the move
   arrows) also carry. That is the whole point: all three columns then start at the same
   line whatever the middle column holds, and an override here would silently decouple
   this one from the two it is meant to line up with.

   It carried space-evenly for a while, to spend the card's height on the stack's gaps
   rather than leave a void beneath it. That is the wrong lever: it makes the glyph's
   position a function of how tall the MIDDLE column happens to be, which nothing in this
   column controls. An expanded module in Expert mode carries three extra rows of return
   pills (.module-summary-additional-pills), which trebles the header height and sank the
   label and the glyph a third and two thirds of the way down, while the same module
   collapsed showed them at the top. Pinned by TestCardSideColumnAlignment, because
   this has now drifted twice and a stylesheet comment did not hold it either time.

   The class survives for the .module-image margin reset below, which IS particular to
   this column. */

/* The card glyph's size and spacing, keyed on the COLUMN rather than on the card state.
   That is the same set of elements either way -- the card's glyph is the only .module-image
   inside an identity column, and the compact pill's sits outside one -- so keying it here
   says it once instead of in a .module-view / .module-edit pair that would shadow, at equal
   specificity, whatever this rule said. It used to zero the margin for the expanded card
   only; both states now share one size, so the identity stack reads the same open or shut.

   Height, not max-width: icons differ in aspect ratio, so sizing by height is what makes a
   column of cards line up on the glyph. Both resets below are load-bearing -- the base
   .module-image sets width:100% + max-width:55px, so without them an icon wider than
   55:45 (= 1.222) stays WIDTH-limited and never reaches 45px (measured: Education ~41.5px,
   Isa ~43.5px). */
.module-identity-column .module-image {
    height: 45px;
    width: auto;
    max-width: none;
    margin-top: 5px;
    margin-bottom: 10px;
}

/* Households: the ownership pill, in the module's identity column directly under its
   type name. Block-level and centred so it lands at the same x on every row: the point
   is that a couple can read a column of You / You & Partner / Partner straight down
   the page, which a pill trailing a variable-length module name cannot do. Sized as a
   real control rather than a caption, because it is clickable -- when it is: a
   read-only pill states the same fact without offering the click. */
.module-ownership-pill {
    display: inline-block;
    padding: 4px 14px;
    /* The EXPANDED size, and the collapsed card steps DOWN from it in the density block below.
       That direction is the whole point: this rule once held the smaller value while the collapsed
       card stepped UP, so opening a module made the pill shrink -- an identity fact getting less
       prominent exactly as the user gave it more attention. The COMPACT pill is a separate class
       with its own size, deliberately. */
    font-size: 1.3em;
    font-weight: 600;
    line-height: 1.3;
    border-radius: 12px;
    border: 1px solid var(--df-border, #bbb);
    background-color: var(--df-surface-muted, #f2f2f2);
    cursor: pointer;
    white-space: nowrap;
}

/* The person glyph(s) leading the pill. Sized in `em` so it tracks whatever font-size the pill is
   given -- the module card and the compact pill set different ones -- rather than needing a px
   override per surface. The right margin separates a glyph from whatever follows it, which for
   shared ownership is the second glyph and then the label; `gap` is unavailable because the pill is
   inline-block with text rather than a flex row, and making it flex would re-wrap the label. */
.module-ownership-pill-glyph {
    height: 1em;
    width: auto;
    vertical-align: -0.12em;
    margin-right: 4px;
    opacity: 0.75;
}

/* The goal card's SUBJECT pill, which reuses .module-ownership-pill wholesale and changes only what
   its column forces. It sits under the (i) inside the tooltip column rather than in a track of its
   own, so it has that column's width to live in rather than the identity column's, and the module
   pill's 1.3em would push "You Partner" past the card's left border on the household subject.

   Reusing the base class rather than cloning it is deliberate: the border, the radius, the muted
   ground and the glyph rule are the same identity control on both cards, and a couple reading a
   column of goals should recognise the same object they clicked on a module. Only the SIZE is this
   surface's business; the column already spaces its own children. */
.goal-subject-pill {
    font-size: 1.0em;
    padding: 2px 8px;
}

/* The pill and its ratio adjuster on ONE line. Emitted only when the ratio shows (see
   ModuleComponent), because the identity column is itself a flex column and wrapping a lone pill
   would make it a flex item of a nested row for nothing.

   `align-items: stretch` is what makes the select exactly as tall as the pill, and it is the reason
   this is a flex row rather than two inline-blocks: matching the height by hand would mean copying
   the pill's font-size, line-height and vertical padding into the select and keeping the two in
   step through every `em` cascade above them (the pill is 1.3em here and steps DOWN on the collapsed
   card). Stretch is the same result with nothing to keep in sync. */
.module-ownership-row {
    display: flex;
    align-items: center;
    gap: 4px;
}

/* The share a shared pill states after each name. A step lighter than the pill's own 600 so the NAME
   stays the identity fact and the percentage reads as qualifying it, which also buys back a few
   pixels in a column the 25 / 75 case overflows. */
.module-ownership-pill-share {
    font-weight: 400;
}

/* A pill carrying BOTH names and BOTH shares ("You 25% Partner 75%") is the widest this column ever
   has to hold, and at the standard 14px it sat on the card's left border.

   Keyed on the CAUSE -- the presence of a share suffix -- not on the ratio row. Those are not the same
   set: a GATED category (or the compact pill, which has no row) carrying an unequal byte from a blob
   or a showcase is exactly as wide and gets exactly the overflow this rule exists to fix, while
   showing no ratio control at all. Keying on the row fixed the case that motivated it and left the
   case nobody was looking at. */
.module-ownership-pill:has(.module-ownership-pill-share) {
    padding-left: 9px;
    padding-right: 9px;
}

/* The visible half of the ratio control: a bare chevron, no box. It replaced a bordered select that
   drew its own `25 / 75` text, which cost ~75px in a column that had none to give -- with the pill
   reading "You 25% Partner 75%" the pair overflowed the card on the left. The glyph is ~18px.

   Resting grey rather than black so it reads as the pill's adjuster and not a second identity fact;
   hover takes it to full black, which is the only affordance saying it is clickable (there is no box
   left to highlight). */
.module-ownership-ratio-trigger {
    position: relative;
    display: inline-flex;
    align-items: center;
    flex: none;
    /* Padding rather than a bigger glyph, so the TOUCH TARGET grows without the chevron shouting or
       the column widening: the select below is `inset: 0`, so it fills the padded box and the whole
       area is tappable. Still under the 44px ideal -- the identity column cannot spare it -- but the
       "works on a mobile picker" claim needs the target to be more than the glyph's own ~13px. */
    padding: 6px 5px;
    /* The glyph is the whole control, so it has to be big enough to read as one rather than as
       punctuation after the pill. It does NOT set the popup's text size any more: the select below
       declares its own, so the two can be tuned independently. */
    font-size: 1.15em;
    line-height: 1;
    color: #999;
    cursor: pointer;
}

/* Hover AND focus-within. The select underneath is `opacity: 0`, which hides the browser's own focus
   ring along with everything else, so without the second selector a keyboard user tabbing onto the
   control got no indication at all that they were on it. The glyph darkening IS the focus ring here. */
.module-ownership-ratio-trigger:hover,
.module-ownership-ratio-trigger:focus-within {
    color: #000;
}

/* The real <select>, laid over the chevron and invisible. Kept as a native select on purpose: the OS
   dropdown, keyboard control, the mobile picker wheel and the accessible role all come free, and a
   hand-rolled listbox would have to reimplement every one of them. `opacity: 0` rather than
   `visibility: hidden` or `display: none`, because it still has to receive the click. */
.module-ownership-ratio {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    border: none;
    opacity: 0;
    cursor: pointer;
    /* The one declaration here that is NOT about hiding the control: the open list is drawn by the
       browser from the SELECT's font, not the trigger's, so this is what sizes the options the user
       reads while choosing. In `rem` deliberately -- an `em` would inherit the trigger's glyph size
       and re-couple the two things this rule exists to separate. */
    font-size: 1rem;
}

/* Only the size, never the side padding. `option { padding }` was tried here and Chrome on Windows
   ignored it, which is long-standing behaviour for a native popup rather than a bug worth chasing;
   the side padding now rides the option TEXT as non-breaking spaces instead (see
   ModuleComponent.OptionPad). Setting it here as well would do nothing in Chrome and double up in
   Firefox, which does honour it. */
.module-ownership-ratio option {
    font-size: 1rem;
}

/* A shared pill reads `{glyph} You {glyph} Partner`: each glyph against its own name, and no conjunction
   between them, because the second glyph is itself the separator. That only works if the gap BETWEEN the
   two owners is clearly wider than the gap inside one, or the four marks read as an undifferentiated
   run. Inside a pair is the glyph's own 4px `margin-right` above; between pairs is this, plus the plain
   space the renderer emits.

   `+` here is doing the same thing it did when this rule was a NEGATIVE margin, and that is the point
   worth remembering rather than the value: it is the next-ELEMENT-sibling combinator, so the name text
   sitting between the two glyphs does not break it. That behaviour was a trap for the old pair-tightener
   (it kept matching after the layout changed, and would have nudged the wrong gap); here it is precisely
   the condition wanted -- "a glyph that follows another owner's glyph, whatever text lies between".

   Tunable on its own: raising it separates the two owners further without touching how close a glyph
   sits to its own word. */
.module-ownership-pill-glyph + .module-ownership-pill-glyph {
    margin-left: 6px;
}

.module-ownership-pill:hover .module-ownership-pill-glyph,
.module-compact-ownership-pill:hover .module-ownership-pill-glyph {
    opacity: 1;
}

.module-ownership-pill:hover {
    border-color: var(--df-accent, #666);
    background-color: var(--df-surface, #e8e8e8);
}

.module-ownership-pill.disabled {
    opacity: 0.5;
    cursor: default;
    /* The click is also refused in CycleOwnershipAsync; this stops the hover even suggesting it. */
    pointer-events: none;
}

/* The same pill on the compact collapsed pill, where the surrounding text is
   already small, so it is sized in absolute terms rather than as a fraction of a
   much larger title. */
.module-compact-ownership-pill {
    /* Sized against the 1.6em compact title: big enough to read as a control, clearly
       subordinate to the name. No margin-left / vertical-align any more -- those were for
       the old inline placement beside the name; it is a centred block in a flex column now. */
    font-size: 1.1em;
    padding: 3px 12px;
    border-radius: 12px;
    border: 1px solid var(--df-border, #ccc);
    background-color: var(--df-surface-muted, #f2f2f2);
    cursor: pointer;
    white-space: nowrap;
}

.module-compact-ownership-pill:hover {
    border-color: var(--df-accent, #888);
}

/* A pill the user cannot change: a mandatory module (a pension belongs to one named person, and
   re-owning one would leave the household holding two of the same person's and none of the other's)
   or a Split module (the household's by construction, its ownership expressed by the PersonItems
   inside it). It still reads as a fact about the module, so unlike `.disabled` it is NOT faded and
   keeps pointer-events -- what it drops is the click AFFORDANCE.

   The signal is LIGHTER TEXT, keeping the pill's shape. Dropping the cursor and the hover alone left
   it identical to an editable pill until you hovered one, so the affordance was discoverable a pill at
   a time. Greyed text is the most universally-read "you cannot change this" in any UI, and unlike
   removing the border it leaves the pill recognisable as the ownership marker on every card, which
   matters now that read-only pills OUTNUMBER editable ones in Couple Mode (three household modules
   plus four per-person pensions).

   #888 is a chosen value with a disclosed contrast cost, not an accident. Against this pill's #f2f2f2
   fill it is ~3.2:1, which clears WCAG AA for LARGE text (3:1) but not the 4.5:1 an AA normal-size
   label needs. The expanded pill is 1.3em at weight 600, so it plausibly counts as large; the
   COLLAPSED one is 1.05em and does not, so that is the case that falls short. An earlier #5a5a5a sat
   at ~6.4:1 and cleared both; the lighter grey was preferred on looking at it, which is a product call
   about how loudly a fact the user cannot change should speak. Recorded here so the next reader knows
   the number was weighed rather than guessed, and so it is one edit to move if the call changes.

   Compounding with `.disabled` is where it bites hardest: a switched-off TaxAccount hits both (that
   module is Split AND optional), and 0.5 opacity over this grey is well under any threshold. It
   applies only to a module the user has switched off, whose content is inert anyway.

   Both the expanded and the compact pill are covered; the hover rules above are overridden by
   restating the resting values at equal-or-higher specificity. */
.module-ownership-pill-readonly,
.module-ownership-pill-readonly:hover {
    cursor: default;
    color: var(--df-text-muted, #888);
    border-color: var(--df-border-faint, #ccc);
    background-color: var(--df-surface-muted, #f2f2f2);
}

/* The glyphs step down with the text, or a full-strength picture beside quietened words reads as an
   unfinished style rather than a deliberate one.

   The `:hover` line is NOT redundant, and leaving it out was a live bug: the brighten rule above is
   `.module-ownership-pill:hover .module-ownership-pill-glyph`, which is a THREE-class selector, so a
   two-class `.module-ownership-pill-readonly .module-ownership-pill-glyph` loses to it on specificity
   however far down the file it sits. Restating at equal specificity is what lets source order decide,
   the same move the pill's own read-only rule makes directly above -- the idiom just has to be applied
   per ELEMENT, and the glyph is a second element. */
.module-ownership-pill-readonly .module-ownership-pill-glyph,
.module-ownership-pill-readonly:hover .module-ownership-pill-glyph {
    opacity: 0.5;
}

/* A pill that is BOTH read-only and on a switched-off module does not take `.disabled`'s fade.
   Its own quietened colour already says "not a control", so the opacity adds no information and only
   subtracts contrast: stacked, the two land near ~1.6:1 against the fill, which is unreadable rather
   than merely quiet. Deactivating a module does not change WHO OWNS IT, so the fact the pill states is
   still true and still worth being able to read on a greyed card.

   `pointer-events` and `cursor` are deliberately NOT restored here -- those are the half of `.disabled`
   that is still correct, and a read-only pill refuses the click anyway (it renders with no handler).
   Only the fade is undone. Specificity is (0,3,0) against `.disabled`'s (0,2,0), so it wins outright
   rather than by source order. */
.module-ownership-pill.disabled.module-ownership-pill-readonly,
.module-compact-ownership-pill.disabled.module-ownership-pill-readonly {
    opacity: 1;
}

.module-compact-ownership-pill.disabled {
    opacity: 0.5;
    cursor: default;
    /* The click is also refused in CycleOwnershipAsync; this stops the hover even suggesting it. */
    pointer-events: none;
}

/* ─────────────────────────────────────────────────────────────────────────
   Collapsed-module density. .module-view is the collapsed card (.module-edit
   is expanded); everything visible in the collapsed header steps down one
   size so more modules fit on a /Design screen. The expanded header keeps
   the regular sizes from the rules above.
   ──────────────────────────────────────────────────────────────────────── */
.module-view {
    padding: 0.6em 1.2em;
}

.module-view .module-title {
    padding-bottom: 2px;
    margin-top: 5px;
    margin-bottom: 10px;
}

.module-view .module-title .module-name {
    font-size: 1.6em;
}

.module-view .module-collapse-chevron {
    font-size: 1.4em;
}

.module-view .module-description-summary {
    font-size: 1.25em;
    margin-bottom: 15px;
}

.module-view .module-summary {
    font-size: 1.1em;
    margin: 2px;
}

.module-view .module-summary-box {
    padding: 2px 8px;
}

.module-view .module-summary-icon {
    height: 22px;
}

/* Placeholder height tracks the shrunken pill row so the header doesn't
   jump when re-pricing finishes. */
.module-view .module-summary-loading {
    height: 35px;
    margin: 2px;
}

.module-view .module-description {
    margin-top: 4px;
    font-size: 1.05em;
}

/* The glyph is the tallest thing in this column, so it is what actually buys the height back.
   Its margins are deliberately NOT stepped down with it: the saving reads better spent on the
   glyph than on closing the gap under it, which the label and pill still need to breathe. */
.module-view .module-identity-column .module-image {
    height: 34px;
}

/* The pill steps down too, and note the DIRECTION: collapsed is smaller than expanded. It used to
   be the other way round -- 1.3em collapsed against a 1.05em base -- which is why opening a module
   made it visibly SHRINK. The base now carries the full size and this is the reduction, so the
   whole identity stack behaves like the action icons opposite it. */
.module-view .module-ownership-pill {
    font-size: 1.05em;
    padding: 3px 11px;
}

/* The stacked action icons (rename / delete / clone, move arrows) drive the
   header height on user-added modules once the text shrinks; step them down
   one size too. */
.module-view .module-side-column {
    gap: 4px;
}

.module-view .module-side-column .fa-2x {
    font-size: 1.5em;
}

.module-view .module-side-column .fa-3x {
    font-size: 2em;
}

/* The on-off switch steps down with the fa icons beside it (1.8em was sized
   for the full-size header). */
.module-view .checkbox-module-toggle {
    font-size: 1.4em;
}

/* ─────────────────────────────────────────────────────────────────────────
   Compact module layout (UIPreferences.UseCompactModuleLayout). Collapsed
   modules render as small fixed-width pills in a wrap grid; the open module
   renders full-width below all of them. See ModuleComponent.RenderCompactPill
   + DesignComponent.RenderPlanModules.
   ──────────────────────────────────────────────────────────────────────── */
/* Right-aligned row hosting the layout toggle, just above the module list
   (below the wizard / warning banners). */
.module-layout-toggle-row {
    display: flex;
    justify-content: flex-end;
    margin: 4px 10px 0 0;
}

/* The shared data-description tooltip anchors left and grows rightwards
   (300 to 500px); this row sits at the screen's right edge, so anchor the
   tooltip right and let it grow leftwards instead. */
.module-layout-toggle-row button[data-description]::after {
    left: auto;
    right: -10px;
}

/* Section nav rendered as the section header's title row: the current section is the big title, the
   other two are small jump-links to the other sections. */
.design-section-nav {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-wrap: wrap;
    gap: 4px 22px;
    margin: 0;
}

/* Now that the nav lives inside the header band, the band carries the gap that separates a section
   from the previous one. Applied consistently to the bare Modules header and the picker-section
   wrapped What-Ifs / Goals headers. */
.design-section > .section-header {
    margin-top: 26px;
}

.design-section > .picker-section {
    margin-top: 26px;
    padding-top: 0;
}

.design-section > .picker-section > .section-header {
    margin-top: 0;
}

/* The other two sections: small links, underline on hover (no pill — the header band already
   carries a background of its own). `color: inherit` rather than a value: a <button> does not
   inherit colour from its parent, so this is how the items take the band's black. */
.design-section-nav-item {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    border: none;
    background: transparent;
    padding: 0;
    color: inherit;
    font-size: 1.3em;
    font-weight: normal;
    cursor: pointer;
    white-space: nowrap;
    line-height: 1.2;
}

.design-section-nav-item:hover {
    text-decoration: underline;
    text-underline-offset: 0.3em;
}

.design-section-nav-icon {
    height: 20px;
    width: 20px;
    display: block;
}

/* The current section is the header's title: large, bold, inert. Its colour is the band's,
   inherited like every other row. */
.design-section-nav-item-current,
.design-section-nav-item-current:hover {
    font-size: 1.8em;
    cursor: default;
    text-decoration: none;
}

.design-section-nav-item-current .design-section-nav-icon {
    height: 40px;
    width: 40px;
}

/* Only the big current title is bold; the small links stay normal weight, and the count is never bold. */
.design-section-nav-item-current .design-section-nav-text {
    font-weight: bold;
}

.design-section-nav-count {
    font-weight: normal;
}

/* Nudge the detailed / compact layout toggle to sit with the section nav. Scoped to the toolbar so
   the shared .user-mode-pill (the Real / Nominal toggle and the user-mode pills) is untouched;
   only .layout-toggle-icon is unique to this toggle. */
.module-layout-toggle-row .user-mode-pill {
    padding: 5px 18px;
}

.module-layout-toggle-row .layout-toggle-icon {
    height: 15px;
}

/* Narrow viewports: keep the text (the nav is the header's title now, so hiding it would leave the
   band titleless — just three bare icons), and shrink the items a little so they fit; the nav
   wraps if it is still too wide. */
@media (max-width: 640px) {
    .design-section-nav {
        gap: 2px 14px;
    }

    .design-section-nav-item {
        font-size: 1.05em;
    }

    .design-section-nav-item-current {
        font-size: 1.4em;
    }

    .design-section-nav-icon {
        height: 17px;
        width: 17px;
    }

    .design-section-nav-item-current .design-section-nav-icon {
        height: 30px;
        width: 30px;
    }
}

.module-compact-pills {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: flex-start;
    gap: 8px;
    margin-top: 10px;
}

/* Compact mode hoists the expanded module directly below the pill grid; give
   it clear separation so it reads as its own surface, not the grid's tail.
   Detailed mode is untouched (no .module-compact-pills there). */
.module-compact-pills + .module {
    margin-top: 35px;
}

/* Three columns: full-height glyph on the left, v-centered name in the middle
   (wraps over two lines when long), vertical action-icon stack on the right.
   min-height keeps the pills uniformly tall whatever the name length. */
.module-compact-pill {
    width: 300px;
    min-height: 110px;
    display: flex;
    align-items: stretch;
    gap: 15px;
    border: 1px solid #BBBBBB;
    border-radius: 10px;
    background-color: var(--color-surface-light);
    color: #444444;
    padding: 15px;
    animation: module-enter 0.3s ease-out;
    /* Same 0.15s as the rest of the --hover-card-ring family; see .sweep-suggestion-card. */
    transition: background-color 0.15s ease, box-shadow 0.15s ease;
}

.module-compact-pill:hover {
    background-color: var(--color-pill-action-light);
    box-shadow: var(--hover-card-ring);
}

.module-compact-pill:hover .module-image {
    opacity: 1;
}

.module-compact-pill.disabled {
    color: #BBBBBB;
    border-style: dashed;
    background-color: #FCFCFC;
}

.module-compact-pill-glyph {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    cursor: pointer;
}

/* Sweeps tab "Suggested sweeps" strip: one clickable card per item-advertised suggestion.
   Clicking creates and runs the sweep. See DF.Claude/Features/SweepSuggestions.md. */
.sweep-suggestion-card {
    width: 280px;
    border: 1px solid #BBBBBB;
    border-radius: 10px;
    background-color: var(--color-surface-light);
    color: #444444;
    padding: 14px 16px;
    cursor: pointer;
    text-align: center;
    /* Same 0.15s as .add-accent-button and .wizard-existing-tile. The family was split down the middle on
       this: two members eased and two snapped, which is the same "one gesture, different look per surface"
       the underline was added to fix. */
    transition: background-color 0.15s ease, box-shadow 0.15s ease;
}

.sweep-suggestion-card:hover {
    background-color: var(--color-pill-action-light);
    box-shadow: var(--hover-card-ring);
}

/* Suggestion strip, grouped into one collapsible section per MODULE CATEGORY. What the heading names used
   to ride a chip band on every card; as a section heading it is stated once per group instead, which is both
   why the chip came off and where the height saving comes from. Sections are built from the cards on hand,
   so a category with none renders nothing and there is never an empty group to explain. */
.sweep-suggestion-sections {
    margin: 18px 0;
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/* The heading is a real <button>: keyboard-reachable, and it carries its own aria-expanded. Neutral gray
   like the chip band it replaces, for the same reason, a sweep changes nothing on click, so there is no
   consequence to signal by colour. */
.sweep-suggestion-section-head {
    width: 100%;
    appearance: none;
    font: inherit;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 7px;
    padding: 10px 12px;
    background-color: #EEEEEE;
    border: 1px solid #BBBBBB;
    border-radius: 8px;
    color: black;
    font-size: 1.4em;
    font-weight: 600;
    cursor: pointer;
}

.sweep-suggestion-section-head:hover {
    background-color: var(--color-pill-action-light);
}

.sweep-suggestion-section-head:focus-visible {
    outline: 2px solid var(--color-blue-accent);
    outline-offset: 2px;
}

/* Chevron drawn in CSS (a rotating triangle) rather than a glyph, so its direction is keyed off the SAME
   aria-expanded the button already carries and the two cannot disagree. */
.sweep-suggestion-section-chevron {
    flex: 0 0 auto;
    width: 0;
    height: 0;
    border-left: 5px solid transparent;
    border-right: 5px solid transparent;
    border-top: 7px solid black;
    transition: transform 0.16s ease;
}

.sweep-suggestion-section-head[aria-expanded="false"] .sweep-suggestion-section-chevron {
    transform: rotate(-90deg);
}

/* The module's own icon, the same SVG the picker and the wizard use. It earns its place here where the old
   grouping's single shared glyph would not have: every section shows a DIFFERENT icon, so it is the fastest
   way to find the module you were thinking about. Sized to the cap height of the label beside it so the row
   still reads as one phrase rather than as an image with text after it. */
.sweep-suggestion-section-icon {
    flex: 0 0 auto;
    width: 18px;
    height: 18px;
    object-fit: contain;
}

/* The count reads as part of the heading phrase, so it sits directly after the label rather than
   being pushed hard right: the whole row then centres as one thing. Quieter than the label, since it
   is the incidental half of "Employment (4)". */
.sweep-suggestion-section-count {
    color: var(--color-text-secondary);
    font-weight: normal;
    font-variant-numeric: tabular-nums;
}

.sweep-suggestion-section-body {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    justify-content: center;
    padding: 12px 0 4px;
}

@media (prefers-reduced-motion: reduce) {
    .sweep-suggestion-section-chevron {
        transition: none;
    }
}

/* Source module label atop a suggestion card (e.g. "Employment 1"). */
.sweep-suggestion-module {
    font-size: 1.2em;
    color: #888888;
    margin-bottom: 8px;
}

.sweep-suggestion-title {
    font-size: 1.45em;
    font-weight: bold;
    margin-bottom: 8px;
    /* Looser than the inherited 1.2, for the hover underline: by the clearance formula on
       .module-block-header-text, gap = descent + (line-height - 1em) - offset, so 1.2 gives
       0.2 + 0.2 - 0.3 = 0.1em and line 1's underline all but touches line 2. 1.4 gives 0.3em.
       These titles are 280px wide at 1.45em, so wrapping is the normal case, not the edge one. */
    line-height: 1.4;
}

.sweep-suggestion-body {
    font-size: 1.15em;
}

/* Active-What-If bolt in the detailed header: inline, to the left of the name. */
.module-whatif-badge {
    width: 3em;
    height: 3em;
    margin-right: 10px;
    vertical-align: middle;
    cursor: pointer;
}

/* Active-What-If bolt in the compact pill: inline, just to the left of the
   title, mirroring the detailed header. Pill-scaled and kept from shrinking. */
.module-compact-pill-whatif-badge {
    width: 2.4em;
    height: 2.4em;
    margin-right: 8px;
    flex-shrink: 0;
    cursor: pointer;
}

/* Override the bare .module-image sizing (55px glyph with a top margin) down
   to a pill-scale glyph; the signature rotation stays. */
.module-compact-pill .module-image {
    width: 40px;
    max-width: 40px;
    margin-top: 0;
}

/* A COLUMN: the badge and the name share the row inside, and the ownership pill sits
   on its own line beneath them, horizontally centred. */
.module-compact-pill-title {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 15px;
    text-align: center;
    cursor: pointer;
}

.module-compact-pill-title-row {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 0;
    max-width: 100%;
}

.module-compact-pill-name {
    font-weight: bold;
    font-size: 1.6em;
    /* 1.4, not the 1.2 this had, for the hover underline: by the clearance formula on
       .module-block-header-text, gap = descent + (line-height - 1em) - offset, so 1.2 leaves
       0.2 + 0.2 - 0.3 = 0.1em and line 1's underline all but touches line 2. A module name at 1.6em bold
       in a 300px pill wraps as the normal case, not the edge one, and the `overflow-wrap: break-word`
       below is this rule's own admission of that. */
    line-height: 1.4;
    overflow-wrap: break-word;
    min-width: 0;
}

.module-compact-pill-actions {
    display: flex;
    flex-direction: column;
    justify-content: space-evenly;
    align-items: center;
    flex-shrink: 0;
}

/* The on-off switch matches the fa-lg action icons beside it. Bootstrap's
   .form-check carries a 1.5rem min-height that made the switch ride higher
   than the icons; flatten the wrapper so the flex row centers all four. */
.module-compact-pill .checkbox-module-toggle {
    font-size: 1.2em;
}

.module-compact-pill-actions .form-check {
    min-height: 0;
    margin: 0;
    display: flex;
    align-items: center;
}

/* Accent-card title (Goals + What-Ifs). Cloned from module-title but tighter margins
 * so the title row's vertical bounds match the side-column icon stack — combined with
 * `.module.accent-style .row.module-header { align-items: center; }` below, the title
 * sits exactly at the midpoint of the gap between the toggle (top) and the trash
 * button (bottom). */
/* The Goal / What-If name on its own centred row below the header (the icon + kind row and the category live
   in the header block above). A flex row so the collapse chevron sits close to the name and vertically
   centred against it. */
.accent-name-row {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 8px;
    padding-top: 14px;
}

.accent-name-row .accent-name {
    font-weight: bold;
    font-size: 1.7em;
    text-align: center;
    padding-bottom: 1px;
    /* Give a two-line wrapped name a little breathing room so the descenders of the first line
     * do not almost touch the ascenders of the second. */
    line-height: 1.3;
}

.accent-collapse-chevron {
    font-size: 1.6em;
    line-height: 1;
}

/* Only the name row toggles the card (the header block above is static), so it alone carries the hand cursor
   and the hover-underline cue on the title, mirroring a module card's clickable title. */
.accent-name-row.clickable {
    cursor: pointer;
}

.accent-name-row.clickable:hover .accent-name {
    text-decoration: underline;
    text-decoration-thickness: 2px;
    text-underline-offset: 0.3em;
}

.accent-name-row.clickable:hover .accent-name.disabled {
    text-decoration-color: #DDDDDD;
}

/* A disabled Goal / inactive What-If greys its name and chevron to match the muted title + eyebrow. */
.accent-name-row.disabled .accent-name,
.accent-collapse-chevron.disabled {
    color: #BBB;
}

/* The static plain-language subtitle (the gist), on its own line directly below the title: centred, and
   smaller than the 1.7em name so it reads as a bridging line rather than a second title. */
.accent-subtitle {
    text-align: center;
    font-weight: normal;
    font-size: 1.4em;
    line-height: 1.25;
    color: #555;
    padding-top: 6px;
    padding-bottom: 4px;
}

.accent-subtitle.disabled {
    color: #BBB;
}

/* The centred [section icon + kind label] row at the very top of a picked Goal / What-If card, above the
   category eyebrow. */
.accent-kind-row {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 12px;
}

/* The "GOAL" / "WHAT-IF" kind label beside the section icon at the top of a Goal / What-If card. Uppercase
   and letter-spaced so it reads as a type marker above the category. */
.accent-kind-label {
    font-size: 1.3em;
    font-weight: 600;
    color: #444444;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    line-height: 1;
}

/* A disabled Goal / inactive What-If greys its kind label to match the #BBB title + eyebrow. */
.accent-kind-label.disabled {
    color: #BBB;
}

/* Category "eyebrow" on a Goal / What-If card: the category shown small and grey above the bold
 * title, carrying the picker's thematic grouping onto the added entry. #888 is the app-wide muted-text
 * grey (no dedicated palette var; the --color-gray-* vars are chart colours, a different role). */
.accent-category-eyebrow {
    font-size: 1.4em;
    color: #888;
    text-align: center;
    margin-top: 5px;
    margin-bottom: 5px;
}

/* The category eyebrow row inside a Goal / What-If card header, below the [icon + kind] row. Centres the
   inline eyebrow span, with a 10px gap above it separating it from the kind row. */
.accent-category-eyebrow-row {
    text-align: center;
    margin-top: 10px;
}

/* Vertically centre the left col-10 title row against the right col-2 side column
 * (toggle + trash, stacked with gap 8px). Without this Bootstrap aligns both cols
 * to the top, so the title sits next to the toggle instead of between toggle + trash. */
.module.accent-style .row.module-header {
    align-items: center;
}

.module-block {
    border-radius: 8px;
    border-color: #CCCCCC;
    border-width: 1px;
    border-style: solid;
    margin-top: 10px;
    margin-bottom: 5px;
    padding: 10px;
    max-width: 480px;
    width: 100%;
}

/* Type-identifying glyph for recurring composites (automatic transfers + its inflow/outflow
   directions, interest-rate composites). Rendered as the header's first child on its own row,
   horizontally centred, above the title (and below the tooltip / .module-block-help). Solid black
   when the composite is active; faded almost away when it is disabled, to match the greyed content. */
.module-block-icon {
    display: block;
    width: 30px;
    height: 30px;
    margin: 0 auto 6px;
}

.module-block-header.disabled .module-block-icon { opacity: 0.15; }

.module-block.disabled { border-color: #DDDDDD; }

/* Publish the chosen stripe as an inheritable custom property so descendants
   (.group-separator-caption etc.) that need to visually mask their own
   surface can pull the matching colour with one variable read. */
.module-block.even { background-color: var(--color-row-stripe-even); --row-stripe: var(--color-row-stripe-even); }

.module-block.odd { background-color: var(--color-row-stripe-odd); --row-stripe: var(--color-row-stripe-odd); }

.module-block-header {
    font-weight: bold;
    text-align: center;
    color: #333333;
    /* rem, not em: .module-block-header-title nests inside this and is also 1.5x.
       em would compound (1.5 x 1.5 = 2.25x); rem keeps both flat against the root anchor. */
    font-size: 1.5rem;
    margin-top: 10px;
    margin-bottom: 10px;
}

/* text-decoration, NEVER border-bottom, and the reason is structural rather than stylistic: this span is
   a FLEX ITEM of the inline-flex .module-block-header-title (it sits beside the chevron), so it is
   BLOCKIFIED. A block box draws its border-bottom once, at the bottom of the box, however many lines the
   text wraps to, so a composite title long enough to wrap got a single underline under the last line only.
   text-decoration is drawn per LINE BOX, so it follows every wrapped line whatever the display type.

   Transparent at rest rather than absent so hover changes only the colour: an underline that appears from
   nothing would shift nothing here (text-decoration is out of flow) but keeping the pair symmetrical is
   what lets the two hover rules below set text-decoration-color alone.

   It must stay on this span and NOT move up to .module-block-header-title: text-decoration propagates to
   the whole flex line, so on the container it would underline the chevron too. That exact mistake is
   already recorded for the sibling .module-block-link / .module-block-link-text pair. */
/* LONGHANDS, not the `text-decoration` shorthand: the shorthand resets text-decoration-thickness to
   auto, so a shorthand plus a thickness line is order-dependent and a reorder silently drops the 1px.
   The four longhands do not reset one another. */
.module-block-header-text {
    text-decoration-line: underline;
    text-decoration-style: solid;
    text-decoration-color: transparent;
    text-decoration-thickness: 1px;
    text-underline-offset: 0.3em;
    /* Looser than the inherited 1.2 (html, body) because an underlined wrapped title needs room the
       unstyled one does not. Measuring down from line 1's baseline: its underline sits at +offset, and
       line 2's ascender top sits at +descent + 2 x half-leading (one half-leading below line 1's box,
       one above line 2's text), where half-leading is (line-height - 1em) / 2. So

           gap = descent + (2 x half-leading) - offset,   descent ~ 0.2em for this font

       At 1.2 that is 0.2 + 0.2 - 0.3 = 0.1em, and the first line's underline all but touches the text
       below. At 1.4 it is 0.2 + 0.4 - 0.3 = 0.3em. (An earlier version of this comment dropped the
       descent term from the formula while keeping figures computed WITH it, so the two disagreed.)
       It costs every card header a few px of height, single-line ones included, because no selector can
       ask whether the text wrapped; that is the price of the fix, not an oversight. */
    line-height: 1.4;
    letter-spacing: 1px;
}

.module-block-header-text:hover {
    text-decoration-color: black;
    cursor: pointer;
}

.module-block-header-text.disabled:hover {
    text-decoration-color: #BBBBBB;
    cursor: pointer;
}

.module-block-header-icon {
    margin-left: 10px;
    color: #444;
    font-size: 1.1em;
}

.module-block-header-icon.disabled { color: #DDDDDD; }

/* ─────────────────────────────────────────────────────────────────────────
   Automatic money strategy: a composite the strategy has taken over is drawn as ONE LINE in its place,
   "{card name}: disabled by the automated strategy." with the link to the Money strategy control
   (ItemsBuilder.RenderStrategyFrozenLine; WithdrawalStrategies.md §12). The card and its controls are not
   drawn at all. Graphite, deliberately NOT the AI violet (that stays reserved for AI-owned plans), and
   sized like the card it stands in for so it sits in the same slot of the module's grid. */
.strategy-frozen-line {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 9px;
    max-width: 480px;
    width: 100%;
    margin-top: 10px;
    padding: 9px 14px;
    background-color: #EEF0F3;
    border: 1px solid #6B7280;
    border-radius: 8px;
    color: #3A3F4A;
    font-size: 1.5rem;
    font-weight: 500;
}

.strategy-frozen-lock { width: 30px; height: 30px; flex: 0 0 auto; }

.module-block-header-title {
    display: inline-flex;
    margin-left: 8px;
    font-size: 1.5rem;
}

.module-block-help {
    font-size: 1.25em;
    color: #AAAAAA;
    margin: 10px;
    text-align: center;
    letter-spacing: 0.5px;
}

/* The send-feedback glyph, shared by the three places one is offered: an item's guidance line, a module
   header, a Results report. Every declaration here used to be an inline `style` re-typed at each of those
   sites, while the wrapper classes beside them (.feedback-item-icon / -module-icon / -results-icon) carried
   no CSS at all, so the class names looked like the styling hook and were not. Three hand-copied strings had
   already drifted (two at `margin: 5px`, Results at `margin-left: 30px`).

   `margin-left` ONLY, never the `margin: 5px` shorthand it replaces: the glyph is vertical-align: middle
   inside a line of prose, so a top and bottom margin grows the line box and leaves the guidance line's last
   row visibly taller than the rows above it. The gap wanted here is horizontal, from the word before. */
.feedback-icon {
    width: 15px;
    height: 15px;
    opacity: 0.3;
    cursor: pointer;
    vertical-align: middle;
    margin-left: 5px;
}

/* Results sets the glyph apart from the nav strip it follows rather than from a preceding word, so it keeps
   the wider gap it already had. Scoped through the wrapper class, which is what finally gives that class a
   job; the size / opacity / cursor still come from the base rule above. */
.feedback-results-icon .feedback-icon { margin-left: 30px; }

.module-block-help.disabled { color: #CCCCCC; }

.module-block-summary {
    border-top: #CCCCCC 1px solid;
    font-size: 1.3em;
    margin-top: 10px;
    text-align: center;
    padding: 10px;
    line-height: 160%;
}

/* A curve profile card shows its glide description and its "Used by ..." links as two lines inside
   one summary block; the links sit a blank line below the description (no second gray border). */
.module-block-summary-links {
    margin-top: 1.4em;
}

/* The profile-picker relationship summary (the allocation glide + the "Defined in Settings ..."
   reverse link) sits below the profile dropdown, mid-composite, where the .module-block-summary top
   border reads as a stray divider rather than a title/summary rule. Drop the border there. The
   Settings card keeps its line (below its title, where it reads as intentional). Compound selector so
   it wins over .module-block-summary regardless of rule order. */
.module-block-summary.module-block-summary-borderless {
    border-top: none;
}

.module-block-summary .time {
    margin-bottom: 5px;
    color: #BBBBBB;
}

.module-block-summary.disabled { color: #CCCCCC; }

/* The cost-share bar: how much of its enclosing section a cost composite accounts for. Last line of the
   header, below the summary it is derived from.

   A BAR rather than a pie, and deliberately: each card shows only its OWN slice, so a pie would be N pies
   each highlighting one wedge -- N times the ink for one number. Because the cards wrap in a flex row
   (.module-group-items), a bar on each one IS a horizontal bar chart read down the group, with no chart
   library involved.

   Quieter than .module-block-summary above it (smaller, gray label, no top border of its own): it is a
   derived comparison, not the card's own figure, and the summary line must stay the thing the eye lands
   on. */
.module-block-share {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 0 10px 10px;
    /* The UN-fade, and it is deliberately immediate: a new pricing's figure is current the moment it
       lands, so making the reader wait to see it at full strength would be theatre. The delayed fade in
       the other direction lives on .module-block-share-stale, which overrides this while the class is on
       -- that asymmetry is the point, and one shared transition could not express it. */
    transition: opacity 0.15s linear;
}

/* A pricing is in flight, so the figure shown is the PREVIOUS simulation's: still true, no longer current.
   Faded rather than removed, because blanking it would make the bars disappear and reappear on every
   keystroke, and rather than replaced by a spinner, because one spinner per cost card on a module with
   half a dozen of them is a strobing page. Opacity alone, so the bar keeps its geometry and nothing
   reflows as it settles. */
.module-block-share-stale {
    opacity: 0.3;
    /* The DELAY is the whole mechanism: pricings fire on every keystroke and mostly finish in well under
       400ms, so a fade that started immediately would strobe the bars while the user types. Delaying past
       the common case means a fast pricing completes before the fade ever begins and the bar simply never
       flickers; only a pricing slow enough to be worth noticing gets to show its staleness. This is the
       no-spinner alternative, and the delay is what makes it quieter than one rather than merely
       different. */
    transition: opacity 0.2s linear 0.4s;
}

/* flex:1 so the track fills whatever the label leaves, keeping the bars of sibling cards aligned at both
   ends and therefore comparable at a glance -- the whole point of drawing them. min-width:0 because a flex
   item defaults to min-content, which would otherwise stop the track shrinking on a narrow card. */
.module-block-share-meter {
    flex: 1;
    min-width: 0;
    height: 12px;
    border-radius: 6px;
    background-color: #EEEEEE;
    overflow: hidden;
}

/* Lighter than the figure beside it, deliberately and not by oversight: the bar is a shape and reads at a
   much lower contrast than text of the same colour, so matching the two made the bar shout. Chosen by eye
   against the real cards. */
.module-block-share-fill {
    height: 100%;
    background-color: #BBBBBB;
    border-radius: 6px;
}

/* font-weight:normal is load-bearing, not tidiness: .module-block-header sets bold, which inherits, and a
   bold label here would read as heavier than the summary line above it and invert the intended emphasis.
   font-size in em compounds off the header's 1.5rem exactly as .module-block-summary's 1.3em does, so 1em
   keeps this the quieter of the two by construction rather than by a hand-matched absolute size.
   The label MAY wrap, but at exactly one point: the space between the figure and the phrase, each of which
   is itself nowrap. That is what keeps the original no-jitter property while letting a longer scope than
   "all costs" fit a narrow card - free word wrapping would move the break every time the figure changed
   width by a character, which is the jitter the nowrap was there to stop. */
/* Lighter than the figure it trails, so the hierarchy survives the two sharing a hue: with the percentage
   pulled onto the bar's own grey, leaving this at that same grey would flatten the pair into one tone. */
.module-block-share-text {
    font-size: 1em;
    font-weight: normal;
    color: #AAAAAA;
    /* min-width:0 so this can shrink below its content and let the wrap actually happen: a flex item
       defaults to min-content, which for a nowrap child is the whole unbroken phrase. */
    min-width: 0;
    text-align: right;
}

/* The scope phrase, kept unbreakable so the wrap can only fall before "of" - never inside it. */
.module-block-share-scope {
    white-space: nowrap;
}

/* The figure carries the weight: a mid grey, darker than the bar it labels and than the phrase it trails,
   with size and weight doing most of the emphasis. Sized in em so it scales with that phrase rather than
   against a second absolute anchor. */
.module-block-share-percent {
    font-size: 1.35em;
    font-weight: bold;
    color: #888888;
    /* Never split a figure across lines. */
    white-space: nowrap;
}

.module-block-header.disabled { color: #CCCCCC; }

/* The app's one "Add ..." affordance for a dynamic list: a leading plus glyph and a black label
   carrying the house detached-dotted underline that turns solid on hover, the same idiom as
   .item-info-action and .explore-clone-link. Every add link renders through
   RendererDesignControlsExtensions.AddDesignAddLink, so the framed list, the hoisted-children list,
   the frameless floating link and the ERC schedule's band link cannot drift apart.

   Flex + gap puts the glyph to the LEFT of the label and centres the pair as a unit, which is why
   there is no text-align here: justify-content already does it, and two mechanisms for one job drift.

   The underline sits on the LABEL (.module-block-link-text), never on this container. On the
   container it would run under the glyph too, which reads as a stray rule beside it rather than as
   a link -- the same lesson .module-group-link records.

   The vertical margin gives a stack of links a rhythm. A multi-choice list renders one link PER
   remaining choice (State Pension's voluntary contributions offers one per buyable tax year, so seven at
   once), and with no margin they run together into a single block of near-identical text. Adjacent
   siblings collapse to one 8px gap rather than summing to 16, which is the intent: 8px between links, and
   the same 8px separating the first from the rows above it. .expandable-floating-link overrides the
   shorthand wholesale, so the floating shape keeps the asymmetric margins tuned for its own context.

   It sits on the BASE deliberately, so it reaches every add link: the framed and hoisted lists, the ERC
   band link, the allocation-curve point link, and button.module-block-link (the action factory, whose
   chrome reset does not restate margin). A single link gains a little breathing room from the rows above
   it, which is right on its own terms; the multi-choice stack is only what made the absence obvious. */
.module-block-link {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    margin: 8px 0;
    font-size: 1.5rem;
    color: #000000;
    cursor: pointer;
}

/* The action factory's add affordance opens a MENU rather than adding directly, so it has to be a
   <button> for Bootstrap's dropdown JS to bind to. Strip the UA button chrome so nothing but the shared
   link look shows. Colour and font-size are NOT restated: .module-block-link already sets both and beats
   the UA sheet, whereas a `font: inherit` here would out-specify it and undo the sizing.

   inline-flex, NOT the base's full-width flex: Popper anchors .dropdown-menu to the toggle's own box, so a
   row-wide button drops the menu at the row's left edge instead of under the label. Shrink-to-fit keeps the
   anchor on the label, and the ancestor's text-align:center still centres it (an inline-level box, unlike
   the block-level flex it would otherwise be). */
button.module-block-link {
    display: inline-flex;
    width: auto;
    background: none;
    border: none;
    padding: 0;
    font-family: inherit;
}

/* 1em so the glyph tracks the label's size, matching .item-info-action-icon (the same
   glyph-beside-a-dotted-link shape). flex-shrink keeps it circular if the row ever runs short. */
.module-block-link-icon {
    width: 1em;
    height: 1em;
    flex-shrink: 0;
}

.module-block-link-text {
    text-decoration: underline;
    text-decoration-style: dotted;
    text-underline-offset: 0.3em;
}

.module-block-link:hover .module-block-link-text {
    text-decoration-style: solid;
}

/* A module-block-link rendered frameless within a group (no parent composite around it).
   module-block-link already centres its glyph + label. width:100% makes it claim a full
   row in the d-flex flex-wrap items container, so the link sits on its own line below the
   child rows (h-centered) instead of wrapping in beside the last one. */
/* The asymmetry is deliberate and faces the LIST: the roomy edge is the one between the link and
   the rows it grows, the tight edge is the one facing away, so the link reads as attached to the
   list without crowding it. The values were 6px top / 14px bottom while the link rendered ABOVE
   the rows; moving it below inverted which edge faces them (.module-block is 10px top / 5px
   bottom, and there is no margin collapsing in the flex container, so the gap to the rows had
   gone 14+10=24px to 5+6=11px with the roomy 14px left dangling at the end). Flipped to match. */
.expandable-floating-link {
    width: 100%;
    margin: 14px 0 6px 0;
}

/* The share bar on a FLOATING-LINK list, which has no card header to sit in: it renders as a bare sibling
   of the "Add ..." link, directly in the module's wrapping .module-group-items flex row. Without a width
   it would be sized by its content and pack onto the same flex line as whatever card precedes it, so it
   claims a whole row the same way .expandable-floating-link and .sibling-separator do.

   The padding restates .module-block-share's, minus its bottom: in a card header the bar is the last thing
   before the block's own edge, whereas here the add link follows it and brings its own top margin. */
.module-block-share-floating {
    width: 100%;
    /* Centred, so it reads as a unit with the centred "Add ..." link directly below it rather than as a
       rule ruled across the whole module. */
    justify-content: center;
    /* Detaches it from the row above. In a card the bar sits inside the block's own border and the frame
       does the separating; floating, there is no frame between it and the last hoisted child, so without
       this it reads as part of that card rather than as the section's total. MARGIN, not padding: the
       padding shapes the bar, this is the gap to the neighbour. */
    margin-top: 20px;
    padding: 0 10px;
}

/* A card's meter is flex:1 and therefore card-width, which is what makes sibling cards' bars comparable at
   a glance. Floating, that same flex:1 stretches the track across the ENTIRE module - a ~1150px rule whose
   label ends up stranded a screen away from its fill, reading as a different kind of thing from the bars
   inside the cards above it. It is the same kind of fact - one claimant's share - so it is drawn at the
   same scale. */
.module-block-share-floating .module-block-share-meter {
    flex: 0 1 380px;
}

/* Extra top spacing for a floating link that follows a sibling item in its group (e.g. the
   Private Pension "other pots" link, which sits below the always-present Workplace pension pot),
   so it detaches from the item above rather than reading as an appendage of it. Applied via
   FloatingLinkFollowsContent; most floating-link lists sit at the top of their group and keep the
   base margin above.

   Deliberately says "detaches from the item above" rather than "reads as the header for the rows
   below": the link always renders AFTER the rows, where it heads nothing. So this governs the
   EMPTY list, where the link is all there is and butts up against whatever precedes it; once the
   list has rows they supply the separation and the extra margin is harmless. Where the detaching
   needs to be visible rather than just roomier, the property carries [GroupSeparatorAbove] instead
   and gets the .sibling-separator band below (SelfEmployment.CapitalPurchases does; the pots list
   carries both, its band above the list and this margin above the link at the end of it).

   Only the TOP is raised. The bottom matches the base 6px, because nothing below the link needs a
   gap whichever variant is in play; the 15px it used to carry was the roomy edge from back when
   this margin sat above the rows. */
.expandable-floating-link-spaced {
    margin: 25px 0 6px 0;
}

/* The band drawn BETWEEN two consecutive sibling GROUPS (the rows of a dynamic list whose rows hoist),
   and above any item carrying [GroupSeparatorAbove] that renders at card level. Never above the first,
   never below the last, so N groups draw N-1 bands and a lone group draws none. It is an empty
   10px-tall .module-group: the same border, radius and fill as the group header above it, so a group
   boundary reads as a quieter echo of a section boundary rather than as a hard black line.

   NOT drawn between the cards WITHIN a group -- see the width note below.

   The box must line up with .module-group exactly, so it repeats that rule's 10px side margin and
   subtracts it from the width. .module-group is a sibling of the form.module-group-items that holds
   the cards, so a bare width:100% here overhangs the header above it by exactly those 20px, which is
   visible and looks like a mistake.

   Full-bleed width is load-bearing, not cosmetic. The tier-1 card container is a wrapping flex row,
   so an element that fills the line claims a whole row and forces a line break -- the same trick
   .expandable-floating-link uses. That is what keeps each group's cards on their own line(s) instead
   of wrapping in beside the next group's and stranding a card next to a title it does not belong to.
   calc(100% - 20px) plus the two 10px margins still fills the line, so the break survives the
   alignment fix. Without the break the band would be decoration on a layout that had already
   scrambled. */
.sibling-separator {
    width: calc(100% - 20px);
    height: 10px;
    margin: 12px 10px 8px 10px;
    padding: 0;
    box-sizing: border-box;
    border: #AAA solid 1px;
    border-radius: 7px;
    background-color: var(--color-pill-action-shadow);
}

.btn-close:focus {
    box-shadow: none;
    outline: none;
}

.action-bar {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 4px 15px;
    flex-wrap: wrap;
    font-size: 1.35em;
}

.action-bar-arrow {
    font-weight: bold;
    font-size: 2em;
    margin-left: 30px;
}

.action-separator {
    white-space: nowrap;
    display: inline-flex;
    align-items: center;
}

.action-separator::before {
    content: "|";
    color: #ccc;
    margin-right: 6px;
}

.action-bar > .action-separator:first-child::before {
    content: none;
}

/* NO SEPARATORS ON A PHONE, a wider gap instead (user, 2026-09-24, from a phone pass). The bar wraps
   there, and each link carries its "|" BEFORE itself, so every link that wrapped to the start of a line
   opened it with a stray bar ("| Describe", "| Rename"). Which item starts a wrapped line is not
   something CSS can select, so the separators go wherever the bar is likely to wrap, and the gap alone
   separates the links, the ordinary shape of a wrapping row of links on a phone. */
@media (max-width: 576px) {
    .action-bar {
        column-gap: 16px;
        row-gap: 6px;
    }

    .action-separator::before {
        content: none;
    }
}

.action-link {
    cursor: pointer;
}

.action-link:hover {
    text-decoration: underline;
    /* Drop the rule clear of the baseline: the browser default does not scale with font size, so on this
       bar (1.35em) it sits tight enough to read as part of the word rather than as an affordance under it.
       em-relative, and that is the whole point rather than house style: a px offset has the same defect it
       is fixing, and would go tight again on a user font-size bump or an edit to .action-bar. It also puts
       the rule below the descender at EVERY size (Tahoma descends ~0.2em, the figure
       TestCompositeHeaderUnderline uses), so "Duplica" in it-IT underlines through rather than having a
       gap carved around the p by text-decoration-skip-ink. Computed from that metric, not browser-checked. */
    text-underline-offset: 0.3em;
}

.description-placeholder {
    color: #999;
    font-style: italic;
    padding: 10px 0;
    font-size: 1.4em;
}

.description-readonly {
    white-space: pre-wrap;
    word-break: break-word;
    padding: 10px 0;
    font-size: 1.4em;
}

.description-textarea {
    resize: vertical;
    min-height: 120px;
    font-size: 1.4em;
}

.description-counter {
    text-align: right;
    font-size: 0.85em;
    color: var(--color-text-secondary);
    margin-top: 4px;
}

.description-counter-over {
    color: #dc3545;
    font-weight: bold;
}

.flag-icon {
    width: 1.33333em;
    height: 1em;
    vertical-align: middle;
    margin-right: 5px;
    object-fit: contain;
}

/* DropdownComponent: the ONE dropdown whose options can carry an image (a flag), used by the project switcher and the
   Profile page. Its parts are buttons, so they start from a reset: the browser's button face, font and alignment. */
.action-dropdown {
    position: relative;
    display: inline-block;
}

.action-dropdown-selected {
    border: 1px solid #ccc;
    border-radius: 4px;
    padding: 4px 8px;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    white-space: nowrap;
    background: transparent;
    color: inherit;
    font: inherit;
}

/* The switcher's project name reads as the page's subject, so it is bold; a country or a language is not. */
.action-dropdown-project .action-dropdown-selected {
    font-weight: bold;
}

/* The new-project country picker sits after the Create link in the action bar. */
.action-separator .action-dropdown {
    margin-left: 8px;
}

.action-dropdown-selected:hover {
    border-color: #999;
}

.action-dropdown-chevron {
    font-size: 0.6em;
    margin-left: 2px;
}

.action-dropdown-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 999;
}

.action-dropdown-options {
    position: absolute;
    top: 100%;
    left: 0;
    z-index: 1000;
    background: white;
    border: 1px solid #ccc;
    border-radius: 4px;
    margin-top: 2px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
    min-width: 100%;
}

.action-dropdown-option {
    width: 100%;
    border: none;
    background: none;
    color: inherit;
    font: inherit;
    text-align: left;
    padding: 4px 10px;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 6px;
    white-space: nowrap;
}

.action-dropdown-option:hover {
    background: #f0f0f0;
}

.action-dropdown-option:first-child {
    border-radius: 4px 4px 0 0;
}

.action-dropdown-option:last-child {
    border-radius: 0 0 4px 4px;
}

.action-dropdown-option-selected {
    background: #e8f0fe;
}

.action-select {
    border: 1px solid #ccc;
    border-radius: 4px;
    padding: 4px;
    font-size: inherit;
    cursor: pointer;
    font-weight: bold;
}

.action-select:hover {
    border-color: #999;
}

/* Banded heading every /Design section (Modules picker, Goals, What-Ifs) opens
   with. Three rows: the .section-header-tagline kicker, then the icon + title
   line, then .picker-header-stats carrying the counts plus the section hint in
   one sentence. The type scale lives on the title row, not the band, so the
   tagline and stats lines keep their own sizes. The margins are the pre-split
   computed values (2em / 0.5em of the old 1.8em font); the padding is a flat
   inset, the rows carry their own spacing.

   It reads as a RULED BAND rather than a card: square corners, no side border,
   and a thick rule above and below that separates one section from the next.

   One place owns the text colour. Every row inside the band (tagline, nav links,
   title, stats, upsell link) inherits this black instead of declaring a muted
   grey of its own, so the hierarchy is carried by size, weight and case alone.
   A <button> does not inherit colour from its parent, so .design-section-nav-item
   opts in explicitly with `color: inherit`. */
.section-header {
    text-align: center;
    margin: 3.6em 10px 0.9em;
    background: #f2f2f2;
    padding: 10px;
    border-top: 10px solid #ddd;
    border-bottom: 10px solid #ddd;
    color: var(--color-black);
}

.section-header-title {
    font-size: 1.8em;
    font-weight: bold;
}

.section-header-icon {
    margin-top: -10px;
    margin-right: 15px;
    height: 40px;
    width: 40px;
}

.module-buttons {
    display: grid;
    /* auto-fit + minmax reflow by actual container width, not viewport width, so
       that comparison-mode sides (half the screen) can collapse to a single column
       without a separate breakpoint. 260px is the smallest a card stays usable. */
    grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
    padding: 10px;
}

.module-picker-group {
    background: #fff;
    border-radius: 8px;
    margin: 30px 10px 0;
    /* grid tracks default to `min-content` width, which expands to fit the widest
       child. Force tracks to honour the track size instead so the card reflows. */
    min-width: 0;
}

.module-picker-group:hover .module-picker-group-title {
    color: #000;
}

.module-picker-group:hover .module-picker-group-subtitle {
    color: #555;
}

.module-picker-group-title {
    font-size: 1.5em;
    color: #999;
    margin-bottom: 0.25em;
    text-align: center;
    font-weight: bold;
    transition: color 0.15s ease;
}

.module-picker-group-subtitle {
    font-size: 1.1em;
    color: #aaa;
    margin-bottom: 0.75em;
    text-align: center;
    transition: color 0.15s ease;
}

/* Tiles top-align so every glyph sits on the same line even when titles
   wrap to different line counts (same treatment as .menu-tiles-row, which
   carries this class too). */
.module-picker-group-row {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: flex-start;
}

/* width: min-content sizes each tile to its longest word, so titles wrap at
   every word boundary but a long single word ("Compensation") never breaks
   mid-word — the tile widens for it instead of splitting it. min-width keeps
   short-named tiles from collapsing. */
.add-module-button {
    width: min-content;
    min-width: 80px;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    align-items: center;
    margin: 5px;
    position: relative;
}

/* Fixed-height icon box: glyphs share a baseline across the row regardless
   of how many lines each tile's title wraps to. */
.add-module-button > div:first-child {
    height: 60px;
    display: flex;
    align-items: center;
    justify-content: center;
}

.add-module-button::before {
    content: "+";
    position: absolute;
    top: 0;
    left: 6px;
    font-size: 2.2em;
    font-weight: 900;
    line-height: 1;
    color: #555;
    opacity: 0;
    transition: opacity 0.15s ease;
    pointer-events: none;
}

.add-module-button:hover::before {
    opacity: 1;
}

/* Picker glyphs sized tighter (45px) than the 55px module-header glyph the
   bare .module-image rule targets. Constrained by HEIGHT (not width) so glyphs
   of different aspect ratios read at one uniform height in the tile grid. The
   item-level menu tiles re-tighten to 40px via .add-module-button .menu-tile-image
   below (same specificity, later in the file). */
.add-module-button .module-image {
    opacity: .4;
    max-height: 45px;
}

.add-module-description {
    color: #AAAAAA;
    margin-top: 7px;
    margin-bottom: 2px;
    font-size: 1.2em;
    text-align: center;
    width: 100%;
    word-break: normal;
    overflow-wrap: break-word;
    font-weight: bold;
}

.add-module-button:hover {
    cursor: pointer;
    color: black;
}

.add-module-button:hover .module-image {
    opacity: 1;
}

.add-module-button:hover .add-module-description {
    color: black;
}

.add-module-button-disabled {
    cursor: default;
}

/* Fade the icon + name (NOT the count badge) so the "N max" badge stays legible on an at-capacity
   tile. All three pickers fade their content only (the accent Goal / What-If tiles the same way via
   .add-accent-button-disabled), never the whole tile, because each carries a corner count badge that
   must stay readable. The name fades from full to the shared disabled opacity. The icon is ALREADY at
   0.4 at idle (see .add-module-button .module-image), so fading it to the same 0.4 would leave it
   visually unchanged; it fades FURTHER, to --picker-module-icon-disabled-opacity, so the icon greys too. */
.add-module-button-disabled .add-module-description {
    opacity: var(--picker-tile-disabled-opacity);
}

.add-module-button-disabled .module-image {
    opacity: var(--picker-module-icon-disabled-opacity);
}

.add-module-button-disabled:hover {
    cursor: default;
    color: inherit;
}

.add-module-button-disabled:hover::before {
    opacity: 0;
}

.add-module-button-disabled:hover .module-image {
    opacity: var(--picker-module-icon-disabled-opacity);
}

.add-module-button-disabled:hover .add-module-description {
    color: #AAAAAA;
}

/* --- Design-page module picker: mini-card treatment ------------------------
   Scoped to `.module-picker-group` so ONLY the design-page module picker adopts
   the Goals / What-If accent mini-card look (bordered, rounded, uniform width,
   equal-height rows, no "+"). The bare `.add-module-button` rules above are
   deliberately left intact, so the in-module menu-tile pickers (SpecialAssets,
   Milestones, ...) keep their floating-icon affordance. Border / surface / hover
   values mirror `.add-accent-button` below so the two families read as one system. */

/* Cards on the same row share the tallest card's height; the row gap replaces
   the per-tile margin. */
.module-picker-group .module-picker-group-row {
    align-items: stretch;
    gap: 8px;
}

/* The tile becomes a bordered, rounded card. Identical 115px width; a 115px *floor* on
   the height (min-height, not a hard height) so the row's `align-items: stretch`
   can equalize each row AND a long two-line caption grows the card rather than
   spilling past the border. The extra TOP padding (18px vs 5px on the other sides)
   clears the .picker-count-badge corner overlay, so the icon never sits under it.
   Tiles are content-box, so padding + the 1px border add outside these dimensions. */
.module-picker-group .add-module-button {
    position: relative;   /* anchors the .picker-count-badge corner overlay */
    width: 115px;
    min-height: 115px;
    min-width: 0;
    margin: 0;
    padding: 18px 5px 5px 5px;
    background: var(--color-surface-light);
    border: 1px solid #ccc;
    border-radius: 12px;
    transition: background-color 0.15s ease, box-shadow 0.15s ease;
}

/* Corner count badge, SHARED by all three Design-page pickers: the module tile (ModuleAddComponent), the
   Goal tile (DesignComponent) and the What-If tile (WhatIfsPanel), all via RenderTileCountBadge. How many
   of this type the plan holds. A compact pill in the top-right, absolutely positioned so it adds no
   tile height. It sits OUTSIDE each tile's content-only disabled fade (see .add-module-button-disabled /
   .add-accent-button-disabled), so "N max" stays legible on an at-capacity tile. */
.picker-count-badge {
    position: absolute;
    top: 4px;
    right: 4px;
    min-width: 26px;
    height: 26px;
    padding: 0 7px;
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 13px;
    background: #fff;
    border: 1px solid #ccc;
    color: #888;
    font-size: 1.4em;
    font-weight: bold;
    white-space: nowrap;
    /* A faded tile icon (opacity < 1) paints in the positioned layer, so it would otherwise render ON TOP
       of this earlier-in-DOM absolute badge. z-index lifts the badge above it so its solid background
       reads solid, not "see-through" over the icon. */
    z-index: 1;
}

/* Slightly shorter icon box + smaller caption than the floating picker, so a
   long single-word name ("Compensation") still fits the fixed card width. */
.module-picker-group .add-module-button > div:first-child {
    height: 52px;
}

.module-picker-group .add-module-description {
    font-size: 1.05em;
    margin-top: 10px;
    /* 1.4, for the hover underline, and it lives HERE rather than on the bare .add-module-description
       because that is where the underline lives: the cue is scoped to .module-picker-group, so scoping
       the compensation anywhere wider hands the extra height to tiles that never get the cue. The two
       that would have paid are MenuTileRenderer's in-module tiles and the wizard's
       .module-picker-group-row menu-tiles-row grid, which is -row and NOT .module-picker-group; both are
       110px menu tiles, i.e. the ones that wrap most.

       The number: by the clearance formula on .module-block-header-text, gap = descent +
       (line-height - 1em) - offset, so the inherited 1.2 leaves 0.2 + 0.2 - 0.3 = 0.1em and line 1's
       underline all but touches line 2. 1.4 gives 0.3em. */
    line-height: 1.4;
}

/* No "+" glyph on the picker cards. */
.module-picker-group .add-module-button::before {
    content: none;
}

/* Hover takes the shared clickable-card treatment (fill + ring, see --hover-card-ring
   at the top of this file), exactly like the accent mini-cards and the sweep cards. */
.module-picker-group .add-module-button:not(.add-module-button-disabled):hover {
    background-color: var(--color-pill-action-light);
    box-shadow: var(--hover-card-ring);
}

/* A single-instance module already added reads as a faint dotted card. The `:not()` on
   the hover rule above is what keeps it inert, so this needs no `:hover` twin -- and
   must not grow one, or the next hover property added above would have to be undone
   here too. Same values as `.add-accent-button-disabled` below, via the shared tokens. */
.module-picker-group .add-module-button-disabled {
    border-color: #ccc;
    border-style: dotted;
    border-width: var(--picker-tile-disabled-border-width);
}

/* Menu-items container -- tile grid uses the shared .add-module-button base rules
   (floating icons on the module body background); the design-page picker's mini-card
   treatment is scoped to .module-picker-group, so these in-module tiles are unaffected
   and keep the plain floating-icon look. Explicit width + flex-wrap
   so the row fully clears its line and added children stack strictly below
   the menu, never beside it. Icon sizing slightly smaller than the module
   picker so item-level tiles read as a smaller-grain affordance. */

/* Optional call-to-action caption a menu container can render above its tile
   grid (see BaseMenuItemsContainer.AddPrompt). Makes a single-tile menu's
   purpose obvious; multi-tile menus typically leave it blank. */
.menu-tiles-caption {
    margin-top: 8px;
    font-size: 1.2em;
    color: #555;
}

.menu-tiles-caption.disabled {
    opacity: 0.4;
}

.menu-tiles-row {
    display: flex;
    flex-wrap: wrap;
    width: 100%;
    margin-top: 5px;
    margin-bottom: 12px;
}

/* Two-class chain so this beats the 45px in `.add-module-button .module-image`
   above (equal specificity, later in the file). */
.add-module-button .menu-tile-image {
    max-width: 40px;
}

/* Three-class chain needed because `.module-edit .module-image { opacity: 1 }`
   has specificity (0,2,0) and would otherwise force the tiles to full opacity,
   making the glyphs render as solid black blocks. Matching the module-picker
   "add-module-button" fade at 0.4 idle / 1 on hover gives the icons the same
   washed-out look as the rest of the app. */
.module-edit .add-module-button .menu-tile-image {
    opacity: 0.4;
}

.module-edit .add-module-button:hover .menu-tile-image {
    opacity: 1;
}

/* Long menu-tile titles ("UK coins (Sovereigns and Britannias)", ...) wrap
   within a fixed tile width instead of stretching the tile. The wrap + the
   top-aligned rows + the fixed-height icon box now live on the base picker
   rules (.module-picker-group-row / .add-module-button above); the tiles
   only pin a fixed width and a slightly shorter icon box to match their
   smaller 40px glyphs. */
.menu-tiles-row .add-module-button {
    width: 110px;
}

.menu-tiles-row .add-module-button > div:first-child {
    height: 56px;
}

/* The `.module-block.menu-child` enter-fade that used to sit here is gone, along with its class hook
   (ItemsBuilder.Composites.cs). It played a 0.3s module-enter fade on a freshly added menu child, and it
   worked, on the right element, off screen: a menu container draws its tile grid above its children, so
   the new card lands well below the tile that was clicked (measured on Milestones at 1280x720, roughly
   two viewport heights down with five milestones). A newly added dynamic child now gets the reveal +
   border flash every newly added module, goal and What-If gets, via the add-highlight. Note the
   `menu-child-type-badge` rules below are a DIFFERENT hook and are unaffected. */

/* Type badge above a menu-added child's composite header: SVG + type label.
   Purely visual cue -- fixed by the concrete type, never interactive. The
   row is horizontally centered so the block reads as a header banner rather
   than a left-aligned prefix. */
.menu-child-type-badge {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 0px 10px 7px 10px;
    border-bottom: 1px solid #ccc;
}

.menu-child-type-badge.disabled {
    opacity: 0.4;
}

/* !important + descendant selector because scoped/framework CSS elsewhere
   cascades opacity: 0 onto unclassed images inside optional-item containers,
   which was making the badge icon invisible even though our .menu-child-type-badge-icon
   rule declared opacity: 0.6. */
.menu-child-type-badge img.menu-child-type-badge-icon {
    width: 35px;
    height: 35px;
    opacity: 0.6 !important;
}

.menu-child-type-badge-label {
    font-weight: normal;
    font-size: 1.5em;
    color: #444444;
    letter-spacing: 0.02em;
}

/* --------------------------------------------------------*/
/* GOALS */
/* --------------------------------------------------------*/

.picker-section {
    margin-top: 12px;
    padding: 12px 0;
}

.section-subheader {
    color: #888;
    font-size: 1.3em;
    margin-bottom: 8px;
    text-align: center;
}

.section-header .picker-header-stats {
    font-size: 1.3rem;
    font-weight: normal;
    margin-top: 10px;
}

/* Section-purpose tagline: the uppercase one-liner at the very top of each /Design
   section header, above the nav / title row. Sized below the section title,
   tracked out a touch for emphasis; the colour is the band's, inherited. */
.section-header .section-header-tagline {
    font-size: 1.2rem;
    font-weight: normal;
    letter-spacing: 1px;
    margin-bottom: 9px;
    text-transform: uppercase;
}

/* Lite-only upsell link in the module- and goal-picker stats lines ("Switch to Standard or Expert
   for N more"). Subtle dotted underline that turns solid on hover, with a pointer cursor, per the
   hint-link affordance. */
.picker-upsell-link {
    color: inherit;
    text-decoration: underline dotted;
    text-underline-offset: 2px;
    cursor: pointer;
}

/* Solid on hover and nothing else: the colour is inherited from the stats line and stays put, like every
   other member of the dotted-underline family. */
.picker-upsell-link:hover {
    text-decoration: underline solid;
}

.picker-grid {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px;
    align-items: flex-start;
}

.picker-add {
    font-size: 1.2em;
    margin-top: 25px;
}

.picker-add-tabs {
    display: flex;
    /* Wrap to multiple rows on narrow screens so the category tabs stay fluid rather than
       overflowing or squishing. On wide screens they still fit in one row (no-op). */
    flex-wrap: wrap;
    gap: 2px;
    border-bottom: 1px solid #ccc;
    margin-bottom: 8px;
}

.picker-add-tab {
    background: #f0f0f0;
    border: 1px solid transparent;
    border-radius: 4px 4px 0 0;
    padding: 6px 14px;
    cursor: pointer;
    color: #555;
    font: inherit;
    font-size: 1.2em;
    display: inline-flex;
    align-items: center;
    gap: 6px;
}

.picker-add-tab-active {
    background: #fff;
    border: 1px solid #ccc;
    border-bottom: 1px solid #fff;
    margin-bottom: -1px;
    font-weight: bold;
    color: #000;
}

.picker-add-tab-count {
    margin-left: 6px;
    color: #888;
    font-weight: normal;
    font-size: 0.9em;
}

/* A catalogue tab (Goals / What-Ifs) whose every tile is unavailable -- all already added, or none
   currently applies. The tab stays clickable so the user can still open it and read why each tile is
   unavailable, but its title and count grey to signal there is nothing to add inside. The bare selector
   already wins over .picker-add-tab-active's #000 by later source order (equal single-class specificity);
   the compound selector pins the same result regardless of rule order, so an OPEN all-unavailable tab
   greys either way. Only the two catalogue panels emit this class, so the shared Assumptions / Cones tab
   strips are untouched. */
.picker-add-tab-unavailable,
.picker-add-tab-active.picker-add-tab-unavailable {
    color: #aaa;
}

.picker-add-tab-unavailable .picker-add-tab-count {
    color: #c4c4c4;
}

.picker-add-tiles {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px;
    padding: 8px 0;
}

.add-accent-button {
    position: relative;   /* anchors the .picker-count-badge corner overlay */
    min-width: 180px;
    max-width: 280px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    background: var(--color-surface-light);
    border: 1px solid #ccc;
    border-radius: 16px;
    padding: 12px 16px;
    margin: 4px;
    cursor: pointer;
    transition: background-color 0.15s ease, box-shadow 0.15s ease;
    font: inherit;
    color: inherit;
    text-align: center;
}

/* The shared clickable-card hover (fill + ring, see --hover-card-ring at the top of
   this file), so the goal / What-If catalogue reads as one system with the module
   picker beside it and the sweep-suggestion cards. */
.add-accent-button:not(.add-accent-button-disabled):hover {
    background-color: var(--color-pill-action-light);
    box-shadow: var(--hover-card-ring);
}

/* Catalogue-tile title: a centred column holding the [icon + kind label] row and the name below it. */
.add-accent-title {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    font-weight: bold;
    font-size: 1.1em;
    margin-bottom: 8px;
    color: #888;
    transition: color 0.15s ease;
}

/* The [tile icon + gray kind label] row at the top of a catalogue tile, centred together above the name. */
.add-accent-kind-row {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 10px;
}

/* The gray "GOAL" / "WHAT-IF" kind label stacked above a catalogue tile's name (right of the tile icon).
   Inherits the tile title's #888 to #000 hover from .add-accent-title; smaller + uppercase so it reads as a
   type marker over the name. */
.add-accent-kind {
    font-size: 0.7em;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    line-height: 1;
}

/* A catalogue tile's name, centred on its own line below the [icon + kind label] row; line-height matches
   the card name so a wrapped tile name gets the same breathing room between its two lines. */
.add-accent-name {
    text-align: center;
    line-height: 1.3;
}

/* Enlarge the tile NAME only on tiles that carry a subtitle (the What-If catalogue tiles), so Goal tiles,
   which share this helper but have no subtitle, keep the default name size. */
.add-accent-title:has(.add-accent-subtitle) .add-accent-name {
    font-size: 1.2em;
}

/* The catalogue tile's static subtitle (the gist), centred and smaller than the tile name, directly below it. */
.add-accent-subtitle {
    display: block;
    text-align: center;
    font-weight: normal;
    font-size: 1em;
    line-height: 1.25;
    color: #999;  /* lighter than the #888 tile title so the subtitle never reads darker than it at rest */
    padding-top: 2px;
}

/* Wrap the subtitle in round parentheses decoratively, so the Subtitle data stays clean (no parens in the
   string) and both surfaces get them uniformly. */
.accent-subtitle::before,
.add-accent-subtitle::before {
    content: "(";
}

.accent-subtitle::after,
.add-accent-subtitle::after {
    content: ")";
}

.add-accent-description {
    font-size: 1em;
    color: #AAA;
    line-height: 1.3;
    transition: color 0.15s ease;
}

.add-accent-button:not(.add-accent-button-disabled):hover .add-accent-title,
.add-accent-button:not(.add-accent-button-disabled):hover .add-accent-subtitle,
.add-accent-button:not(.add-accent-button-disabled):hover .add-accent-description {
    color: #000;
}

.add-accent-button:not(.add-accent-button-disabled):hover .add-accent-icon {
    opacity: 1;
}

/* A disabled accent tile fades its CONTENT (title/icon/name/subtitle, description, status), NOT the whole
   tile, so the corner .picker-count-badge stays legible on an at-capacity tile, exactly as the module tile
   does (.add-module-button-disabled). The border + surface stay crisp. Content fades to the same shared
   --picker-tile-disabled-opacity the tile used to fade wholesale to, so the content reads the same; only
   the badge, border and background are now excluded from the fade. */
.add-accent-button-disabled {
    border-style: dotted;
    border-width: var(--picker-tile-disabled-border-width);
    cursor: default;
}

.add-accent-button-disabled .add-accent-title,
.add-accent-button-disabled .add-accent-description,
.add-accent-button-disabled .add-accent-status {
    opacity: var(--picker-tile-disabled-opacity);
}

/* Gray status line under a catalogue tile (muted text + a divider above): the goal "Already added." /
   "Unavailable: ..." line and the What-If "Unavailable: ..." line. Emitted by RenderTileStatus. */
.add-accent-status {
    color: #888;
    margin-top: 10px;
    border-top: 1px solid #ddd;
    padding-top: 8px;
}

.accent-verdict-badge {
    width: 2em;
    height: 2em;
    margin-right: 10px;
    flex-shrink: 0;
    text-align: center;
    line-height: 2em;
    font-size: 1.5em;
    font-weight: bold;
}

img.accent-verdict-badge {
    font-size: 1em;
}

/* Section icon (images/sections/Goal.svg / WhatIf.svg) to the left of a Goal / What-If card title,
   so the two accent families are distinguishable at a glance. Sized to sit beside the 1.4em title. */
.accent-section-icon {
    width: 2.6em;
    height: 2.6em;
    flex-shrink: 0;
}

/* Mute the section icon when the card is disabled (Goal disabled / What-If inactive), to match the
   #BBB greyed title + eyebrow. An <img> ignores `color`, so it is greyed via opacity on the light card
   (~0.3 of black on the light surface lands close to the #BBB the disabled text uses). */
.module.disabled .accent-section-icon {
    opacity: 0.3;
}

/* The same section icon on a catalogue tile (a yet-to-pick Goal / What-If), left of the tile name.
   Muted by default and full-black on hover, matching the tile title/description text (which goes
   #888 / #AAA -> #000 on hover); the black SVG mimics that with opacity 0.5 -> 1. */
.add-accent-icon {
    width: 1.4em;
    height: 1.4em;
    flex-shrink: 0;
    opacity: 0.5;
    transition: opacity 0.15s ease;
}

/* Block C on a Goal / What-If accent card — the info icon and its hover popup explaining the entry (shared by
   GoalComponent + WhatIfComponent). The anchor is the position:relative host for the absolutely-positioned
   popup, and carries cursor:help. The black SVG icon renders at full opacity with no hover fade or transition,
   so it reads as a solid, always-legible help glyph. */
.accent-tooltip-anchor {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: help;
}

.accent-tooltip-icon {
    width: 4em;
    height: 4em;
    flex-shrink: 0;
}

/* The hover popup — a real DOM element (not the CSS content: attr() tooltip, which renders plain text only),
   so a Goal/What-If Tooltip can carry inline <b>/<i> emphasis. Mirrors the app-wide §TOOLTIP look (off-white card,
   #BBB border, --tooltip-font-size, pre-line paragraph breaks, high z-index) and its show-after-0.4s /
   hide-instantly hover behaviour. Anchored just below the icon and growing rightward into the card; the card /
   panel carry no overflow:hidden, so it is not clipped. Base weight is normal so only the <b> runs read bold. */
.accent-tooltip-popup {
    position: absolute;
    top: calc(100% + 8px);
    left: -10px;
    min-width: 300px;
    max-width: 500px;
    border: 1px #BBBBBB solid;
    border-radius: 10px;
    background-color: #f9f9f9;
    padding: 10px;
    color: #444444;
    font-size: var(--tooltip-font-size);
    line-height: var(--tooltip-line-height);
    font-weight: 400;
    text-align: left;
    white-space: pre-line;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    z-index: 1000000;
    transition: opacity 0.15s ease-in, visibility 0s linear 0.15s;
}

/* No CSS :hover reveal: .accent-tooltip-popup is a `popover="manual"` top-layer element (.tooltip-floating),
   shown / positioned by the shared tooltip positioner in general.js after the sustained-hover delay. */

/* Grey the glyph when the card is disabled (What-If inactive / Goal disabled), so it fades with the rest of
   the card exactly like the section icon (.module.disabled .accent-section-icon). An <img> ignores `color`, so
   opacity is the lever (~0.3 of black on the light card lands near the #BBB the disabled text uses). The
   hover tooltip still works on a disabled card; only the glyph dims. */
.module.disabled .accent-tooltip-icon {
    opacity: 0.3;
}

/* Accent panel (Goals + What-Ifs) — module-like box but narrower with hover-only blue when open. */
.module.accent-style {
    flex: 0 1 500px;
    max-width: 500px;
    min-width: 320px;
}

.module.accent-style.module-edit {
    background-color: var(--color-surface-light);
    box-shadow: none;
}

.module.accent-style.module-edit:hover {
    background-color: var(--color-pill-action-light);
    box-shadow: 0 0 0 0.25rem var(--color-pill-action-shadow);
    transition: box-shadow 0.2s;
}

/* Accent cards (Goals + What-Ifs) need no title-underline rule of their own: their has-items
   card uses the same .module-body wrapper as modules, so the shared `.module-body:hover
   .module-name` rule above already underlines the title (text-decoration, hugging the glyphs on
   each wrapped line). This is where the old accent-specific border-bottom + 2px reservation lived;
   text-decoration causes no layout shift, so the reservation is gone. */

/* Full-width summary line below the title row + side controls — hosts both the
 * Goal/WhatIf description and the WhatIf "Targets:" line so they share a font. */
.accent-summary {
    text-align: center;
    font-size: 1.2em;
    color: #000;
    margin-top: 10px;
    margin-bottom: 10px;
    padding: 0 12px;
}

.accent-summary.disabled {
    color: #CCC;
}

/* Orphan-state variant of accent-summary: warning color for "no targets" / "deleted pin" / etc. */
.accent-summary.orphan {
    color: #cc7a00;
    font-weight: 500;
}

/* Key numbers (money amounts, ages, percentages) in the goal description stand out. Tahoma ships only
   Regular (400) and Bold (700), no semibold, so 700 is the only weight that renders distinctly here. */
.accent-summary .goal-key-number {
    font-weight: 700;
}

.accent-message {
    text-align: center;
    font-size: 1.2em;
    color: #000;
    margin-top: 10px;
    margin-bottom: 10px;
    padding: 10px 12px 0;
    border-top: 1px solid #ddd;
}

/* Leading-icon variant of .accent-message, used by both accent families: the Goal outcome line (its
   verdict badge) and the What-If "Affecting ..." line (its activity bolt). The flex is scoped to this
   modifier rather than .accent-message itself so a leading-icon-less message keeps the plain layout. */
.accent-message.has-leading-icon {
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Inactive What-If: grey the whole "Affecting ..." line (the leading bolt is dropped when inactive,
   and the body inherits this), matching the greyed header + description. */
.accent-message.disabled {
    color: #CCC;
}

/* And grey + inert the line's module nav link, so an inactive What-If's "Affecting 'X'"
   reads as dormant and is not clickable. */
.accent-message.disabled a.plan-nav-link {
    color: #CCC;
    pointer-events: none;
}

.accent-items {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 8px 12px;
}

/* --------------------------------------------------------*/
/* CONTINGENCIES */
/* --------------------------------------------------------*/

.contingencies {
    display: flex;
    justify-content: center;
    font-size: 1.2em;
    padding: 6px;
    margin-bottom: 5px;
}

.contingency {
    margin: 4px;
    text-align: center;
    min-height: 110px;
    border: 1px solid #666666;
    background-color: white;
    border-radius: 0.5em;
    padding: 7px;
    /* min(300px, 100%): 300px on desktop, but shrink to the container on phones so the
       ExtraInfo boxes (warning / info / note / suggestion / event / action) that use this
       base rule don't overflow a narrow viewport. .contingency.tip-pill (the Insight /
       Curiosity tips) overrides width with fit-content and keeps its own 400px min-width,
       so its sizing is unaffected by this base width. */
    width: min(300px, 100%);
}

.contingency-date {
    padding-top: 6px;
    padding-bottom: 4px;
    font-size: 1.1em;
}

.contingency-time-info {
    padding-bottom: 9px;
    border-bottom: black solid 1px;
}

.contingency-header {
    padding: 7px;
    font-size: 1.05em;
    font-weight: bold;
    border-bottom: black solid 3px;
    border-top: black solid 3px;
    letter-spacing: 1px;
}

.contingency-header.information {
    color: var(--color-black);
    border-color: var(--color-black);
    background-color: var(--color-pill-information-light);
}

.contingency-header.warning {
    color: var(--color-red);
    border-color: var(--color-red);
    background-color: var(--color-pill-error-light);
}

.contingency-header.suggestion {
    color: var(--color-green-dark);
    background-color: var(--color-green-pale);
    border-color: var(--color-green-dark);
}

.contingency-header.note {
    color: var(--color-pill-note-text);
    background-color: var(--color-pill-note-light);
    border-color: var(--color-pill-note-border);
}

.contingency-header.event {
    color: var(--color-pill-event-dark);
    border-color: var(--color-pill-event-dark);
    background-color: var(--color-pill-event-light);
}

.contingency-header.action {
    color: var(--color-blue);
    border-color: var(--color-blue);
    background-color: var(--color-pill-action-light);
}

.contingency-icon {
    height: 26px;
    margin-right: 7px;
}

/* The SUBJECT glyph: which wedding / sculpture this card is about, as opposed to  */
/* .contingency-icon above, which says what kind of contingency it is and is the   */
/* same on every card. Deliberately the .module-block-icon shape (30px, centred     */
/* block) rather than .contingency-icon's inline 26px, though the margin is the     */
/* opposite one: that rule clears BELOW its glyph, this one clears ABOVE, because   */
/* here the glyph follows a rule rather than opening a header. The same             */
/* picture already renders that way on the menu tile and the row's type badge, so   */
/* matching it keeps one glyph reading as one thing wherever the user meets it.     */
.contingency-subject-icon {
    display: block;
    width: 30px;
    height: 30px;
    margin: 8px auto 0;
}

.contingency-name {
    margin-top: 4px;
    margin-bottom: 6px;
    padding: 4px;
    font-size: 1.2em;
    font-weight: bold;
}

.contingency-description {
    padding: 4px;
    font-size: 1em;
}

.contingency-information {
    font-size: 1em;
    padding: 4px;
    color: #333333;
    line-height: 1.35em;
}

/* "Triggered by: '<predicate>'" line on an event card whose firing was driven by  */
/* a Dynamic TimePoint ("When ..."). Sits directly under the event name; black and  */
/* the same size as the description text, reading as a normal provenance line.      */
.contingency-trigger {
    font-size: 1em;
    padding: 4px;
    color: #000000;
}

/* --------------------------------------------------------*/
/* TIP PILLS (bottom of module body)                       */
/* Piggyback on .contingency* classes used by ExtraInfo so  */
/* the pill matches the Information / Warning / Suggestion   */
/* boxes visually; full width, no min-height, hover-only     */
/* underline on Title.                                       */
/* --------------------------------------------------------*/

.contingency-header.insight {
    color: var(--color-pill-insight-dark);
    border-color: var(--color-pill-insight-dark);
    background-color: var(--color-pill-insight-light);
}

.contingency-header.curiosity {
    color: var(--color-pill-curiosity-dark);
    border-color: var(--color-pill-curiosity-dark);
    background-color: var(--color-pill-curiosity-light);
}

.contingency.tip-pill {
    cursor: pointer;
    flex: 0 1 auto;
    width: fit-content;
    /* min(400px, 100%): keep the 400px floor on desktop, but let the box shrink to
       the container on phones, where a hard 400px floor overflowed the viewport. */
    min-width: min(400px, 100%);
    max-width: 800px;
    min-height: 0;
    margin: 30px auto 0;
}

.tip-pill:hover .tip-pill-title {
    text-decoration: underline;
}

.tip-more-indicator {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 6px;
    padding: 15px 0 15px;
}

.tip-more-indicator > span {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background-color: #9ca3af;
}

.tip-pill .contingency-information {
    text-align: left;
}

.tip-pill .contingency-information ul {
    margin-top: 1em;
    text-align: left;
}

/* --------------------------------------------------------*/
/* RESULTS */
/* --------------------------------------------------------*/

/* To avoid Google charts flickering when hovering the tooltips */
svg > g > g:last-child {
    pointer-events: none
}

.div-graph-mini { margin-bottom: 3px; }

.div-graph { margin-top: 25px; }

/* The L2 tab bar (The economy | Tax and benefits) at the top of the Assumptions section. Reuses the shared
   .picker-add-tab* catalogue-tab styling (as in the Goals / What-Ifs panels); this adds space above it and
   the 1.2em that the Goals / What-Ifs tabs inherit from their enclosing .picker-add wrapper (which this bar
   has no equivalent of), so the .picker-add-tab 1.2em compounds to the same 1.44em and the tab font matches
   theirs exactly. The .picker-add-tabs flex handles the left-aligned strip + underline. */
.assumptions-tab-bar {
    margin-top: 16px;
    font-size: 1.2em;
}

/* The "All rates | Used only" pill toggle sits centred above the tab bar (it is page-wide: it drives both
   the economy tab's rate table and the Tax-and-benefits tab's legislative sections), reusing the shared
   .user-mode-toggle / .user-mode-pill styling. */
.assumptions-rates-toggle {
    display: flex;
    justify-content: center;
    margin-top: 20px;
}

/* Keep the Assumptions rate charts in a readable, centred column instead of spanning the full 1600px
   content width. Google Charts draws to the div's offsetWidth at draw time, so capping the container
   caps the chart and also defeats the "drew full-width before layout settled" flicker. */
.assumptions-charts .div-graph,
.assumptions-charts .div-graph-title,
.assumptions-charts .div-graph-description {
    max-width: 900px;
    margin-left: auto;
    margin-right: auto;
}

/* Paired-chart row: a pie card on the left + its stacked-area sibling on the
   right, so each pie sits next to the chart it summarises. flex-wrap lets
   the pair collapse to two stacked rows on narrow viewports. No column gap
   — the chart bodies already carry their own padding. Row gap (24px) only
   kicks in when the row wraps. */
.chart-pair-row {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: flex-start;
    row-gap: 24px;
    column-gap: 0;
    margin-top: 25px;
}

.chart-pie-card {
    flex: 0 0 480px;
    display: flex;
    flex-direction: column;
}

/* Fills the remaining row width once the pie has its 480px. flex-basis: 0
   so flex-grow distributes the post-pie remainder rather than holding 480px
   first; min-width keeps the axis labels readable when the row wraps. */
.chart-stacked-card {
    flex: 1 1 0;
    min-width: 480px;
    display: flex;
    flex-direction: column;
}

/* The Couple tab's contribution chart and the two sentences that caption it. Full-width rather than
   the paired chart-plus-table row the Estate section uses, because its table sits BELOW it and reads as
   the same block: the chart shows the shape, the caption states the lifetime figure, the table gives the
   years. Bounded so the chart does not stretch to an unreadable aspect on a wide monitor. */
.couple-chart-card {
    display: flex;
    flex-direction: column;
    max-width: 960px;
    margin: 0 auto 12px auto;
}

.couple-chart-card > .div-graph {
    margin-top: 0;
}

.chart-pie-card > .div-graph,
.chart-stacked-card > .div-graph {
    margin-top: 0;
}

/* 2x2 grid for the lifetime-style pies on the Tax Year page. Wide viewport
   shows 2 columns × 2 rows; narrow viewport collapses to a single column
   (1×4 stack). Matches the .chart-pie-card width so each grid cell holds one
   pie. The break point (~1024px) is roughly where two 480px cards plus the
   24px gap fit with breathing room. */
.chart-pies-grid-2x2 {
    display: grid;
    grid-template-columns: minmax(0, 480px);
    justify-content: center;
    gap: 24px;
    margin-top: 25px;
}

@media (min-width: 1024px) {
    .chart-pies-grid-2x2 {
        grid-template-columns: repeat(2, minmax(0, 480px));
    }

    /* A lone pie (e.g. an embedded-only fiscal year, where only the Embedded
       composition pie renders) would otherwise sit in the left column of the
       centred 2-column track group and read as left-of-centre. Collapse to a
       single centred column so the sole pie is centred like on narrow viewports. */
    .chart-pies-grid-2x2:has(> .chart-pie-card:only-child) {
        grid-template-columns: minmax(0, 480px);
    }
}

/* Estate section: each composition pie sits beside the table it summarises —
   the liquid/illiquid split row first, then the asset-type row, matching the
   table order. flex-wrap stacks the pie above its table on a narrow viewport
   ("fluidifies"); the breakdown table further down stays full width, no pie.
   align-items:center vertically centres the pie card against its table (a short
   pie sits mid-height beside a taller table); the top/bottom margins give each
   paired row breathing room from the header above and the next table below. */
.estate-chart-table-row {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    row-gap: 12px;
    column-gap: 24px;
    margin-top: 30px;
    margin-bottom: 30px;
}

/* The table column beside the 480px pie: flex-basis 480px + grow fills the rest
   of the row; min-width forces the pair to wrap (pie on top, table below) once
   a 480px pie and a 480px table no longer fit, the same ~1000px breakpoint the
   tax-page paired row uses. */
.estate-chart-table-row > .estate-table-col {
    flex: 1 1 480px;
    min-width: 480px;
}

/* Zero the in-row table's own margins so its height is just the table and it
   centres cleanly against the pie; the paired-row spacing is the row's own
   top/bottom margins. The breakdown table outside the rows keeps its margins. */
.estate-chart-table-row .table-responsive {
    margin-bottom: 0;
}

.estate-chart-table-row .details-table {
    margin-top: 0;
}

.div-graph-title {
    /* A gap below the chart so the title is not flush against the plot. Charts fill their div by
       different amounts (the module-chart path draws its x-axis near the bottom edge; the full-summary
       charts leave internal whitespace), so the separation lives here, at the layout level, uniform
       across every chart, rather than depending on each chart type's own chartArea padding. */
    margin-top: 10px;
    text-align: center;
    font-size: 1.4em;
    font-weight: bold;
}

.div-graph-description {
    /* Constrained to the PLOT, not to the card. Every chart that carries a description draws its
       chartArea at 70% of the canvas (charting.js), roughly centred by Google's own axis margins, so an
       unconstrained description runs visibly wider than the chart it describes and reads as belonging to
       something else. The auto side margins centre the measure under the plot; keep them if this max-width
       is ever retuned, or the text pins to the left edge instead. */
    max-width: 70%;
    margin: 10px auto 20px;
    text-align: center;
    color: var(--color-text-secondary);
    font-size: 1.2em;
}

table.details-table {
    margin-left: auto;
    margin-right: auto;
}

.details-table {
    align-self: center;
    line-height: 1.5;
    margin-top: 20px;
    padding: 0;
    border-collapse: collapse;
    border-style: solid;
    border: lightgray;
}

.details-table td { padding: 0 8px; }

.details-table td.separator {
    border-right-style: solid;
    border-right-width: 1px;
    border-right-color: #DDDDDD;
}

.right { text-align: right; }

.center { text-align: center; }

.right-no-wrap {
    text-align: right;
    white-space: nowrap;
}

.details-table td.bold { font-weight: bold; }

.details-table th.bold { font-weight: bold; }

.details-table td.larger { font-size: 1.2em; }

.details-table th {
    font-size: 1.3em;
    padding: 8px;
    background-color: #000000;
    color: white;
    text-align: center;
    vertical-align: middle;
    font-weight: bold;
    border-color: #DDDDDD;
    border-width: 1px;
    border-style: solid;
}

.navigation-arrow-div {
    margin-left: 5px;
    margin-right: 5px;
    display: initial;
}

/* The arrow glyph is an SVG used as a CSS mask, so background-color tints it:
   solid black at rest, the accent blue on hover (matching the toggle controls). */
.navigation-arrow-img {
    display: inline-block;
    width: 1em;
    height: 1em;
    vertical-align: middle;
    background-color: black;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-position: center;
    mask-position: center;
    -webkit-mask-size: contain;
    mask-size: contain;
}

.navigation-arrow-left {
    -webkit-mask-image: url(../images/arrows/LeftArrow.svg);
    mask-image: url(../images/arrows/LeftArrow.svg);
}

.navigation-arrow-right {
    -webkit-mask-image: url(../images/arrows/RightArrow.svg);
    mask-image: url(../images/arrows/RightArrow.svg);
}

.navigation-arrow-div:hover {
    cursor: pointer;
}

.navigation-arrow-div:hover .navigation-arrow-img {
    background-color: var(--color-blue-accent);
}

.details-table .red { color: var(--color-red); }

.details-table .blue { color: var(--color-blue); }

/* Movements table: an unsliced transfer is internal to the household and net-zero,
   so its amount renders muted grey to read as informational, not part of the total. */
.details-table .grey { color: #999999; }

.details-table .green { color: black; }

.details-table tr { border: 1px solid #ddd; }

.details-table tr.odd { background-color: var(--color-row-stripe-even); }

.details-table tr.decade {
    background-color: white;
    font-size: 1.1em;
}

td.key {
    text-align: left;
    font-weight: bold;
    font-size: 1.2em;
}

/* Assumptions table: a derived rate's name sits beneath its base-driver parent
   (a td.key), indented and un-bold so the parent / child hierarchy reads at a glance.
   Prefixed with .assumptions-rate-table so specificity (0,2,1) beats the later
   `.fluid-table td { padding: 2px 10px }` rule (0,2,1 > 0,1,1) whose padding
   shorthand would otherwise reset padding-left and swallow the indent. */
.assumptions-rate-table td.sub-key {
    text-align: left;
    padding-left: 44px;
}

/* Assumptions table: a module's own custom rate sits one level deeper again,
   beneath the official product rate it overrides. Same specificity bump. */
.assumptions-rate-table td.sub-sub-key {
    text-align: left;
    padding-left: 64px;
}

/* Assumptions table: a rate no enabled module reads at its official value is
   dimmed to read as inactive. Scoped to td (not a) so the row's Settings links
   keep their live colour and stay clickable. Specificity (0,3,1) beats
   .details-table td and tr.row-group-header td. */
.details-table tr.not-in-use td {
    color: #AAAAAA;
}

/* Whole-section greying for a legislative section the plan never read, shown (greyed) only in the "All"
   mode of the Tax-and-benefits tab. Its title carries a "(not used by this plan)" marker; the links inside
   stay live and clickable (like the greyed economy-table rows), only the visual weight drops. "Used only"
   hides such a section entirely, so this rule is reached only in "All" mode. */
.assumptions-legislative-table.not-in-use {
    opacity: 0.55;
}

/* Assumptions table only: fence each of the four base-driver sections with a
   black top border on its group-header row. Scoped to .assumptions-rate-table so
   the shared tr.row-group-header (Tax-Year attribution table) is untouched. */
.assumptions-rate-table tr.row-group-header {
    border-top: 2px solid #000;
}

span.decimal {
    font-size: 0.7em;
    font-weight: normal;
}

tr.row-total {
    font-weight: bold;
    font-size: 1.15em;
    border-top-color: black;
    border-top-width: 2px;
}

/* Intermediate-totals row inside a grouped table (e.g. the per-Kind subtotal
   inside the Tax-Year attribution table). Bold like the grand total but no
   bigger font and no thick top border — keeps the visual hierarchy clear:
   regular row < subtotal row < grand total row. */
tr.row-subtotal {
    font-weight: bold;
    border-top: 2px solid #ddd;
}

/* Group header inside a grouped table — same font-size as the grand totals
   row at the bottom (1.15em bold). Used on the per-Kind label row above each
   group of attribution rows. Pairs with row-subtotal at the bottom of the
   group and row-total at the very bottom of the table. The light grey
   background visually fences each group's content. */
tr.row-group-header {
    font-weight: bold;
    font-size: 1.15em;
    background-color: #ededed;
}

/* Consistent vertical spacing between Results-tab tables (Tax Summary stacks
   two; SummaryAll / Tax Year / etc. each render their own block). 30px gives
   visual breathing room between the table block and whatever comes above /
   below (charts, headings, other tables). */
.table-responsive {
    margin-bottom: 30px;
}

/* Clickable rows on the Results drill-down tables (Summary > Year > Month) wrap each
   row in its own <tbody> (one tbody per OpenRow call); the hover styling has to attach
   there so the whole row reads as the hover target, not just one cell. Pointer cursor
   already lived here; the black outline makes the hover-to-drill-down affordance explicit. */
.table-hover tbody:hover {
    cursor: pointer;
    outline: 1px solid black;
    outline-offset: -1px;
}

.hovering-effects:hover {
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 4px;
}

.details-header {
    font-size: 1.2em;
    border-radius: 8px;
    margin-top: 15px;
    margin-bottom: 5px;
    text-align: center;
    border-style: solid;
    padding: 7px;
    border-color: #AAAAAA;
    border-width: 1px;
    background-color: #EFEFEF;
}

.details-type {
    font-weight: bold;
    font-size: 1.5em;
}

.details-title { font-size: 1.2em; }

/* The module-slicing dropdown button (a Bootstrap .btn-link) is permanently
   underlined by default. Strip the base underline so it reads as a normal
   label; keep the hover underline so the user gets the standard interactive
   feedback. The chevron already signals "this is a control" at rest. */
.details-title .btn-link { text-decoration: none; }
/* Bootstrap's .text-dark / .text-primary utilities set colour with !important,
   so the hover colour must also be !important to win. */
.details-title .btn-link:hover {
    text-decoration: underline;
    text-underline-offset: 4px;
    color: var(--color-blue-accent) !important;
}

/* The Show / Hide toggle on each expandable Results section (a Bootstrap .btn-link,
   underlined by default). Underline only on hover, sat well below the larger text. */
.expandable-section-toggle .btn-link { text-decoration: none; }
.expandable-section-toggle .btn-link:hover {
    text-decoration: underline;
    text-underline-offset: 6px;
}

.details-title-separator {
    color: lightgray;
    padding: 0px 5px;
}

.details-breadcrumbs {
    padding: 5px;
    margin-top: 7px;
    font-weight: normal;
    font-size: 1.4em;
    border-top: #ccc solid 1px;
}

.breadcrumbs:hover { color: #444444; }

span.separator {
    color: lightgray;
    margin-left: 10px;
    margin-right: 10px;
}

.report-image {
    margin: 3px;
    height: 30px;
    margin-right: 10px;
    margin-left: 15px;
    vertical-align: middle;
}

.module-image {
    width: 100%;
    max-width: 55px;
    opacity: 0.6;
    vertical-align: middle;
    margin-top: 10px;
    -webkit-transform: rotate(14deg);
    -moz-transform: rotate(14deg);
    -ms-transform: rotate(14deg);
    -o-transform: rotate(14deg);
    transform: rotate(14deg);
}

.module-edit .module-image { opacity: 1; }

.module-view:hover .module-image { opacity: 1; }

.module-view:hover .disabled.module-image { opacity: 0.2; }

.module-image.disabled { opacity: 0.2; }

.flow-snapshot {
    font-size: 1.3em;
    margin-top: 25px;
    margin-bottom: 25px;
}

.flow-snapshot-table {
    margin-left: auto;
    margin-right: auto;
}

.flow-snapshot-row {
    /* still empty */
}

.flow-snapshot-name {
    font-weight: bold;
    text-align: right;
    padding-right: 20px;
}

.flow-snapshot-values {
    display: flex;
    flex-wrap: nowrap;
    align-content: space-around;
    justify-content: flex-start;
    align-items: stretch;
    flex-direction: row;
}

.flow-snapshot-value {
    border-width: 1px;
    border-color: black;
    border-style: solid;
    padding: 5px 10px;
    margin: 5px;
    border-radius: 4px;
    color: white;
}

.flow-snapshot-yield {
    background-color: var(--color-blue);
}

.flow-snapshot-consume {
    background-color: #cc0000;
}

.flow-snapshot-give {
    background-color: #333333;
 }

.flow-snapshot-receive {
    background-color: #333333;
}

/* --------------------------------------------------------*/
/* TRIGGER */
/* --------------------------------------------------------*/

.action-description {
    padding: 10px;
    font-size: 1.3em;
    border: solid 1px black;
    margin: 0 0 10px 0;
    border-radius: 0.5em;
    background-color: white;
}

.trigger-explanation {
    padding: 10px;
    font-size: 1.3em;
    text-align: center;
}

.trigger-contingency-description {
    margin-bottom: 10px;
    margin-top: 10px;
    font-size: 1.3em;
    text-align: center;
}

.trigger-text {
    text-align: center;
    color: white;
    border-width: 1px;
    border-radius: 0.4em;
    font-size: 1.3em;
    background-color: black;
    font-weight: bold;
    margin: 20px 0 20px 0;
    padding: 5px 0 5px 0;
}

.trigger-text-as-icon {
    font-weight: 800;
    font-family: monospace;
    font-size: 1.3em;
    letter-spacing: -2px;
}

.trigger-select-highlight {
    color: var(--color-blue);
    font-weight: 700;
    font-size: 1.1em;
}

/* --------------------------------------------------------*/
/* TAX */
/* --------------------------------------------------------*/

.tax-details-module {
    display: flex;
    justify-content: center;
}

.tax-details-table {
    line-height: 1.5;
    padding: 0;
    border-collapse: collapse;
    width: 100%;
}

.tax-details-table td {
    padding: 1px 2px;
    vertical-align: top;
}

td.tax-key {
    text-align: right;
    font-weight: bold;
    font-size: 1.3em;
}

td.tax-sub-key {
    text-align: right;
    padding-left: 10px;
}

/* Sub-total row inside a narrative sub-block — applied to a parent-row that's
   rendered AFTER its children. Carries a top border (the accountant's "these
   numbers above sum to me" rule) and a slightly larger font than tax-key so
   the row stands out as the aggregate. */
td.tax-key-subtotal {
    text-align: right;
    font-weight: bold;
    font-size: 1.45em;
    border-top: 1px solid #cccccc;
    padding-top: 4px;
}

tr.tax-flow-row {
    font-size: 0.9em;
    color: #888888;
}

.tax-details {
    /* Column geometry shared with .tax-details-layout's grid below, so the
       frame's card-count cap and the grid's track sizing stay in lockstep from
       one source.

       500px, not the original 360px: every calculation row is a label opposite
       an amount, and the label column now also carries the (i) icon, so at
       360px the longer labels ("Gain above the lifetime limit, taxed at
       standard rates") wrapped to two and three lines and the cards read as
       cramped. Widening the track trades a column on a mid-width screen for
       rows that fit on one line. */
    --tax-col-min: 500px;
    --tax-col-gap: 15px;
    --tax-frame-pad: 10px;

    /* width:100% (not fit-content) keeps the width DEFINITE so the auto-fill
       grid inside sees a real container width and lays out multiple columns —
       fit-content would collapse it to one --tax-col-min track. max-width then caps the
       frame to exactly --tax-card-count columns, so a partly-filled grid does
       not leave a wide empty band to the right: the frame hugs its cards and
       .tax-details-module (flex, justify-content:center) centres it. On a narrow
       viewport the cap exceeds the available width, so width:100% wins and the
       grid wraps responsively. --tax-card-count is set inline per render; the 99
       fallback means "no cap" for any path that leaves it unset. box-sizing is
       border-box (bootstrap reboot), so the padding + border terms land inside
       max-width and the grid's content box resolves to exactly N tracks. */
    width: 100%;
    max-width: calc(
        var(--tax-card-count, 99) * var(--tax-col-min)
        + (var(--tax-card-count, 99) - 1) * var(--tax-col-gap)
        + 2 * var(--tax-frame-pad) + 2px /* border: 1px each side */);
    margin-top: 15px;
    padding: var(--tax-frame-pad);
    border-style: solid;
    border-width: 1px;
    border-color: #999999;
    border-radius: 6px;
    background-color: #f1f1f1;
}

.tax-details-title {
    padding: 10px;
    text-align: center;
    color: white;
    background-color: #333333;
    font-weight: bold;
    font-size: 1.6em;
    border-radius: 6px;
}

/* Couple Mode: a household files one tax return per person, so the year renders as two frames,
   yours then your partner's. The banner names whose return it is, and .tax-details-partner tints
   the second frame so the two are separable at a glance while scrolling rather than only by
   reading. Neither rule is emitted on a one-person plan, so nothing there restyles. */
.tax-details-owner {
    padding: 6px 10px;
    margin-bottom: 6px;
    text-align: center;
    color: #333333;
    font-weight: bold;
    font-size: 1.1em;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.tax-details-partner {
    border-color: #6a5acd;
    background-color: #f0eefb;
}

.tax-details-partner .tax-details-owner {
    color: #4b3fa8;
}

.tax-details-partner .tax-details-title {
    background-color: #4b3fa8;
}

/* Tax-return layout — narrative stream (root-tier rows reading top-to-bottom)
   plus per-unit detail cards (Employment, Property, CurrentAccount …) all
   flowing as siblings in a single auto-fit grid. Each track is min --tax-col-min
   (clamped to the container, see below) / max-1fr, so the layout stacks to one
   column on narrow screens and adds columns as width allows — narrative + each
   section card all sized equally. The .tax-details-units wrapper uses
   `display: contents` so the section cards become grid items of the parent
   layout rather than children of an inner wrapper that would otherwise force
   them into a sub-grid. */
.tax-details-layout {
    display: grid;
    /* auto-fill, not auto-fit: an empty tax return has only the "General
       summary" card. auto-fit would collapse the unused tracks and stretch
       that lone card across the full width; auto-fill keeps the implicit
       tracks so a single card stays at one track's width. */
    /* min(--tax-col-min, 100%): the track keeps its floor on desktop but shrinks to the
       container on a phone. Without the clamp a bare minmax(500px, 1fr) track cannot fit a
       narrower viewport, so it overflows and the whole page scrolls sideways -- the failure
       Calculators.md records at ~360px, and a 500px floor is wider than every phone rather
       than borderline. Same idiom as the two min(X, 100%) sites above. */
    grid-template-columns: repeat(auto-fill, minmax(min(var(--tax-col-min, 500px), 100%), 1fr));
    gap: var(--tax-col-gap, 15px);
    margin-top: 10px;
    align-items: start;
}

.tax-details-narrative,
.tax-section-card {
    /* Card wrapper for one column in the auto-fill grid — narrative summary or
       per-unit section card. The visible rounded look is provided by the
       inner banner+body's own border-radius (top corners on .tax-section-title,
       bottom corners on .tax-section), not by clipping this wrapper — clipping
       leaves subpixel artifacts where the inner straight borders meet the
       clipped curve. */
    min-width: 0;
}

.tax-details-units {
    /* Lift section cards up into the parent grid so narrative + cards flow
       as equal-width siblings rather than narrative-vs-units-block. */
    display: contents;
}

/* Reset the legacy 10px top margin on the title when it sits inside a card —
   the grid `gap` handles vertical spacing between cards now. */
.tax-section-card > .tax-section-title,
.tax-details-narrative > .tax-section-title { margin-top: 0; }

.tax-section {
    padding: 10px;
    border-style: solid;
    border-width: 0 1px 1px 1px;
    border-color: #444444;
    border-bottom-left-radius: 6px;
    border-bottom-right-radius: 6px;
}

.tax-section-title {
    margin-top: 10px;
    padding: 5px;
    text-align: center;
    color: black;
    font-size: 1.4em;
    font-weight: bold;
    border-color: #444444;
    border-width: 1px;
    border-style: solid;
    border-top-left-radius: 6px;
    border-top-right-radius: 6px;
}

.tax-unit {
    margin-top: 10px;
}

.tax-unit-title {
    padding: 5px;
    text-align: center;
    color: black;
    font-weight: bold;
    border-color: #444444;
    border-width: 1px;
    border-style: solid;
    border-radius: 6px;
}

.tax-calculation { margin-top: 5px; }

/* Narrative sub-block partition — each composable group inside the General
   summary card (Taxable income / Income tax / National Insurance / Dividend
   tax / CGT / Total tax for this year) renders as a .tax-narrative-subblock
   with its own bold subtitle + horizontal rule. Lightweight by design — the
   outer .tax-section card already carries the box framing, so the sub-blocks
   only need to signal "this is a new group" without adding more borders or
   backgrounds. */
.tax-narrative-subblock {
    margin-top: 14px;
}

.tax-narrative-subblock:first-child {
    margin-top: 0;
}

.tax-narrative-subblock-title {
    font-weight: bold;
    font-size: 1.3em;
    background-color: #eeeeee;
    border-top: 1px solid #888888;
    border-bottom: 1px solid #888888;
    padding: 2px 4px 4px 4px;
    margin-bottom: 2px;
}

.tax-flow-group { margin-top: 10px; }

.tax-flow-group-title { font-weight: bold; }

.tax-flow-group-table { margin-top: 5px; }

/* The (i) on a tax-return HEADING: the FY-root header (1.6em, white on near-black), a section card
   banner (1.4em, on the section's own colour) and a narrative sub-block title (1.3em, on grey).

   Sized and centred exactly like the Assumptions table's icon (.label-tooltip-host) and the Cones band
   key, and for the same reason: img.help-icon is 1.2em, so inside a 1.6em banner it resolves to ~31px
   and competes with the title it annotates. Fixed at 14px instead, so the glyph stays a small
   annotation whatever those three title sizes are retuned to.

   No colour rule is needed on any of the three backgrounds, including the near-black root banner:
   div.help-icon paints its own near-white disc with a grey border, so the black glyph always sits on
   its own light ground rather than on the banner's.

   The host is inline-flex so the icon rides the text baseline block and the pair centres as one run
   inside the title's text-align: center. The icon's own div must be inline too, not the block it is by
   default, or it would break the line inside that host. */
.tax-title-tooltip-host {
    display: inline-flex;
    align-items: center;
}

.tax-title-tooltip-host div.help-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;            /* the icon keeps its size in the flex row rather than stretching */
    max-height: 22px;
}

.tax-title-tooltip-host img.help-icon {
    height: 14px;
}

/* --------------------------------------------------------*/
/* TOOLTIP */
/* --------------------------------------------------------*/

/* Position context for ::after tooltip placement — both divs and buttons
   that carry data-description need this. */
div[data-description],
button[data-description],
.inline-toggle-text[data-description],
.segmented-toggle-text[data-description],
.radio-vertical-badge[data-description] {
    position: relative;
}

/* Help cursor on divs only. Buttons keep their own cursor logic (pointer for
   clickable inactive pills, default for the active one) — see
   .user-mode-pill / .user-mode-pill-active. */
div[data-description]:hover {
    cursor: help;
}

/* Render the tooltip body for hover/focus on both div and button. Each
   selector is its own comma-separated entry — a previous shotgun edit
   collapsed `div[data-description], button[data-description]:hover::after`
   into one selector, which CSS reads as "div[data-description] (always!)
   OR button[data-description]:hover::after", making every module pill's
   tooltip render permanently.

   The base `::after` always carries the content but stays invisible
   (opacity:0 + visibility:hidden + pointer-events:none). The :hover / :focus
   variants flip it visible with a 400ms transition-delay — same idea as
   the native browser title-tooltip delay, so the tooltip doesn't pop on
   accidental hovers while the user is scanning across pills. */
div[data-description]::after,
button[data-description]::after,
.inline-toggle-text[data-description]::after,
.segmented-toggle-text[data-description]::after,
.radio-vertical-badge[data-description]::after {
    content: attr(data-description);
    position: absolute;
    top: 45px;
    left: -20px;
    min-width: 300px;
    max-width: 500px;
    border: 1px #BBBBBB solid;
    border-radius: 10px;
    background-color: #f9f9f9;
    padding: 10px;
    color: #444444;
    /* Absolute, NOT `1.2em`: see --tooltip-font-size. An em here inherits the trigger's
       scale, so every scaled pill / toggle produced its own tooltip size. */
    font-size: var(--tooltip-font-size);
    line-height: var(--tooltip-line-height);
    /* Always normal weight: the popup must not inherit a bold trigger (e.g. the selected
       summary/detail pill's bold label) — it is body text, not an echo of the control. */
    font-weight: normal;
    /* Same reasoning, and the same bug one property along. A ::after is a CHILD of its trigger,
       so it inherits every inheritable text property: an uppercase, letter-spaced trigger handed
       its tooltip the same treatment and the popup read as a shouted label rather than a
       sentence. First hit by .radio-vertical-badge, which is uppercase with 1.3px tracking; no
       earlier carrier set either property, which is why the base rule had not needed to say this.
       Normalised HERE rather than overridden per control, for the reason the font-size comment
       below gives: a new styled carrier then gets a correct tooltip for free, and per-control
       overrides are the thing that produced per-control tooltip sizes in the first place. */
    text-transform: none;
    letter-spacing: normal;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    z-index: 1000000;
    /* pre-line preserves explicit '\n' newlines (paragraph breaks) while still
       collapsing runs of regular whitespace — lets callers pass multi-paragraph
       tooltip strings via \n\n separators. */
    white-space: pre-line;
    /* Disappear instantly: opacity fades in 0.15s, visibility flips at 0.15s
       (linear 0s with a 0.15s delay — visibility is non-transitionable so the
       delay is the only knob). */
    transition: opacity 0.15s ease-in, visibility 0s linear 0.15s;
}

/* No per-trigger font-size overrides here any more. The user-mode pills (1.4em text) and the
   segmented toggle (1.5em) each used to need one, because the base rule's `1.2em` compounded off
   their scaled labels. The base is now an absolute --tooltip-font-size, so there is nothing to
   compound and nothing to correct: a new scaled control inherits the right size for free. Adding
   an override back would re-introduce per-control tooltip sizes, which is the bug this removed. */

div[data-description]:hover::after,
div[data-description]:focus::after,
button[data-description]:hover::after,
button[data-description]:focus::after,
.inline-toggle-text[data-description]:hover::after,
.inline-toggle-text[data-description]:focus::after,
.segmented-toggle-text[data-description]:hover::after,
.segmented-toggle-text[data-description]:focus::after,
.radio-vertical-badge[data-description]:hover::after,
.radio-vertical-badge[data-description]:focus::after {
    opacity: 0.95;
    visibility: visible;
    /* Appear after 0.4s of sustained hover — short enough that the tooltip
       feels responsive when you genuinely want it, long enough that quick
       drive-by hovers don't fire it. */
    transition: opacity 0.15s ease-in 0.4s, visibility 0s linear 0.4s;
}

/* On a phone these popups are removed from layout entirely, not merely kept hidden, and that is a
   LAYOUT fix rather than a cosmetic one.

   The rule above hides the popup with visibility:hidden, which still occupies space: an absolutely
   positioned box of min-width 300px anchored at left:-20px inside a control near the right of a
   320px screen extends far past the viewport, and an overflowing absolute descendant grows every
   ancestor's scrollWidth up to the document. Measured on the Design page at three widths: the
   document came out 427px at a 320px viewport, 447 at 360, 462 at 390, tracking (viewport + 534)/2,
   which is the signature of a fixed-width box centred in the row. That sideways scroll is what makes
   a position:fixed modal appear cut off on the right on a real phone, because a mobile browser sizes
   the layout viewport to the content it finds.

   Keyed on POINTER, not width, and the difference is an accessibility one. The justification is that
   the popup opens on :hover and a touch screen has no hover, so keying it on width instead would take
   the tooltips away from a hover-capable desktop merely narrowed below 576px -- including the
   :focus::after reveal, which is the keyboard path, and which the uncontested display:none here would
   override. (hover: none) removes them exactly where they could never have been read, at any width,
   and leaves every hover-capable device untouched at every width. A narrow desktop window therefore
   keeps its (working) tooltips and its horizontal scrollbar, which is the right way round: the
   scrollbar is cosmetic there, whereas on a phone it makes fixed content look cut off.

   Every carrier that matters on a phone states its content another way (a composite's guidance line
   is always visible; a help page opens on tap). */
@media (hover: none) {
    div[data-description]::after,
    button[data-description]::after,
    .inline-toggle-text[data-description]::after,
    .segmented-toggle-text[data-description]::after,
    .radio-vertical-badge[data-description]::after {
        display: none;
    }
}

/* The tooltip host is a <u>, so the browser's own underline has to go. The class name is vestigial: it
   says "dotted" but nothing here or anywhere else in this file draws a dotted rule for the host, which
   signals itself with the (i) icon beside it instead.

   The :not() carve-out is load-bearing rather than defensive: the help-page class lands on
   this same element whenever a field carries BOTH a help page and reference figures (the popup opens for
   either), and at (0,1,1) a bare u.dotted would out-specify .help-page-link and silently strip the very
   underline that is its whole affordance. */
u.dotted:not(.help-page-link) {
    text-decoration: none;
}

/* --------------------------------------------------------*/
/* CONTROL-LABEL TOOLTIP (rich)                              */
/* --------------------------------------------------------*/
/* The leaf control row's label tooltip — the popup behind the (i) icon — is a REAL
   hidden element, not the data-description ::after above. A pseudo-element's
   `content: attr(...)` is one unstyled text run, so it can carry no emphasis and no
   list structure; a real element lets a tooltip use paragraphs, bullets and bold /
   italic. Emitted by RendererDesignControlsExtensions.AddDesignInputLabel from the
   TooltipMarkup block model.

   The data-description mechanism above is untouched and still serves the module pills,
   the user-mode / layout / Real-Nominal toggles and the inline radio options, whose
   tooltips are single-sentence and need no structure. */
.control-tooltip-host {
    position: relative;
}

.control-tooltip-host:hover {
    cursor: help;
}

/* Geometry mirrors the data-description popup so both read as the same object. Hidden
   the same way too: opacity + visibility + pointer-events, flipped on hover with the
   0.4s delay that keeps drive-by hovers quiet. */
.control-tooltip {
    position: absolute;
    top: 45px;
    left: -20px;
    min-width: 300px;
    max-width: 500px;
    border: 1px #BBBBBB solid;
    border-radius: 10px;
    background-color: #f9f9f9;
    padding: 10px;
    color: #444444;
    /* The SAME absolute size as the data-description popup (see --tooltip-font-size): these are
       two mechanisms but one thing to the user, and they used to disagree by ~30% because this
       1.2em resolved against a plain control label (1.2rem) while the pills' resolved against a
       1.3em summary (1.56rem). */
    font-size: var(--tooltip-font-size);
    /* The popup hangs inside a <label>, so reset the label's own text styling rather
       than inheriting a bold, centred or truncated run into the body copy. */
    font-weight: normal;
    font-style: normal;
    text-align: left;
    line-height: var(--tooltip-line-height);
    white-space: normal;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    z-index: 1000000;
    transition: opacity 0.15s ease-in, visibility 0s linear 0.15s;
}

/* No CSS :hover reveal: the popup is a `popover="manual"` top-layer element (.tooltip-floating), which is
   display:none until general.js calls showPopover() after the sustained-hover delay. The base opacity /
   visibility / position above are all overridden by .tooltip-floating; general.js owns show / hide / place. */

/* Blocks carry no intrinsic margin; the gap between two blocks is the blank line the
   author wrote, applied once between siblings so the popup never opens or closes on
   stray whitespace. */
.control-tooltip > p,
.control-tooltip > ul {
    margin: 0;
}

.control-tooltip > * + * {
    margin-top: 0.6em;
}

.control-tooltip-list {
    padding-left: 1.35em;
}

.control-tooltip-list > li {
    margin: 0.15em 0;
}

/* A label and its module (i) on one line, centred in its circle and sized to the table rather than to the
   host's own font. TWO callers, rendering OPPOSITE orders: the Assumptions figures cell puts the icon
   FIRST (so every row's icon lines up in a column), the Strategy strip's rung label puts it LAST (so every
   row's icon lines up against the right edge of a right-aligned column). */
.label-tooltip-host {
    display: inline-flex;
    align-items: center;
    /* GAP, not a directional margin, and that is the whole point of this block. The spacing was
       margin-right: 10px on the icon, which silently encodes "the icon comes first": the second caller
       then rendered its label and icon TOUCHING, with the 10px stranded on the far side of the icon
       where it separated nothing. A gap is the same 10px whichever way round the two are, so neither
       caller's order is privileged and a third needs no new rule.
       The popup is a child of this host and would otherwise become a third flex item, but it is
       .control-tooltip { position: absolute }, so it is out of flow and the gap never reaches it. */
    gap: 10px;
}

.label-tooltip-host div.help-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: none;            /* the icon keeps its size in the flex row, never stretching to fill the cell */
    margin: 0;             /* the host's gap owns the spacing; div.help-icon's own margin-left would add to it */
    max-height: 22px;
}

.label-tooltip-host img.help-icon {
    height: 14px;          /* fixed, so the icon does not scale up with the bold 1.3em first-column font */
}

/* The floating tooltip: a top-layer popover (popover="manual", shown and positioned by general.js on hover)
   shared by the control-label (i) popups, the Goal / What-If accent popups and the Assumptions-figure popups.
   Being in the top layer it escapes BOTH the Results side's overflow clip (.comparison-side { overflow-x:
   hidden }) AND a greyed not-in-use section's opacity: opacity caps every DESCENDANT, but a top-layer element
   is not a descendant for rendering, so the tooltip always reads full-strength and black, like a module
   control's. And a bulky tooltip can no longer render partially off-screen: general.js sets its viewport
   left/top, clamped inside the viewport and flipped above the trigger near the fold (see the positioner
   there). It carries NO type of its own: each popup is still a .control-tooltip / .accent-tooltip-popup, so
   the body rules above apply directly (the cascade, not inheritance), giving every floating tooltip the same
   absolute --tooltip-font-size. The compound selectors keep this override winning over each base rule's
   position / opacity / visibility regardless of source order. Parked off-screen until general.js positions it,
   and opacity:1 (a shown popover has no CSS :hover to fade it in — display:none until showPopover()). */
.control-tooltip.tooltip-floating,
.accent-tooltip-popup.tooltip-floating {
    position: fixed;
    inset: auto;
    margin: 0;
    top: -9999px;
    left: -9999px;
    opacity: 1;
    visibility: visible;
    /* Cap to the viewport (minus the 8px top/bottom margins general.js clamps to) and scroll internally, so a
       popup taller than the window stays fully on-screen instead of overflowing the bottom fold: it is
       position:fixed, so anything past the fold could not otherwise be reached. */
    max-height: calc(100vh - 16px);
    overflow-y: auto;
}

/* Sources section inside a figure tooltip: shown on hover beneath the description, separated only by a grey
   divider (no heading). The whole popup, description and sources together, appears on hover; general.js's
   grace-period close lets the pointer reach a source link. */
.figure-sources {
    margin-top: 10px;
    padding-top: 8px;
    border-top: 1px solid #dddddd;
}

.figure-sources-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.figure-source {
    margin-bottom: 9px;
    line-height: 1.35;
}

.figure-source:last-child {
    margin-bottom: 0;
}

/* Kind badge, colour-coded by how the source AGES (see LegislativeSourceKind): a Statute is stable,
   Guidance is revised silently, an Announcement is not yet law. The base carries a neutral background so a
   future LegislativeSourceKind with no colour rule of its own still renders a visible badge (rather than
   white-on-transparent), until it is given its own colour below. */
.figure-source-kind {
    display: inline-block;
    font-size: 0.9em;
    text-transform: uppercase;
    letter-spacing: 2px;
    text-indent: 2px;      /* balances the trailing space letter-spacing adds after the last letter */
    padding: 4px 6px;
    border-radius: 8px;
    margin-right: 7px;
    color: #ffffff;
    background-color: #616161;
    line-height: 1;        /* tight box so vertical-align: middle centres it against the citation text */
    vertical-align: middle;
    position: relative;
    top: -1px;             /* nudge up: middle alone reads slightly low next to the larger citation text */
}

.figure-source-kind-statute {
    background-color: #2e7d32;
}

.figure-source-kind-guidance {
    background-color: #ef6c00;
}

.figure-source-kind-announcement {
    background-color: #c62828;
}

/* An outbound citation link, in the app's one link idiom rather than a browser-blue one: black, detached
   dotted underline, solid on hover, colour fixed. It used to be #1565c0 with a solid underline that
   DISAPPEARED on hover, which inverted the house behaviour on both axes at once. The tooltip it sits in is
   light (#f9f9f9 on #444), so black reads cleanly. */
.figure-source-link {
    color: #000000;
    text-decoration: underline;
    text-decoration-style: dotted;
    text-underline-offset: 0.3em;
}

.figure-source-link:hover {
    text-decoration-style: solid;
}

/* "Typical values": reference figures from the outside world, appended at the BOTTOM of a field's
   tooltip to orient a user who has no idea what to type.

   THE WHOLE SECTION IS ONE GREY CARD: glyph, title and body inside a single rounded light-grey box,
   so it reads as one object rather than as a heading with loose text under it. That is also why it
   carries NO top divider: an earlier version had both a divider and an inner badge, which drew two
   separations where the card's own background already provides one.

   Deliberately the shape of the three growth bands at the top of this same popup (same centring,
   same border-radius) so the popup has ONE heading language, but with a neutral grey fill where
   they carry a coloured wash. The neutral is load-bearing: those washes each encode a claim about
   how a value behaves, and these figures are informally sourced (see
   DF.Core/Dom/Items/TypicalValues.cs), so a coloured fill would make an informal figure read as
   more authoritative than a legislated one. Visual authority must stay proportional to backing.
   Localisation note: the title is rendered from C#, not CSS content, unlike the growth bands; it
   moves to ILanguagePack with the rest (CLAUDE.md rule 14). */
.typical-values {
    /* No top margin by default; it is ADDED below when real content precedes the card. See that rule for
       why this is stated positively rather than as "10px, cancelled when first". */
    margin-top: 0;
    background-color: #F2F2F2;
    /* #BBBBBB is exactly the border the three growth bands use, so this darkens the card AND completes
       the family match: same shape, same border, same type, differing only in the fill that carries the
       claim. It was #DDDDDD, which read as too faint against the card's own grey. */
    border: 1px solid #BBBBBB;
    border-radius: 8px;
    padding: 8px 12px;
}

/* The card is separated from the author's PROSE, so the margin belongs to "something real precedes me"
   rather than to the card itself. A figures-only popup must open flush: that is not a hypothetical shape,
   it is the Employment gross-salary field, which carries no tooltip of its own and whose whole popup
   exists for its figures (see AddDesignInputLabel's shouldOpenPopup gate).

   This was `.typical-values:first-child { margin-top: 0 }` until the growth band moved into the popup, and
   that construct is now UNUSABLE here: the band is a real first child, so it voids `:first-child` for every
   in-flow sibling while contributing no layout of its own (it is position: absolute, seated by the popup's
   own padding-top). The rule read as correct, matched nothing, and reintroduced a 10px gap on exactly the
   field its comment named. Stating the margin positively fixes the whole class rather than that instance:
   the card is spaced from the block above it when that block is CONTENT, and the band is chrome, not
   content. No rule inside this popup may key on :first-child again, which TestGrowthMarkStyling enforces. */
.control-tooltip > :not(.control-tooltip-band) + .typical-values {
    margin-top: 10px;
}

/* Glyph and title, centred as a group above the body.
   font-size sits HERE rather than on .typical-values-title so the flex `gap` below is measured in
   the same 1.25em context the growth bands space themselves in. The rendered title size is
   unchanged either way (the title inherits it); what changes is what `gap`'s em resolves against. */
.typical-values-heading {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.25em;
    /* Match the bands' glyph-to-label spacing, which is now `gap: 1em` for exactly the reason this is.
       The bands USED to bake it as three non-breaking spaces inside generated content
       (`content: url(...) "\a0\a0\a0Inflation adjusted"`), because a content-list image + text renders
       as one run and could not use `gap` at all; they are real elements now and set the gap directly.
       The distance is unchanged, and both are still derived from the
       font rather than guessed: the stack is Tahoma (see the `body` rule), whose space advances
       682/2048 = 0.333em, making three of them ~1em at this font-size. A generic "~0.25em per space"
       would have left it a quarter short. The previous `7px` was measured at the BASE font size, i.e.
       under half the band's, which is why the band's gap read as the correct one beside it. */
    gap: 1em;
    margin-bottom: 5px;
}

/* The compass. Sized here rather than on the asset, as every icon now is including the three band
   glyphs, which were the one exception until they stopped being CSS content:url() images. The asset is a
   2-path ring-and-needle compass; an earlier 6-path one with cardinal tick marks was replaced because
   it muddied at this size, so keep a replacement sparse. */
.typical-values-glyph {
    width: 20px;
    height: 20px;
    flex-shrink: 0;
}

/* Type matched EXACTLY to the growth bands' ("Inflation adjusted"): weight 600, 1.25em, #223344, and
   NO uppercase or letter-spacing. Same surface, same heading language, so a reader meets one kind of
   tooltip heading rather than two. An earlier version was small uppercase grey letterspaced text,
   which read as a different component entirely.
   Note the text colour matching the bands does NOT weaken the neutral-fill argument above: the claim
   is carried by the card's FILL (neutral grey here, a coloured wash there), so the shared ink is the
   thing that makes the fills comparable at all.
   The 1.25em is inherited from .typical-values-heading rather than set here, so the heading's `gap`
   resolves against it; the rendered size is the same 1.25em the bands use. */
.typical-values-title {
    font-weight: 600;
    color: #223344;
    line-height: 1;
}

/* One paragraph, figures inline. See AddTypicalValues for why this is not a list plus a note. */
.typical-values-body {
    margin: 0;
    line-height: 1.35;
    color: #555555;
}

.typical-value-label {
    color: #777777;
}

.typical-value-figure {
    font-weight: 600;
    color: #333333;
    white-space: nowrap;   /* a money figure must never break mid-number */
}

.figure-source-citation {
    color: #444444;
}

.figure-source-reviewed {
    color: #888888;
    margin-left: 5px;
}

/* --------------------------------------------------------*/
/* GROWTH CONTROL MARKS — CpiInflatedControls.md §6 / §4.5    */
/* --------------------------------------------------------*/
/* A value the engine grows over the simulation. Three mutually-exclusive marks, split by WHICH
   rate drives the value, each two things off one class emitted on the control row:
     .cpi-inflated   (ICpiInflated / IInflationGrown) — grown at CPI: a light warm-neutral wash
        on the number box + an "Inflation adjusted" band leading its tooltip.
     .market-grown   (IMarketGrown) — grown at a MARKET anchor (Equity Return / House Price
        Growth / Crypto Return): a light-green wash + a "Grows with the market" band.
     .interest-grown (IInterestGrown) — grown at a DEPOSIT / savings rate (Savings / Premium
        Bonds prize fund): a light cool-blue wash + an "Earns interest" band. The user can tell
        a capital-safe interest earner (a savings balance, a bond) apart from an at-risk market
        holding and a CPI-tracking value.
   Only the wash COLOUR splits per mark now: the band's shape is a single .control-tooltip-band rule,
   and the glyph and the wording are not in this stylesheet at all (they moved to GrowthMarkExtensions
   in C#, where ILanguagePack can reach the wording). So each mark is one wash rule per surface, and
   the surfaces are three: the control row's box, the tooltip band, and the wizard pill. The £ /
   currency prefix span is deliberately left untinted. Colours are --color-cpi-inflated-wash
   (ChartPalette.CpiInflatedWash = #F6F3EB, warm) / --color-market-grown-wash (= #F1FBEE, green)
   / --color-interest-grown-wash (= #EEF4FB, cool blue), all kept off the blue focus glow so a
   marked field never reads as "focused". */

/* The box wash — the always-visible mark. Per-mark, since the colour is the whole point. */
.control-row.cpi-inflated input.form-control:not(.disabled) {
    background-color: var(--color-cpi-inflated-wash);
}
.control-row.market-grown input.form-control:not(.disabled) {
    background-color: var(--color-market-grown-wash);
}
.control-row.interest-grown input.form-control:not(.disabled) {
    background-color: var(--color-interest-grown-wash);
}

/* The tooltip band: a stand-alone, fully-rounded card seated INSIDE the field's own tooltip
   popup, at the top. The popup keeps the base tooltip's shape (a normal #f9f9f9 rounded box);
   the band is inset from every popup edge so the popup's own background shows around it on all
   four sides, and the popup is given extra top padding so its text sits below the band. Marked
   fields with no author tooltip render no popup at all, so they show the box wash but no band;
   that is fine, the wash is the always-visible mark, and keeping the band a CHILD of the popup
   is what preserves that for free.
   The band is a real div rendered by AddTooltipPopup inside .control-tooltip, the popup element
   the control label renders (see the CONTROL-LABEL TOOLTIP section above). Being a child of the
   popup, it inherits the popup's own hide / show, so unlike the old ::after-based band it needs
   no opacity, visibility, transition or :hover rules of its own.
   Localisation note: the band's wording is no longer here. It moved to GrowthMarkExtensions.Label
   in C#, where ILanguagePack can reach it (CLAUDE.md rule 14); inside this stylesheet it could
   not be localised at all. */
.control-tooltip-band {
    /* Shared band shape. The glyph and label are real children rendered by AddTooltipPopup, so this
       flexbox centres them as a group and `gap` spaces them. They used to be a `content: url(...) "text"`
       list on a ::before, which forced two workarounds now gone: the spacing had to be baked as three
       non-breaking spaces (a content list renders as ONE generated run, so `gap` had nothing to act
       between), and the Interest rule needed a load-bearing trailing space because "\a0Earns" parses as
       the escape \a0Ea plus "rns interest". It also forced a width/height onto the three SVG assets,
       since a content:url() image renders at its intrinsic size and is not CSS-sizable.
       The gap is 1em, and that is NOT a fresh guess: it reproduces those three spaces exactly. `gap`
       resolves against this element's own computed font-size, which the 1.25em below puts in the same
       context the spaces sat in, and Tahoma's space advances 682/2048 = 0.333em, so three of them are
       ~1em. `.typical-values-heading` was deliberately tuned to this same distance, so a number picked
       here for merely looking right silently un-pairs the two. */
    gap: 1em;
    position: absolute;
    /* Inset from the popup's own edges — the popup is the containing block, so the band
       stretches between left and right rather than guessing a width. */
    top: 10px;
    left: 10px;
    right: 10px;
    box-sizing: border-box;
    display: flex;                   /* centre the glyph+label run ... */
    align-items: center;             /* ... vertically ... */
    justify-content: center;         /* ... and horizontally across the full band */
    font-weight: 600;
    font-size: 1.25em;
    color: #223344;
    border: 1px #BBBBBB solid;       /* full border on all four sides */
    border-radius: 8px;              /* rounded on every corner — a stand-alone card */
    padding: 6px 12px;               /* symmetric — flexbox handles the centring */
}

/* The glyph, shared by the tooltip band and the wizard pill (both drew the same asset at its
   intrinsic size before, so one class reproduces both). Height only, with width auto, so each asset
   keeps the aspect its own viewBox declares. This is the sizing that used to have to live on the SVG
   root, which is what rule 7 of SvgImageGuidelines.md forbids.
   Heights matched at 13px before and after; the WIDTHS deliberately did not. The old root attributes
   were 20 / 13 / 19 against viewBoxes of 22x14 / 14x14 / 22x14, i.e. two of the three were squashed,
   so the rendered widths are now ~20.4 / 13 / ~20.4. Do not "restore" 19px for Market: that number
   was the distortion, not the target. */
.growth-mark-icon {
    height: 13px;
    width: auto;
    flex-shrink: 0;
}

/* Per-mark wash. The glyph and wording are no longer here: they are rendered from C# by
   GrowthMarkExtensions, which is also what makes the wording reachable by ILanguagePack when the
   rule 14 migration lands. It was unreachable while it sat in this stylesheet. */
.control-tooltip-band.cpi-inflated {
    background-color: var(--color-cpi-inflated-wash);
}
.control-tooltip-band.market-grown {
    background-color: var(--color-market-grown-wash);
}
.control-tooltip-band.interest-grown {
    background-color: var(--color-interest-grown-wash);
}

/* Pin the popup's width so the inset band aligns to it, and reserve top padding to seat the
   band. Corners, border, and background stay the base tooltip's — a normal rounded #f9f9f9
   popup with the band floating inside its top. */
.control-row.cpi-inflated .control-tooltip,
.control-row.market-grown .control-tooltip,
.control-row.interest-grown .control-tooltip {
    width: 340px;
    min-width: 340px;
    max-width: 340px;
    padding-top: 54px;               /* band zone; other sides keep the base 10px */
}

/* Wizard-only: an always-visible growth-mark pill rendered ABOVE a marked control in a wizard step,
   right-aligned (WizardSessionComponent.RenderGrowthTagIfMarked). A wizard step never surfaces the field
   tooltip, so the hover band above never shows there; this pill carries the same wash, glyph, and wording as
   the Design-page mark, so it is explicit during guided setup rather than only the subtle box wash. It sits
   above (not beside) the control so it never competes with the row's horizontal space, which the suggestible
   chevron + currency prefix already claim; the .wizard-growth-tag-row right-aligns the inline-flex badge over
   the control's right edge. The pill carries the shared .wizard-growth-tag shape plus the mark class
   (.cpi-inflated / .market-grown / .interest-grown) for the wash. */
.wizard-growth-tag-row {
    text-align: right;
    margin-bottom: 6px;
}

.wizard-growth-tag {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 4px 12px;
    font-weight: 600;
    font-size: 1em;
    color: #223344;
    border: 1px #BBBBBB solid;
    border-radius: 8px;
}

/* Per-mark wash, on the SAME three mark classes the control row and the tooltip band use: one mark
   vocabulary app-wide rather than a wizard-private .wizard-cpi-tag trio. Safe to share because every
   rule that reads a mark class qualifies it with the element it sits on. The glyph is an <img> child
   rendered by WizardSessionComponent (sized by .growth-mark-icon above), not a ::before. */
.wizard-growth-tag.cpi-inflated {
    background-color: var(--color-cpi-inflated-wash);
}
.wizard-growth-tag.market-grown {
    background-color: var(--color-market-grown-wash);
}
.wizard-growth-tag.interest-grown {
    background-color: var(--color-interest-grown-wash);
}

/* --------------------------------------------------------*/
/* PROFILE */
/* --------------------------------------------------------*/

.info {
    word-break: break-all;
    margin-top: 20px;
    padding: 10px;
    border-collapse: collapse;
    background-color: var(--color-row-stripe-even);
    border-style: solid;
    border-width: 1px;
    border-color: #ddd;
    border-radius: 10px;
}

.info-title {
    font-weight: bold;
    padding-top: 5px;
    font-size: 2em;
    text-align: center;
}

/* Profile tab: horizontally center everything below the user-mode selector
   (separators, sign-in / sign-out / delete sections). text-align centers the
   paragraph text and the inline-level buttons; the full-width separator rules
   are unaffected. */
.profile-below-modes {
    text-align: center;
}

.info-text {
    padding-bottom: 5px;
    padding-top: 3px;
    font-size: 1.1em;
}

.info-text div { padding: 5px; }

.info-text p {
    font-size: 1.2em;
    word-break: normal;
    line-height: 1.4;
}

.info-text a.link {
    font-size: 1.2em;
    font-weight: bold;
}

.info-separator {
    border-bottom-color: #999;
    border-bottom-style: solid;
    border-bottom-width: 1px;
    margin-top: 15px;
    margin-bottom: 10px;
}

/* Section header above each of the four /Profile groups: the mode cards (Lite / Standard / Expert), the
   identity cards (this browser / recovery key / Google), then Profile Management and Privacy Settings.
   Bold, centered, large. Tweak freely, this class is used only on that tab.
   (Two until Sep 2026; TestProfileComponentRecoveryKey asserts all four by name and in order.) */
.profile-section-title {
    font-size: 2em;
    font-weight: 700;
    text-transform: uppercase;
    text-align: center;
    margin-top: 20px;
    margin-bottom: 5px;
}

/* A thin gray rule ABOVE every section title but the first, so the four groups read as separate blocks
   rather than one long scroll. Drawn by the title itself rather than a separator element between sections,
   so a section added or reordered later gets its divider with no markup to remember, and the page can never
   start or end on a stray line. ADDITIVE: the first title (User Mode, the first child of .profile-page) is
   simply not matched, so it keeps the base rule above untouched and the base stays the one source for its
   spacing. Same #ccc as the other faint borders in this file. */
.profile-page .profile-section-title:not(.profile-page > :first-child) {
    border-top: 1px solid #ccc;
    margin-top: 40px;
    padding-top: 30px;
}

.profile-section-subtitle {
    text-align: center;
    color: var(--color-text-secondary);
    font-size: 1.5em;
    margin-bottom: 14px;
}

/* The two sections BELOW the identity cards (Profile Management, Privacy Settings), each holding a
   pair of action cards. Same auto-fit idea as .tier-cards, with a wider floor: these cards carry a
   paragraph rather than a one-line title, so they need more room before going side by side, and at
   two per section the auto-fit resolves to 2 columns on a wide viewport and 1 when it narrows.

   They are a SEPARATE class from .tier-card deliberately. A tier card is a control (the non-active
   ones are clickable and carry the hover/active states above); these are inert containers, so
   borrowing the class would inherit a cursor and a hover lift that mean nothing here. */
/* ------------------------------------------------------------------ */
/* PROFILE PAGE BUTTONS                                                 */
/* ------------------------------------------------------------------ */

/* BIGGER BUTTONS AND BIGGER LABELS, on this page only.

   Every action here (sign out, delete, export, the analytics toggle, the tier-card credential maintenance)
   is a Bootstrap .btn-lg, and .btn-lg is shared with the whole app, so this has to be scoped or it resizes
   buttons on screens nobody asked about. .profile-page is the root div ProfileComponent opens, and it
   exists for exactly this.

   It sets Bootstrap 5's OWN custom properties rather than re-declaring padding and font-size, which is the
   part worth copying: .btn reads --bs-btn-padding-y / -x / --bs-btn-font-size at render, so overriding them
   needs no !important and wins no specificity fight -- the variables cascade and the framework's rule
   consumes them. Re-declaring `padding` here would instead have to out-specify .btn-lg and would then also
   have to restate the border-radius it carries.

   Sizes are rem, like Bootstrap's own, so they key off the root 10.8px rather than off whichever container
   the button happens to sit in: an em here would make a button in a tier card a different size from the
   identical button in an action card. */
.profile-page .btn-lg {
    --bs-btn-padding-y: 0.75rem;
    --bs-btn-padding-x: 1.6rem;
    --bs-btn-font-size: 1.65rem;
}

/* ------------------------------------------------------------------ */
/* PROFILE CARD-SET SPACING                                             */
/* ------------------------------------------------------------------ */

/* ONE PADDING FOR ALL FOUR CARD SETS on /Profile, and it is grouped rather than repeated because the two
   classes are genuinely different structures that happen to need identical spacing:

     .tier-cards          the 3-up SELECTOR grids (User Mode, Sign-in Method). Their cards are controls:
                          the non-active ones are clickable and carry hover and active states.
     .profile-action-cards  the 2-up ACTION sections (Profile Management, Privacy Settings). Their cards
                          are inert containers.

   They stay separate classes for that reason, and the cards must not share one: borrowing .tier-card for
   an inert box would inherit a cursor and a hover lift that mean nothing there. What they DO share is
   where they sit under their section subtitle, and that had already drifted 20px apart once and could not
   be fixed from either rule alone. Top and sides align the sets; the bottom is the gap to the next
   section's heading. */
.tier-cards,
.profile-action-cards {
    padding: 10px 30px 60px;
}

.profile-action-cards {
    display: grid;
    /* min(320px, 100%), not a bare 320px: on a phone the grid's content box is NARROWER than 320px
       (360px viewport, less the page gutter and this rule's 30px sides), and a bare minmax floor then
       forces a track wider than its container, so both cards overflowed to the right and read as
       off-centre under the centred tier cards above. Capping the floor at the container keeps the
       2-up-on-wide / 1-up-on-narrow behaviour and lets the single column shrink to fit. */
    grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
    gap: 20px;
    /* The section sits inside .profile-below-modes (text-align: center); the cards keep that for
       their buttons but the grid itself must not inherit any inline centring of its tracks. */
    align-items: start;
}

.profile-action-card {
    border: 2px solid #e5e5e5;
    border-radius: 12px;
    padding: 20px;
    background-color: #fafafa;
}

/* The heading of one action card ("Sign out of this device", "Your data", ...). Its own class rather than
   the <p><b> the surrounding .info-text body uses, so the four read as card TITLES at a glance and can be
   sized in one place: at 1.2em they were the same size as the paragraph under them and the eye had to read
   the words to find the boundary. Sits between .profile-section-subtitle (1.5em, muted, one per section)
   and the card body (1.2em), in weight rather than in size. */
.profile-action-card-title {
    font-size: 1.9em;
    font-weight: 700;
    line-height: 1.3;
    padding-bottom: 6px;
}

/* The Country and language card: a label, then its dropdown, one row per choice. */
.profile-choice-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    margin-top: 10px;
}

/* Right-aligned in a fixed column, so the two labels end at the same edge beside their dropdowns. */
.profile-choice-label {
    min-width: 11em;
    text-align: right;
}

/* The choices read larger than the labels beside them (owner, Sep 2026); the switcher's dropdowns keep their size. */
.profile-choice-row .action-dropdown {
    font-size: 1.25em;
}

/* Recovery-key fork popup: the two action halves ("Create a new key" / "You already have a
   key") each sit in their own bordered action-tinted box. */
.recovery-key-box {
    border: 1px solid #999;
    background-color: var(--color-pill-action-light);
    border-radius: 10px;
    padding: 10px;
}

.recovery-key-box + .recovery-key-box {
    margin-top: 12px;
}

/* Centre the fork's two action buttons ("Create a new key" / "Restore profile") under their
   left-aligned blurb. fit-content shrinks the block-level button so margin auto can centre it. */
.recovery-key-box > button {
    display: block;
    margin-left: auto;
    margin-right: auto;
    width: fit-content;
}

.recovery-key-box-title {
    text-align: center;
    font-weight: bold;
    font-size: 1.6em;
    margin-bottom: 8px;
}

.recovery-key-box-text {
    font-size: 1.15em;
    line-height: 1.4;
    word-break: normal;
    margin-bottom: 10px;
}

/* Once a fresh key has been minted and revealed in the same popup, the "You already have a key"
   restore box goes inactive: its paste field and Restore button are functionally disabled (so a
   different code cannot be pasted and restored over the just-minted key), and its title + blurb grey
   to the app's standard disabled-text colour so the whole box reads as inactive rather than leaving
   full-strength prose over greyed controls. Colour-only, so nothing shifts, the modal's
   no-layout-shift promise holds. */
.recovery-key-box-inactive .recovery-key-box-title,
.recovery-key-box-inactive .recovery-key-box-text {
    color: #BBBBBB;
}

/* The paste field's key, and its format hint, centred (user, 2026-09-25), the same as the landing sign-in
   panel's key field (.lp-tier-input), so the one key reads the same in both places. */
.recovery-key-box input.form-control {
    text-align: center;
}

/* Lighter-grey ghost text for the paste field's format hint (default placeholder grey is too dark
   against the action-tinted box). */
.recovery-key-box input.form-control::placeholder {
    color: #bbbbbb;
    opacity: 1;   /* Firefox dims placeholders by default; keep the chosen grey exact */
}

.btn-google-signin {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    background-color: #1a1a1a;
    color: #fff;
    border: none;
    border-radius: 4px;
    padding: 10px 20px;
    font-size: 1.1em;
    font-weight: 500;
    cursor: pointer;
    transition: background-color 0.2s;
}

.btn-google-signin:hover {
    background-color: #333;
    color: #fff;
}

.btn-google-signin:disabled {
    background-color: #555;
    cursor: not-allowed;
    opacity: 0.7;
}

.btn-google-signin img {
    width: 20px;
    height: 20px;
}

/* --------------------------------------------------------*/
/* PRICE BOX */
/* --------------------------------------------------------*/

.price-box {
    border-radius: 8px;
    font-size: 1.3em;
    position: fixed;
    bottom: 5px;
    left: 5px;
    color: #CCCCCC;
    padding: 10px;
    border-style: none;
    border-width: 3px;
    z-index: 9000;
    background-color: #0D1021;
    box-shadow: rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px;
}

.price-box-charts {
    border-radius: 8px;
    padding: 3px;
}

.price-box:hover { cursor: pointer; }

.price-box.blue { border-color: var(--color-blue); }

.price-box.red { border-color: var(--color-red); }

.price-box .price-title { text-align: left; }

.price-box .small-info {
    font-size: .7em;
    padding-top: 5px;
}

.price-box .price-result {
    font-weight: bold;
    text-align: right;
}

.price-box .price-result.blue { color: #009EFF; }

.price-box .price-result.red { color: #E60000; }

/* Insolvency value renders as a yellow/black caution badge so it stands out from
   the red loss figures elsewhere in the box (yellow = caution, red = error).

   THE YELLOW IS LOAD-BEARING, and this comment used to undersell why, which invited
   the reasonable question "should this be red, like the Timeline's insolvency row?".
   It is not merely that a red figure appears somewhere in the box. GeBoxClass turns
   BOTH the box's border (.price-box.red) and the headline final-net-worth figure
   (.price-result.red) red whenever that total is negative, i.e. on a failing plan,
   which is exactly the plan whose insolvency badge most needs to be seen. A red badge
   would be red-on-red beneath a red number, disappearing precisely when it matters.
   (Insolvency and a negative final total are correlated, not identical: a plan can go
   broke at 55 and recover. So the collision is not guaranteed on every insolvent plan,
   only on the worst ones.)

   The Timeline reaches the opposite answer for the same reason, not a different one:
   nothing there is red, so red discriminates. See DF.Claude/Features/Timeline.md. */
.price-box .insolvency-badge {
    display: inline-block;
    background-color: var(--color-caution-strong);
    color: var(--color-black);
    border-radius: 5px;
    padding: 1px 6px;
}

/* Yellow What-If bolt shown in front of the final net-worth figure whenever a
   What-If is active, reminding the user the headline total is a What-If result
   and not their baseline. Sized to sit inline with the bold number. The colour
   lives in the SVG (WhatIf.svg is filled yellow), so this rule only sizes and
   aligns the image. */
.price-box .price-whatif-badge {
    height: 1.4em;
    vertical-align: middle;
    margin-right: 5px;
}

.price-box td { padding: 2px 4px; }

.comparison-pricebox-row {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    position: fixed;
    bottom: 5px;
    left: 5px;
    right: 5px;
    z-index: 9000;
    pointer-events: none;
}

.comparison-pricebox-side {
    pointer-events: auto;
}

.comparison-pricebox-right {
    margin-left: auto;
}

.comparison-pricebox-side .price-box {
    position: static;
    z-index: auto;
    width: auto;
    box-sizing: border-box;
}

/* --------------------------------------------------------*/
/* SPECIAL */
/* --------------------------------------------------------*/

#cover-spin {
    position: fixed;
    width: 100%;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    background-color: #ffffff;
    background-color: rgba(255, 255, 255, 0.7);
    z-index: 9998;
}

#logo-text {
    border-radius: 10px;
    border-color: #444444;
    border-width: 1px;
    border-style: dotted;
    background-color: #EFEFEF;
    padding: 20px;
    text-align: center;
    font-size: 1.7em;
    margin-top: 50px;
    margin-bottom: 10px;
    color: black;
    font-weight: bold;
    position: relative;
    z-index: 9999;
}

#logo {
    height: 64px;
    width: 64px;
    overflow: hidden;
    display: block;
    position: absolute;
    top: 30%;
    left: 50%;
    margin: -32px 0 0 -32px;
    z-index: 9999;
}

/* Modals sit ABOVE the busy overlay (#cover-spin 9998, its label and logo 9999). A modal opened while a page is
   still loading is a question the load is WAITING on (the Terms acceptance prompt, the project-limit modal during
   start-up), so the overlay must never cover it: at Bootstrap's own 1050/1055 it did, leaving the question greyed
   out behind a spinner that could not finish until it was answered. Tooltips (1000000) stay above both. */
.modal-backdrop {
    z-index: 10000;
}

.modal {
    z-index: 10001;
}

/* In-app project-loading spinner: SpinnerComponent renders #cover-spin + #logo +
   #logo-text as siblings. The #logo-text "Application loading..." label sits at the
   top in normal flow, so move the spinner to the vertical centre of its offset parent
   (the viewport / app container; #cover-spin is a sibling, not the containing block) to
   clear the label box. The full-screen #cover-spin overlay covers that same area, so the
   spinner reads as centred over it. Scoped via the sibling combinator so the boot screen's
   #logo (index.html, which has no #cover-spin sibling) keeps its own placement. */
#cover-spin ~ #logo {
    top: 50%;
}

img.logo-normal {
    height: 65px;
    width: 65px;
    overflow: hidden;
    display: block;
    margin-left: auto;
    margin-right: auto;
}

img.logo-small {
    height: 30px;
    width: 30px;
    overflow: hidden;
    display: block;
    margin-left: auto;
    margin-right: auto;
}

img.logo-mini {
    height: 16px;
    width: 16px;
    overflow: hidden;
    display: inline;
    margin: 0;
}

#footer-results {
    padding: 10px;
    margin-top: 20px;
}

#footer-page {
    margin: 15px;
    color: #555555;
    font-size: 1.4em;
    text-align: center;
    letter-spacing: 1px;
    border-top: 1px solid #DDD;
    padding: 10px;
}

/* THE BRAND LINE: "Copyright (c)YYYY [logo] Diligent Finance", and the app's only route back to the
   public site. See MainLayout.razor for why the way out lives here rather than in the chrome bar.

   BLACK, against the #555555 the rest of the footer inherits (user, Sep 2026). The line is the page's
   signature, so it is the one part of the footer that should not read as a muted afterthought; the
   tagline, the legal links and the feedback control below it keep the grey deliberately, which is what
   gives this line its weight. */
.footer-brand-line {
    color: #000000;
}

/* An anchor, so the link colour and the underline both need neutralising: this must read as the brand
   mark it already was, not as a link in a sentence. The colour comes from the line above rather than
   being restated, so the two cannot drift apart. There is deliberately NO :hover rule restating
   text-decoration: none -- this selector (0,1,0) already outranks Bootstrap's `a { text-decoration:
   underline }` (0,0,1) at every state, and reboot's a:hover only reassigns a colour variable, so the
   restatement was dead weight (raised in review, #3637). The sibling .explore-header-brand:hover
   restates `color` for the same non-reason. */
.footer-brand {
    color: inherit;
    text-decoration: none;
}

/* FULL OPACITY, where this mark carried 0.7 before (user, Sep 2026): the whole line is black now, and a
   logo at 0.7 beside black text reads as a different, lighter mark rather than as the same one. */
.footer-brand-logo {
    width: 50px;
    height: 50px;
    margin: 0 10px;
}

/* THE NAME NEVER BREAKS: "Diligent" on one line and "Finance" on the next reads as two words rather than
   as a mark, and a phone did exactly that (user, 2026-09-24). */
.footer-brand-name {
    white-space: nowrap;
}

/* And the logo never parts from it: the anchor is one unbreakable unit, so a line too narrow for the
   whole lockup breaks after the copyright rather than between the mark and the name. */
.footer-brand {
    white-space: nowrap;
}

/* STACKED ON A PHONE (user, 2026-09-24): copyright, then the logo, then the name, one per line, instead
   of one inline line that broke wherever the width ran out and split the logo from the name. The line
   needs about 347px at the landing footer's size and more at the app's, so it broke on most phones;
   576px is Bootstrap's small breakpoint, which covers them all. */
@media (max-width: 576px) {
    .footer-brand-line,
    .footer-brand {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 6px;
    }

    .footer-brand-logo {
        margin: 0;
    }
}

/* The only hover affordance the lockup offers, and it matches .footer-legal-link's. It is on the NAME
   rather than the anchor so the rule is not drawn under the logo as well. */
.footer-brand:hover .footer-brand-name,
.footer-brand:focus-visible .footer-brand-name {
    text-decoration: underline;
    text-underline-offset: 0.25em;
}

#about {
    font-size: 1.3em;
    text-align: center;
    /* Cap the prose to a centered column; without this the page runs the full
       layout width (up to 1600px) and the centered text reads as awkwardly long lines. */
    max-width: 720px;
    margin-left: auto;
    margin-right: auto;
}

#about h2 {
    padding-bottom: 10px;
    margin-top: 50px;
    margin-bottom: 20px;
    border-bottom: #DDDDDD solid 1px;
}

#about p {
    /* Left-align the body copy for readability; the headers and the 720px column stay centered. */
    text-align: left;
}

#help {
    font-size: 1.3em;
}

#help h2,
#help h3 {
    padding-left: 5px;
}

/* --------------------------------------------------------*/
/* ANALYTICS NOTICE */
/* --------------------------------------------------------*/

.analytics-notice {
    text-align: center;
    color: #555555;
    font-size: 1.1em;
}

/* --------------------------------------------------------*/
/* WARNING TOP BANNER */
/* --------------------------------------------------------*/

/* --------------------------------------------------------*/
/* FRAMES */
/* --------------------------------------------------------*/

/* Grow-in for a block that arrives because a simulation finished, so it opens rather than snapping
   and shoving whatever the user was reading down the page.

   It replaced a `max-height: 0 -> 10000px over 3s` that could not work, in two independent ways, and
   both are worth stating because either one on its own makes an animation here silently inert:

   1. A max-height animation reveals its content at the instant the animated value passes the
      content's REAL height, so the ceiling has to be the same order as the content or almost all of
      the duration animates above it, invisibly. Against a 10000px ceiling a 60px row finished
      revealing 0.6% of the way in, ~18ms: an instant snap followed by 2.98 seconds of nothing.
      Lowering the ceiling does not fix this, it only moves it: at a 600px ceiling the same row
      finishes at 27.5ms, still 94% wasted, and a ceiling tight enough to be honest is one that CLIPS
      any content taller than the guess.
   2. max-height collapses a box without clipping it, because overflow defaults to visible. So even
      in the window where the value was below the content height, the children painted at full size
      and OVERLAPPED what sat below rather than being revealed.

   The grid form has neither problem: the row resolves 1fr to the child's own height, so the animated
   value IS the height, whatever the content turns out to be. Nothing to guess and nothing to clip
   wrongly. ease-out therefore maps straight onto the height, opening quickly and settling gently.

   Cost, stated rather than glossed: grid-template-rows is a layout property, so this runs layout per
   frame, exactly as max-height did. That is affordable here because it is ONE element on the frame a
   pricing completes (only one module is expanded at a time). It would not be affordable applied to
   several boxes at once; if this is ever extended to the chart slots, measure first.

   Support: Chrome 107+, Firefox 66+, Safari 16+ interpolate a <flex> track. An engine that does not
   simply snaps to the final height, i.e. degrades to the behaviour this replaces. */
/* Structure, always on: the wrapper is emitted whether or not it animates, so that toggling the
   modifier below cannot renumber the render frames after it. At 1fr with one child the row is the
   child's own height, so a wrapper that never animates is layout-neutral. */
.reveal-grow {
    display: grid;
    grid-template-rows: 1fr;
}

/* The animation. What makes it fire ONCE is that a CSS animation starts when the class ARRIVES on an
   element, and a re-render that keeps the class does not restart it. The component emits both classes
   unconditionally on a row that has content, so the class is there from the element's first frame and
   the row does not re-animate on every pricing. Measured, not assumed: at a temporary 12s duration a
   re-price left the row at full height.

   Do not re-gate the modifier on a "these pills are new" flag. That is the version that breaks, and
   quietly: such a flag flips whenever the pill count crosses zero on a row that keeps rendering
   because of its info boxes, so the class lands on a MOUNTED element and collapses an already-visible
   box to 0fr to regrow it, which is the jump this exists to remove.

   The animation lives on the modifier rather than on .reveal-grow so the base class stays pure
   structure: a future wrapper can reuse it without animating by accident. */
.reveal-grow-run {
    animation: reveal-grow-rows 0.4s ease-out;
}

/* A grid item's min-height defaults to auto, which floors the ROW at the content height and would
   make the 0fr keyframe a no-op. This is the one declaration that has to live on the child, and it
   is inert outside an animation (it only removes a floor), so it can stay on permanently. */
.reveal-grow > * {
    min-height: 0;
}

@keyframes reveal-grow-rows {
    from {
        grid-template-rows: 0fr;
        /* Point 2 above. It is declared in the KEYFRAMES rather than on .reveal-grow so it applies
           only while the animation runs: a permanent overflow:hidden here would clip any popup that
           escapes the revealed block, and the contingency pills carry hover tooltips that do. */
        overflow: hidden;
    }

    to {
        grid-template-rows: 1fr;
        overflow: hidden;
    }
}

/* The whole point of the animation is to soften a movement, which is precisely what a reader who
   asks for reduced motion does not want animated. Landing at the final height immediately is the
   pre-animation behaviour, so this is a clean opt-out rather than a degraded one. */
@media (prefers-reduced-motion: reduce) {
    .reveal-grow-run {
        animation: none;
    }
}

/* --------------------------------------------------------*/
/* FOOTER LEGAL LINKS */
/* --------------------------------------------------------*/

.footer-legal-link {
    cursor: pointer;
    color: #888;
    text-decoration: none;

    /* The in-app twin of `#footer-page .footer-legal-links a`: a different element (a span that
       opens a modal, rather than an anchor to the standalone page) rendering the footer row, so it
       carries the same offset for the same reason. Fixing only one would leave the two footers
       underlining differently on surfaces a user meets in the same session.

       The class has a FOURTH consumer that is not a legal link: MainLayout.razor's "Send us
       feedback", which carries an inline font-size of 0.85em against the row's 0.8em. An em offset
       is the right unit for that: it scales with each consumer's own type instead of pinning a
       pixel gap that would read differently at the two sizes. */
    text-underline-offset: 0.25em;
}

.footer-legal-link:hover {
    text-decoration: underline;
    cursor: pointer;
}

/* --------------------------------------------------------*/
/* COMPARISON LAYOUT */
/* --------------------------------------------------------*/

.comparison-container {
    display: flex;
    gap: 0;
    width: 100%;
}

.comparison-side {
    flex: 1;
    min-width: 0;
    overflow-x: hidden;
    padding: 3px;
}

.comparison-divider {
    display: flex;
    align-items: flex-start;
    padding: 12px 6px 0 6px;
    font-size: 2em;
    color: #999;
    cursor: default;
    user-select: none;
}

/* The two-arrow compare glyph (Compare.svg) that replaced the former ↔ between the two sides of a
   comparison. The SVG is black and rendered at full opacity everywhere it is used. */
img.compare-icon {
    width: 30px;
    height: 30px;
    vertical-align: middle;
}

.comparison-toggle-icon img.compare-icon {
    width: 18px;
    height: 18px;
}

.action-label {
    color: inherit;
}

.settings-diff-banner {
    background-color: #fff3cd;
    color: #856404;
    border: 1px solid #ffc107;
    border-radius: 4px;
    padding: 8px 15px;
    margin: 5px 15px;
    font-size: 1.3em;
}

/* Last-resort recovery surface shown when startup left no project (e.g. the server
   was unreachable during load). A Retry button replaces the indefinite loading spinner. */
.project-load-error {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    /* Tighter than the gap it separates the notice from, so the buttons read as ONE action group rather
       than as three evenly-spaced things. A flat gap made the distance from the notice to the first button
       identical to the distance between the buttons, which says nothing about what belongs to what. */
    gap: 10px;
    padding: 60px 20px;
    color: #555;
}

/* The notice inside it is the SHARED .banner-box.warning, so this rule adds no chrome of its own: it undoes
   two things the SURFACE does that the banners' usual home does not. First, width: the surface is a centred
   column (align-items: center), so a child shrinks to its content instead of filling the row, and align-self
   restores the full width the banners get from ordinary block flow. Second, alignment: the surface centres
   its text for the buttons, and a full-width box with centred prose reads as a different component again.

   It carried a 520px max-width for about an hour, on the reasoning that a centred surface wants a reading
   measure. That was backwards: the point of moving this message onto .banner-box was that it should look like
   every other notice, and a cap none of the others have made it consistent in chrome and inconsistent in
   shape, which is the more visible half. If a reading measure is ever wanted here it belongs on .banner-box
   itself, applied to all of them.
 */
.project-load-error .banner-box {
    align-self: stretch;
    text-align: left;
    /* The notice, THEN its actions. This is the only spacing on the surface that is deliberately larger than
       the rest; it is what groups the buttons under the message instead of leaving them floating below it.
       Spacing rather than nesting, and that is a considered choice: the family's own actions slot renders
       dotted-underline LINKS (see WizardBannerComponent, the only user of it, which strips the box chrome
       entirely), so moving these inside would demote the only way off this page to a text link. */
    margin-bottom: 14px;
}

/* Shown inside the project-limit modal when a delete the user asked for did not happen. It lives here
   rather than in the shared open-failure banner because that banner is mounted by the design and results
   pages, which sit BEHIND this modal's overlay: an explanation the user cannot see is the silence this
   was built to remove. Red, because unlike the banner it reports a gesture that failed just now, under
   the pointer. */
.project-limit-failure {
    color: var(--color-red);
    font-weight: 600;
    margin-bottom: 12px;
}

/* The one-time recovery-key code shown in the "Your recovery key" modal. Centered by its
   flex row (justify-content-center) and rendered large, bold and black so the user can read
   and transcribe it clearly. word-break lets a long code wrap inside a narrow modal. */
.recovery-key-code {
    color: #000;
    font-weight: bold;
    font-size: 1.5rem;
    word-break: break-all;
}

/* The "copied to clipboard" confirmation shown directly under the key once the user copies it:
   green + centered, echoing the key it sits below. */
.recovery-key-copied {
    text-align: center;
    color: var(--color-green-dark);
    font-weight: 600;
    margin-top: 6px;
}

/* Design-page input-difference summary (comparison mode) */
.difference-summary {
    border: 1px solid #bbb;
    border-radius: 6px;
    background-color: var(--color-gray-steel);
    margin: 15px 0px 10px 0px;
    font-size: 1.3em;
}

.difference-summary-header {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 15px;
    padding: 8px 12px;
    user-select: none;
}

.difference-summary-title {
    display: flex;
    align-items: center;
    justify-content: center;
}

.difference-summary-chips {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 6px;
}

.difference-summary-header .chevron {
    margin-right: 8px;
}

.difference-summary-header-clickable {
    cursor: pointer;
}

.difference-summary-header-clickable:hover .difference-summary-count {
    text-decoration: underline;
}

.difference-summary-count {
    font-size: 1.2em;
}

.difference-summary-count-text {
    font-weight: bold;
}

.difference-chip {
    display: inline-block;
    padding: 1px 10px;
    border-radius: 9px;
    border: 1px solid #666;
    color: var(--color-black);
    font-size: 1em;
    line-height: 1.6;
    white-space: nowrap;
}

.difference-chip-symbol {
    font-weight: bold;
}

.difference-chip-added {
    background-color: var(--color-green-pale);
}

.difference-chip-removed {
    background-color: var(--color-pill-error-light);
}

.difference-chip-changed {
    background-color: #fff;
}

.difference-summary-body {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    justify-content: center;
    padding: 10px 12px;
    border-top: 1px solid #e0e5eb;
}

/* A card in the wrapping .difference-summary-body row. Its width follows its content (flex-basis
   auto), which is what keeps a card holding two short rows narrow, and is also why it needs a
   ceiling: one long row is one long line, so without max-width the card grew to that line's
   max-content width and a single difference could stretch the card past 1000px, shoving its
   siblings onto their own rows.

   480px + no `width` is deliberate, and is the .module-block idiom rather than a new one: those
   cards sit in the wrapping .module-group-items row and cap at the same figure. Do NOT add
   `width: 100%` alongside it as .module-block does — .module-block WANTS every card the same size,
   whereas here it would inflate a two-word card to the full 480px.

   overflow-wrap is inherited, so declaring it on the card covers the module name and every row
   below it in one place. Ordinary prose already wraps at spaces once the ceiling is in; this is for
   the token that has none, e.g. a long unspaced name the user typed, which would otherwise paint
   straight out of the capped box. */
.difference-module-card {
    border: 1px solid #666;
    border-radius: 10px;
    background-color: #fff;
    padding: 8px 12px;
    min-width: 170px;
    max-width: 480px;
    overflow-wrap: break-word;
    text-align: center;
}

.difference-module-name {
    font-weight: bold;
    font-size: 1.2em;
    margin: 4px 0 8px 0;
    padding: 5px;
    background-color: var(--color-gray-steel);
    border-radius: 8px;
    border: #ddd solid 1px;
}

.difference-rows {
    list-style: disc;
    width: fit-content;
    margin: 0 auto;
    padding: 0 0 0 1.3em;
    text-align: left;
}

.difference-row {
    padding: 2px 0;
}

.difference-row-label {
    color: #44505e;
}

.difference-row-change {
    color: #1a1a1a;
    font-weight: bold;
}

.comparison-no-match {
    background-color: #fff3cd;
    color: #856404;
    border: 1px solid #ffc107;
    border-radius: 8px;
    padding: 7px;
    margin-top: 15px;
    margin-bottom: 5px;
    margin-left: 15px;
    margin-right: 15px;
    font-size: 1.4em;
    font-weight: bold;
    text-align: center;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100px;
}

/* Narrow-screen comparison toggle */
.comparison-toggle {
    display: none;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 6px 0;
    font-size: 1.2em;
    margin-top: 5px;
    margin-bottom: 5px;
}

.comparison-toggle-name {
    cursor: pointer;
    color: #888;
}

.comparison-toggle-name:hover {
    text-decoration: underline;
}

.comparison-toggle-name-active {
    font-weight: bold;
    color: inherit;
}

.comparison-toggle-icon {
    color: #999;
    user-select: none;
}

/* COMPARISON PANEL */

.comparison-panel {
    border: 1px solid #DDDDDD;
    border-radius: 8px;
    padding: 20px;
    margin-bottom: 20px;
    background-color: var(--color-surface-light);
}

.comparison-headline {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 20px;
    margin-bottom: 20px;
}

.comparison-headline-side {
    flex: 1;
    text-align: center;
}

.comparison-headline-name {
    font-size: 1.4em;
    margin-bottom: 4px;
}

.comparison-headline-value {
    font-size: 2em;
    font-weight: bold;
}

.comparison-headline-center {
    text-align: center;
    min-width: 140px;
}

.comparison-headline-label {
    font-size: 1.4em;
    margin-bottom: 4px;
}

.comparison-headline-delta {
    font-size: 1.1em;
    font-weight: bold;
}

.comparison-delta-positive {
    color: var(--color-green);
}

.comparison-delta-negative {
    color: #CC0000;
}

.comparison-delta-zero {
    color: #888;
}

.comparison-winner {
    background-color: var(--color-green-light);
    border-radius: 8px;
    border: 1px solid var(--color-green);
    padding: 10px;
}

.comparison-loser {
    background-color: #F0F0F0;
    border-radius: 8px;
    border: 1px solid #AAAAAA;
    padding: 10px;
}

.comparison-headline-no-match {
    background-color: #fff3cd;
    color: #856404;
    border: 1px solid #ffc107;
    border-radius: 8px;
    padding: 15px;
    font-size: 1.2em;
    font-weight: bold;
    text-align: center;
    display: flex;
    align-items: center;
    justify-content: center;
    align-self: stretch;
}

.comparison-headline-center-box {
    background-color: #FFFFFF;
    border-radius: 8px;
    border: 1px solid #AAAAAA;
    padding: 10px;
    min-width: 180px;
    text-align: center;
}

.comparison-headline-difference-value {
    font-size: 2em;
    font-weight: bold;
    color: #000;
}

.comparison-arrow-left,
.comparison-arrow-right {
    color: var(--color-green);
}

.comparison-module-value-winner {
    background-color: var(--color-green-light);
}

.comparison-module-value-loser {
    background-color: #F5F5F5;
}

.comparison-module-difference {
    text-align: center;
    font-weight: 500;
    padding: 0 15px;
}

.comparison-modules-table tr.comparison-modules-total-row td {
    font-weight: bold;
    font-size: 1.5em;
    border-top: 2px solid #888;
    padding-top: 10px;
}

.comparison-modules-table {
    width: auto;
    margin: 30px auto 0 auto;
}

.comparison-modules-table th,
.comparison-modules-table td {
    padding: 6px 15px;
    white-space: nowrap;
}

.comparison-modules-table td,
.comparison-modules-table th {
    font-size: 1.3em;
    line-height: 1.2;
}

.comparison-modules {
    width: 100%;
    overflow-x: auto;
}

.comparison-module-name-left {
    text-align: right;
    padding-right: 10px;
}

.comparison-module-value-left {
    text-align: center;
    font-weight: bold;
}

.comparison-module-value-right {
    text-align: center;
    font-weight: bold;
}

.comparison-module-name-right {
    text-align: left;
    padding-left: 10px;
}

@media (max-width: 1199px) {
    .comparison-container {
        display: block;
    }

    .comparison-side-hidden {
        display: none;
    }

    .comparison-divider {
        display: none;
    }

    .comparison-toggle {
        display: flex;
    }

    .comparison-headline {
        flex-direction: column;
        gap: 10px;
    }

    .comparison-headline-value {
        font-size: 1.6em;
    }

    .comparison-headline-difference-value {
        font-size: 1.6em;
    }

    .comparison-modules-table td,
    .comparison-modules-table th {
        font-size: 1em;
        padding: 4px 6px;
    }

    .comparison-modules-table tr.comparison-modules-total-row td {
        font-size: 1.1em;
        padding-top: 6px;
    }

    .comparison-module-difference {
        padding: 0 6px;
    }
}

/* --------------------------------------------------------*/
/* PDF EXPORT MODAL */
/* --------------------------------------------------------*/

.pdf-export-options {
    padding: 10px;
}

.pdf-export-options p {
    font-size: 1.2em;
    margin-bottom: 15px;
}

.pdf-export-option {
    border: 1px solid #ddd;
    border-radius: 6px;
    padding: 12px 15px;
    margin-bottom: 10px;
    cursor: pointer;
    transition: border-color 0.15s, background-color 0.15s;
}

.pdf-export-option:hover {
    border-color: #999;
    background-color: var(--color-surface-light);
}

.pdf-export-option-selected {
    border-color: #333;
    background-color: #f0f0f0;
}

.pdf-export-option label {
    cursor: pointer;
    font-size: 1.15em;
}

.pdf-export-option-description {
    color: var(--color-text-secondary);
    font-size: 1.1em;
    margin-top: 4px;
    padding-left: 22px;
}

.pdf-export-generating {
    text-align: center;
    padding: 30px 20px;
}

.pdf-export-spinner {
    color: #333;
    margin-bottom: 15px;
}

.pdf-export-status {
    font-size: 1.2em;
    color: var(--color-text-secondary);
}

.pdf-export-error {
    font-size: 1.2em;
    color: var(--color-red);
    font-weight: bold;
}

/* ============================================
   Explore / Showcase Razor Pages
   ============================================ */

.explore-header {
    background: white;
    border-bottom: 1px solid #EEEEEE;
    padding: 15px 0;
    margin-bottom: 25px;
}

.explore-header .container {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.explore-header-brand {
    display: flex;
    align-items: center;
    gap: 14px;
    text-decoration: none;
    color: #000000;
    /* ONE TOKEN, so a page can size its header without restating the phone rule below: the landing page
       sets 2.9em, everything else takes this 2.2em default. */
    font-size: var(--explore-header-size, 2.2em);
    letter-spacing: 1px;
}

.explore-header-brand:hover {
    color: #000000;
    text-decoration: none;
}

/* IN em OF THE WORDMARK, so the glyph keeps its relationship to the name at every size, the phone's
   included. 2.36em is exactly what the two fixed sizes were: 56px against the app's 23.8px wordmark and
   74px against the landing page's 31.3px. */
.explore-header-logo {
    width: 2.36em;
    height: 2.36em;
}

/* THE NAME NEVER BREAKS BY ACCIDENT, the header's twin of .footer-brand-name: a phone showed "Diligent"
   beside the logo and "Finance" under it, which reads as two words rather than as the mark (user,
   2026-09-24). Below 640px it now STACKS ON PURPOSE (user, Sep 2026), both words beside the logo, one over
   the other; that is a designed lockup, not the wrap this rule exists to stop. */
.explore-header-name {
    white-space: nowrap;
}

.explore-breadcrumb {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 1.3em;
    padding-left: 2px;
}

/* THE PLAIN-PAGE BREADCRUMB: centred and enlarged (user, Sep 2026). Here rather than in
   calculator.css and showcases.css, which carried a byte-identical copy each until the pair was folded:
   app.css owns the .explore-breadcrumb base above, `body.plain-page` already scopes this to exactly the
   surfaces that want it (the calculators, the showcases and the two legal pages), and _Layout.cshtml
   serves this sheet to all of them. Two copies is the pair that diverges on the next tweak.

   justify-content is what does the centring: the element is already display:flex above, so text-align
   would do nothing to the flex items. The base's padding-left: 2px clears the header's optical edge,
   which is a LEFT-aligned concern that would push a centred row off true, so it is zeroed rather than
   left to cancel out by luck. The phone override in each page sheet keeps its own 1.3em, which exists
   because the long final crumb overflows a 360px screen; it inherits the centring from here. */
body.plain-page .explore-breadcrumb {
    justify-content: center;
    padding-left: 0;
    font-size: 1.7em;
}

.explore-breadcrumb-link {
    color: var(--color-blue-accent);
    text-decoration: none;
    /* Detach the hover underline from the glyph baseline so it reads as an affordance line.
       Matches the calc-page treatment for region pills, source citations, and share buttons —
       set in DF.Web.Server/wwwroot/calculators/calculator.css. */
    text-underline-offset: 0.25em;
}

.explore-breadcrumb-link:hover {
    text-decoration: underline;
}

.explore-breadcrumb-separator {
    color: #BBBBBB;
    font-size: 0.7em;
}

.explore-breadcrumb-current {
    color: #555555;
}

.explore-main {
    padding: 10px 0 30px 0;
}

.explore-main h1 {
    font-size: 2.3em;
    color: #333;
    margin-bottom: 12px;
}

.explore-main h2 {
    font-size: 1.8em;
    font-weight: bold;
    margin-bottom: 8px;
    text-align: center;
}

.explore-main h2 a {
    color: var(--color-blue-accent);
    text-decoration: none;
}

.explore-main h2 a:hover {
    color: var(--color-blue-accent);
    text-decoration: underline;
}

.explore-main p {
    font-size: 1.4em;
    color: #555;
    line-height: 1.6;
}

.explore-main article {
    border: 1px solid #BBBBBB;
    border-radius: 15px;
    padding: 22px 28px;
    margin-bottom: 18px;
    background: var(--color-surface-light);
}

.explore-main .tags {
    margin-bottom: 25px;
    text-align: center;
}

.explore-main .tag {
    display: inline-block;
    background: var(--color-pill-action-shadow);
    color: #333;
    padding: 5px 12px;
    border-radius: 6px;
    font-size: 1.3em;
    margin: 2px 3px;
    border: 1px solid #ccc;
}

.explore-main .cta {
    margin-top: 25px;
    text-align: center;
}

.explore-main .cta-button {
    display: inline-block;
    background: var(--color-blue-accent);
    color: white;
    padding: 12px 35px;
    border-radius: 8px;
    text-decoration: none;
    font-size: 1.4em;
    border: none;
    cursor: pointer;
}

.explore-main .cta-button:hover {
    background: #0b5ed7;
    color: white;
    text-decoration: none;
}

.explore-main .cta-button-small {
    display: inline-block;
    background: var(--color-blue-accent);
    color: white;
    padding: 7px 14px;
    border-radius: 6px;
    text-decoration: none;
    font-size: 0.95em;
    border: none;
    cursor: pointer;
}

.explore-main .cta-button-small:hover {
    background: #0b5ed7;
    color: white;
    text-decoration: none;
}

/* ── In-app Showcases page (Blazor /Showcases) — all text black ── */

.showcases-page h1,
.showcases-page h2,
.showcases-page p,
.showcases-page li,
.showcases-page .explore-plan-card,
.showcases-page .explore-plan-card-title,
.showcases-page .explore-plan-card-description {
    color: #000;
}

/* Showcase type badge: the single mandatory classification pill (Life Story / Money Lesson) at the top
   of each card, distinct from the auto-derived module Labels chips. The label text carries the meaning;
   colour reinforces it, so it is never colour-only. Its own white text + background, so the .showcases-page
   black override above does not reach it. */
.showcase-badge-row {
    margin: 0 0 12px;
    text-align: center;
}

.showcase-badge {
    display: inline-block;
    padding: 6px 12px;
    border-radius: 12px;
    border: 1px solid #444;
    font-size: 1.2em;
    font-weight: bold;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: #fff;
    /* Neutral fallback so a badge is never white-on-white: a new ShowcaseType whose per-type colour rule
       is missing still renders a visible grey pill. The per-type classes below override it. */
    background-color: #555;
}

.showcase-badge-lifestory {
    background-color: var(--color-blue-accent);
}

.showcase-badge-moneylesson {
    background-color: var(--color-green-dark);
}

/* Toss Up is deliberately NEUTRAL rather than a warm colour. Blue says "a life", green says "a winner";
   this badge means neither option wins, and a neutral slate is the honest signal for an absence. An amber
   or orange would read as a caution, and there is nothing to be careful about on a card whose whole message
   is that the choice matters less than the reader fears.

   A MID-TONE slate, and its own token, because this pill carries white text. This first shipped on
   --color-gray-steel, whose name says steel and whose value is a near-white panel background: 1.07:1, an
   invisible word inside a visible pill, on two public SEO pages. TestShowcaseBadgeContrast now computes the
   ratio for every badge from the stylesheet, so the next one cannot ship unreadable. */
.showcase-badge-tossup {
    background-color: var(--color-slate-badge);
}

/* The household pill: whose life this showcase is, beside the type badge. Deliberately QUIETER than the
   badge -- neutral surface, normal weight, no uppercase -- because colour is the badge's meaning-bearing
   channel and a second saturated pill next to it would compete for the same glance. The word carries the
   meaning here, exactly as it does on the badge, so the pill is never glyph-only. */
.showcase-household-pill {
    display: inline-block;
    padding: 6px 12px;
    border-radius: 12px;
    /* Literal values, not var(--df-border) / var(--df-surface-muted): those two names are READ in four
       places in this file and DECLARED nowhere in the repo, so the fallback is what has always rendered.
       Copying the pattern would spread a theme hook that does not exist. */
    border: 1px solid #bbb;
    background-color: #f2f2f2;
    font-size: 1.05em;
    font-weight: 600;
    line-height: 1.3;
    white-space: nowrap;
}

.showcase-badge + .showcase-household-pill,
.showcase-household-pill + .showcase-household-pill {
    margin-left: 8px;
}

/* Sized in `em` so it tracks the pill's font-size, and spaced with a margin rather than `gap`, both
   matching .module-ownership-pill-glyph: the visitor meets these same two figures on their module cards
   after cloning, so the two surfaces must not drift into different glyph treatments.

   Note there is deliberately NO extra margin between two adjacent glyphs. An earlier version added 6px,
   which inverted the grouping: 10px between the two figures of a couple and 4px between the second figure
   and the word they both label, so the second figure read as belonging to the text rather than to the
   pair. The module pill carries such a rule because its glyphs alternate with per-owner labels; here they
   are adjacent and share one label. */
.showcase-household-pill-glyph {
    height: 1em;
    width: auto;
    vertical-align: -0.12em;
    margin-right: 4px;
    opacity: 0.75;
}


.showcases-page .cta-button {
    padding: 10px 20px;
    font-size: 1.3em;
}

/* ── In-app showcase GALLERY: the collapsed card grid ──────────────────────────────────────────────
   Every rule below is scoped under .showcases-page, which exists only on the Blazor /Showcases page.
   That is not tidiness: DF.Web.Server/Pages/_Layout.cshtml serves THIS stylesheet to the public
   /uk/showcases pages too, where showcases.css defines its own .showcase-card family and loads after
   this file. An unscoped rule here would reach that page, and a more specific one would win there. */

/* auto-fill + minmax fills the row at every width with no media query: four columns inside the
   1600px container, three on a laptop, one on a phone. A fixed card width instead would pick a
   column count for one screen and leave a dead gutter on the rest. */
.showcases-page .showcase-gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(360px, 100%), 1fr));
    gap: 18px;
}

/* The card shell is unchanged (.explore-main article above); only its bottom margin goes, because
   the grid's gap now owns the spacing and the two would add up.

   The cards in a row still stretch to a common height (the grid default, so `align-items` is NOT set
   to `start`): a card's natural height is driven by how many tag chips it happens to carry, which is
   derived metadata and says nothing about the showcase, so ragged row bottoms would be noise. The
   content simply sits at the top of the taller ones. */
.showcases-page .showcase-gallery > article {
    margin-bottom: 0;
    /* Tighter than the shared `.explore-main article` 22px/28px, for the same reason the plan cards
       inside are tighter: that value is tuned for the public detail page, which gives one showcase a page
       to itself. Here it is a gallery tile. Scoped rather than changed at source, because `_Layout.cshtml`
       puts `.explore-main` around every server-rendered page too, and only the calculator and public
       showcase sheets override the padding, so lowering the shared value would reach anything else that
       does not. */
    padding: 12px;
    transition: background-color 0.15s ease, box-shadow 0.15s ease;
}

/* Pointer cue on a COLLAPSED card, and it is the shared clickable-card treatment (fill + ring, see
   --hover-card-ring at the top of this file), not a border tint invented here: the module picker, the
   goal / What-If catalogue, the sweep-suggestion cards and the wizard tiles all already wear it, and a
   showcase card is the same kind of thing, a tile you press to open something in place.

   It does NOT contradict the rejected blue title above. What was rejected there is blue TEXT, which on
   the sibling public listing card marks a whole-card link to another page; this fill is the family cue
   for a card that acts where it stands, which is exactly what this one does.

   The ring never travels without the fill (TestHoverCardRing): alone on a light card it measures about
   1.24:1, a hover the user cannot see. The .year-tile border-to-black alternative is deliberately not
   copied either, because that is an exemption for a card whose background carries MEANING (a heat tint,
   a completed / started state) and would be overpainted by a fill. This card's background carries none.

   Collapsed only. An opened card spans the row and is 400 to 700px tall, so filling it would tint a
   large slab of prose the reader is in the middle of; its header stays clickable and keeps the title
   underline below as its cue. The 12px padding ring is part of the hover area but not of the click
   target, which is the ordinary card trade: reacting as the pointer enters the card is what a reader
   expects, and matching the button exactly (:has(.showcase-gallery-toggle:hover)) would instead leave a
   band that looks like the card and does not respond.

   The :focus-visible twin is NOT the same question as that rejected :has(...:hover) form, and is why the
   two are not symmetric. The card is opened by a real <button>, so a keyboard user reaches it and gets
   the toggle's own focus outline, but until Aug 2026 got neither the fill nor the title underline that a
   pointer user gets: the cue was :hover-only, which is the one input method it was never checked against.
   :has() is the only way to reach the ARTICLE from the focused button inside it. The title underline
   follows through the header rule below. */
.showcases-page .showcase-gallery > article:not(.is-expanded):hover,
.showcases-page .showcase-gallery > article:not(.is-expanded):has(.showcase-gallery-toggle:focus-visible) {
    background-color: var(--color-pill-action-light);
    box-shadow: var(--hover-card-ring);
}

/* An opened card takes the whole row, then bounds ITSELF and centres in it. Spanning the row is the one
   thing a grid can express and a flex row cannot, and is why the gallery is a grid; the max-width is what
   stops the result being a 1400px box with a 900px column of content adrift in the middle of it and a wide
   empty band down each side.

   Bounding the CARD rather than its contents is the whole trick, and it replaces three max-widths with
   one: the header and the body used to carry their own 900px caps precisely because the card did not, so
   the prose was bounded while the box around it was not. Now nothing inside needs a cap, because there is
   no excess width left inside to escape into.

   Deliberately NOT grid-auto-flow: dense — an opened card leaves the cells it vacated empty, and
   backfilling them with later cards would reorder the gallery against ShowcaseProject.DisplayOrder,
   which UkShowcaseCatalogue curates by hand. */
.showcases-page .showcase-gallery > article.is-expanded {
    grid-column: 1 / -1;
    max-width: 900px;
    width: 100%;
    margin-left: auto;
    margin-right: auto;
}

/* Opening the card is the one place this page moves, so the body UNFOLDS rather than appearing, through
   the shared `.reveal-grow reveal-grow-run` wrapper (see that block above). It is the codebase's one way
   to animate a height to `auto`: a grid whose single row runs 0fr -> 1fr, clipped by its own child.

   Nothing is redefined here on purpose. An earlier version of this change wrote a parallel copy of that
   mechanism under gallery-specific names, which is the §6 defect exactly: a second implementation of a
   shipped, guarded primitive (TestRevealGrow pins six declarations of it), free to drift from the first.
   The body therefore carries `reveal-grow reveal-grow-run` in the markup and this file adds no animation
   of its own. The `-inner` element is the single child that wrapper requires.

   The card's jump to its open WIDTH is still not animated: that is a grid-column change, which nothing
   can interpolate, and it lands before the unfold begins, so the two read as one movement. */

/* ── Tighter plan / comparison cards INSIDE an opened showcase ──────────────────────────────────
   The shared .explore-plan-card and .explore-comparison spacing is tuned for the public DETAIL page,
   which shows one showcase on a page of its own and can afford to breathe. Here the same cards sit
   inside a card, inside a gallery the visitor is scanning, so the outer chrome is already paying for
   the separation and the inner padding repeats it. Every value below is a REDUCTION of a shared one,
   scoped to this page, so /uk/showcases keeps the airier treatment it was designed with.

   What is NOT reduced: the type sizes, the module pills and the clone links. Compacting is about the
   whitespace between things, and shrinking the text would cost legibility to buy the same pixels the
   margins give away for free. */
.showcases-page .showcase-gallery-body .explore-plan-card,
.showcases-page .showcase-gallery-body .explore-comparison {
    margin-top: 10px;
    padding: 12px 16px;
}

.showcases-page .showcase-gallery-body .explore-plans {
    margin-top: 8px;
    gap: 4px;
}

/* The pills sat 20px clear of the title, which is the single largest gap in the card and the least
   earned: they label the plan the title names, so they belong close to it. */
.showcases-page .showcase-gallery-body .explore-module-pills {
    margin-bottom: 10px;
}

.showcases-page .showcase-gallery-body .explore-plan-card-title {
    margin-bottom: 6px;
}

.showcases-page .showcase-gallery-body .explore-plan-card-counts {
    margin-bottom: 6px;
}

/* Anchors the absolutely-positioned toggle below, and nothing else: the 900px reading measure it used to
   carry now lives on the opened card itself, so the header simply fills whatever card it is in.

   No padding is reserved on either side. There was a chevron here, a centred row at the foot of the
   header, and it was REMOVED as confusing: at the bottom of a card a downward chevron reads as "there is
   more below", which is what a scroll cue means, not "this opens". The hover underline on the title and
   the pointer cursor carry the affordance instead. If an explicit glyph is ever wanted back, the
   conventional place for a disclosure marker is the TOP-RIGHT corner, not the foot; that costs a reserved
   strip on BOTH sides, since everything in this header is centred. */
.showcases-page .showcase-gallery-header {
    position: relative;
}

/* ── The open unfold's tempo ───────────────────────────────────────────────────────────────────────
   The shared `reveal-grow-run` runs for 0.4s, tuned for the box it was written for: a contingency-pills
   row a few dozen pixels tall. This body is 400 to 700px, so the same duration is a far higher
   pixels-per-second and reads as a flicker rather than a reveal. Only the tempo is local; the mechanism,
   the keyframes and the reduced-motion opt-out all stay the shared ones.

   **It honours `prefers-reduced-motion`, which means a developer can lose an hour here.** On a machine
   asking for reduced motion the shared block sets `animation: none` and this card opens instantly, at
   ANY duration. That is correct behaviour, not a bug, and it is worth knowing because Windows turns
   that setting on BY ITSELF under Energy Saver / on battery, so an unfold that "worked yesterday" can
   be gone today with nothing committed in between. Diagnosed exactly that way in Aug 2026: 0.4s, 0.7s
   and 5s all looked identical to the reporter and all animated correctly in a headless browser, and the
   thing that settled it was re-asserting the animation at a specificity above the opt-out and watching
   it slide. If you are testing this, check the OS setting before touching the CSS. */
.showcases-page .showcase-gallery-body {
    animation-duration: 0.7s;
}

/* ── The tag chips, quieter and last ───────────────────────────────────────────────────────────────
   The chips are the least discriminating thing on the card and were the most prominent. Measured over
   the 14 shipped showcases: "Earned Income" is on 14 of 14, "Retirement Planning" 13, "Appreciating
   Asset" and "Tax Efficient" 12 each, so four labels account for 51 of the 72 chips on the page. At
   the shared 1.3em the heaviest card wrapped to three rows ABOVE the title, which is the one thing on
   the card that actually tells showcases apart.

   Two changes, and neither drops a label: the markup now renders the chips after the lede (see
   Showcases.razor), and they are smaller here than the shared `.explore-main .tag`. They sit in a
   symmetric 12px band, replacing the shared 25px gap that used to separate them from the title.

   NO LONGER in-app only: the public DETAIL page took the same treatment in Aug 2026 (markup move in
   `Showcase.cshtml`, density under `article.showcase-detail` in showcases.css), because the argument is
   about what tags ARE rather than which surface shows them. This comment asserted the opposite until then.
   The public LISTING card still carries no tags at all.

   The three values below are therefore duplicated in showcases.css rather than shared, because the two
   sheets serve different apps and app.css is scoped `.showcases-page` (a class the public page does not
   have). Change one and change the other: they are meant to read identically, and nothing enforces it. */
.showcases-page .showcase-gallery .tags {
    margin-top: 12px;
    margin-bottom: 12px;
}

.showcases-page .showcase-gallery .tag {
    font-size: 1.1em;
    padding: 3px 8px;
    margin: 2px;
}

/* The transparent disclosure button, covering the header so the whole header is one click target.
   Sits above the header content to receive the click. The content is not interactive, so this costs
   only two things, both minor and both deliberate: selecting the header's text (the opened body still
   allows it), and the native `title` tooltips on the admin-only view / import stat badges, which the
   overlay covers. */
.showcases-page .showcase-gallery-toggle {
    position: absolute;
    inset: 0;
    padding: 0;
    background: none;
    border: 0;
    border-radius: 8px;
    cursor: pointer;
}

.showcases-page .showcase-gallery-toggle:focus-visible {
    outline: 2px solid var(--color-blue-accent);
    outline-offset: -2px;
}

/* The title stays BLACK, like everything else on this page (`.showcases-page h2`). It was briefly given
   the interactive blue as a replacement affordance for the removed chevron, and that is rejected: the
   in-app gallery is not the public listing card, where a blue title marks a whole-card LINK to another
   page. Nothing here navigates, so the hover underline below and the pointer cursor carry the affordance
   and the page keeps the single text colour it has always had.

   Hover cue on the title alone: the card does not lift and nothing moves, matching the public listing
   card's treatment (showcases.css, .showcase-card:hover .showcase-card-title). The hover rule itself is
   two blocks down; the line-height between them is what buys that underline its clearance. */

/* The h2 is loosened from the 1.2 headings inherit, and it must be, because these titles WRAP: counted
   off a rendered gallery, three of the 25 shipped ones run to two lines ("Is your Cash ISA costing you a
   fortune?", "When does a side hustle need declaring?", "LISA or pension for your first home?"), and a
   narrower viewport wraps more. By the clearance formula on .module-block-header-text, gap = descent + (line-height - 1em) - offset, so 1.2 at this offset leaves
   0.1em and line 1's underline all but touches line 2. At 1.4 it is 0.3em. It costs a few px on the
   single-line cards too, since no selector can ask whether the text wrapped. */
.showcases-page .showcase-gallery-header h2 {
    line-height: 1.4;
}

.showcases-page .showcase-gallery-header:hover h2,
.showcases-page .showcase-gallery-header:has(.showcase-gallery-toggle:focus-visible) h2 {
    text-decoration: underline;
    text-decoration-thickness: 2px;
    /* 0.3em, the site's offset for this cue, raised from a local 0.22em: this card is now one of several
       surfaces wearing it and a private value here would read as a different cue at a glance. */
    text-underline-offset: 0.3em;
}

/* FIVE lines while collapsed, in full once open. Chosen when the 14 shipped descriptions ran 138 to 246
   characters and five lines showed every one of them in full. That no longer holds, and the clamp now
   ROUTINELY truncates the longer cards: estimated Sep 2026 at ~57 characters a line on a ~455px card, the
   77 descriptions run 138 to 704 characters and 25 of them overflow five lines (14 did before authored
   paragraph breaks, since each paragraph starts a line of its own). It is kept at five deliberately: the
   first paragraph (the person) is what a collapsed tile needs, and the toggle opens the rest. The lede
   stays on the collapsed card deliberately: it is what tells a visitor which way the lesson goes, so
   hiding it would put the cost this grid removes straight back. */
.showcases-page .showcase-gallery-lede {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 5;
    line-clamp: 5;
    overflow: hidden;
    margin-bottom: 1rem;
}

/* The lede is a box of paragraphs (ShowcaseDescriptionHelper.Paragraphs), so the paragraph margin lives on
   the box and not on each <p>: inside the clamped box a trailing margin would be clipped along with the
   hidden lines, and the gap below a collapsed card would shrink by exactly that margin. Between two
   paragraphs the gap is the same 0.5em the public listing card uses (showcases.css), on top of the
   global p padding both surfaces share. */
.showcases-page .showcase-gallery-lede > p {
    margin: 0;
}

.showcases-page .showcase-gallery-lede > p + p {
    margin-top: 0.5em;
}

.showcases-page .showcase-gallery > article.is-expanded .showcase-gallery-lede {
    display: block;
    overflow: visible;
}

.showcase-event-stats {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-bottom: 8px;
}

.stat-badge {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: 0.85rem;
    color: var(--color-text-secondary);
    background: #f0f0f0;
    padding: 2px 8px;
    border-radius: 4px;
}

.stat-badge i {
    font-size: 0.8rem;
    color: #999;
}

.stat-badge-time {
    color: #999;
    font-style: italic;
}

.explore-main .country-list {
    list-style: none;
    padding: 0;
    font-size: 1.4em;
}

.explore-main .country-list li {
    margin-bottom: 10px;
}

.explore-main .country-list a {
    color: var(--color-blue-accent);
    text-decoration: none;
}

.explore-main .country-list a:hover {
    text-decoration: underline;
}

.explore-plans {
    margin-top: 12px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.explore-plan-card {
    border: 1px solid #CCCCCC;
    border-radius: 10px;
    margin-top: 15px;
    padding: 18px;
    background: white;
    font-size: 1.2em;
    color: #555;
}

.explore-plan-card-title {
    font-weight: bold;
    display: block;
    font-size: 1.35em;
    margin-bottom: 10px;
    text-align: center;
}

a.explore-plan-card-title {
    color: var(--color-blue-accent);
    text-decoration: none;
}

a.explore-plan-card-title:hover {
    text-decoration: underline;
}

/* Element+class selector: the description is a <p> (uniform with the showcase description),
   so this must out-specify `.explore-main p` (1.3em) and reset the <p>'s default margin. */
p.explore-plan-card-description {
    display: block;
    margin: 0;
    color: #777;
    font-size: 1.2em;
}

/* Grey "3 Goals · 4 What-Ifs" counts line under the title. It carries its own colour so the
   .showcases-page override (which forces the title and description to black) does not reach it, and it
   is centered to sit under the centered title. A <div>, so `.showcases-page p`/`li` never catch it. */
.explore-plan-card-counts {
    color: #888;
    font-size: 1.05em;
    text-align: center;
    margin: 0 0 10px;
}

/* Per-plan module pills: the plan's user-added modules as icon + name chips, styled like the
   project tag chips but smaller (one per module, so more of them) and carrying the module's SVG icon.
   Shows a visitor what each plan is made of before cloning. */
.explore-module-pills {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 6px;
    margin: 0 0 20px;
}

.explore-module-pill {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    color: #333;
    padding: 4px 10px;
    border-radius: 6px;
    font-size: 1em;
    background: #f2f2f2;
    border: 1px solid #ccc;
}

.explore-module-pill-icon {
    width: 20px;
    height: 20px;
}

/* ── Comparison presets (Standard+): rounded boxes like the plan cards, on a light tint, each a
   [left | Clone and Compare | right] row with the caption (a description) below. ── */

.explore-comparisons {
    margin-top: 16px;
    display: flex;
    flex-direction: column;
    gap: 12px;
}

/* A sibling-but-distinct card to .explore-plan-card (same radius, padding and font-size), on a light
   blue tint with a light-blue border. The 1.2em base is load-bearing: it makes the .cta-button-small
   inside render at exactly the size the plan card's button does. */
.explore-comparison {
    border: 1px solid lightblue;
    border-radius: 10px;
    margin-top: 10px;
    padding: 18px;
    background: #F4F8FD;
    font-size: 1.2em;
    color: #555;
}

/* Three columns: the two plan titles flank a centred button. The middle `auto` column is the
   button's natural width; the equal 1fr flanks share the rest, so the button stays centred whatever
   the titles' lengths. */
.explore-comparison-row {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: 10px;
}

.explore-comparison-plan {
    font-weight: bold;
    font-size: 1.25em;
    text-align: center;
}

/* Below the row, sized like a plan description but in black, not a heading. Element+class so it
   out-specifies `.explore-main p`, exactly as p.explore-plan-card-description does. */
p.explore-comparison-caption {
    display: block;
    margin: 10px 0 0;
    text-align: center;
    color: black;
    font-size: 1.2em;
}

/* The clone actions are dotted-underline text links, one at the bottom of each description. Black and
   staying black on hover (the only hover effect is the dotted underline turning solid), with the
   underline detached (0.3em) like the site's other detached-underline links. Sized to match the
   description directly above: the showcase description (1.4em) for the project link, and the 1.2em
   card / caption descriptions for the per-plan and compare links (overridden below). */
.explore-clone-link {
    color: #000;
    font-size: 1.4em;
    text-decoration: underline dotted;
    text-underline-offset: 0.3em;
    cursor: pointer;
}

.explore-clone-link:hover {
    text-decoration: underline solid;
}

/* Inside a plan card or comparison box (both 1.2em) the description above is 1.2em, not the
   showcase description's 1.4em, so match that. */
.explore-plan-card .explore-clone-link,
.explore-comparison .explore-clone-link {
    font-size: 1.2em;
}

.explore-clone-link-line {
    margin-top: 8px;
    text-align: center;
}

/* The compare glyph between the two plan titles in a compare preset (smaller than the design/results
   divider). */
.explore-comparison-row img.compare-icon {
    width: 22px;
    height: 22px;
}

/* ── Legal content (Privacy, Terms — used in Razor Pages and Blazor modals) ───────── */

/* FONT-SIZES below are in em, so one container font-size scales them together. The Blazor modals
   take that from the root; the standalone /{country}/privacy and /{country}/terms pages set their
   own in calculator.css (`body.plain-page .legal-content`), beside the rest of that surface's
   page-scoped chrome, because a full page has no modal around it to set the scale.

   The VERTICAL RHYTHM is not in em and does not follow: `p` and `ul` take Bootstrap reboot's
   rem-pinned `margin-bottom: 1rem`, so raising the container leaves the gaps where they were and
   tightens the prose. calculator.css re-expresses those margins in em for the pages; a surface that
   changes the scale owes the same. Change a size here and both surfaces move together. */
.legal-content {
    max-width: 780px;
}

/* No font-size here on purpose. This selector can only match the two standalone legal pages, and
   both always load calculator.css, whose `body.plain-page .legal-content h1` (0,2,2) beats this
   (0,2,1) and pins the title at 2.3rem. A font-size here would therefore never apply, while still
   resolving to ~32px against that sheet's 1.35rem container if the override were ever narrowed or
   a new legal page forgot the calculator.css link. The margin is live and stays. */
.explore-main .legal-content h1 {
    margin-bottom: 18px;
}

.legal-content h2 {
    font-size: 1.3em;
    font-weight: bold;
    color: #333;
    margin-top: 1.6em;
    margin-bottom: 0.4em;
}

/* A literal a legal text quotes (the "#" of a link's fragment) is set in monospace so it reads as the character
   itself, but in the prose's colour: Bootstrap paints every <code> pink (--bs-code-color), which in a legal notice
   reads as emphasis nobody intended. Styled here rather than by editing the text, because a published legal text
   is immutable (TestPublishedLegalDocuments) and a colour is not a change to what anyone accepted. */
.legal-content code {
    color: inherit;
    font-size: 0.95em;
}

.legal-content p,
.legal-content li {
    font-size: 1em;
    line-height: 1.6;
    color: #444;
}

.legal-content ul {
    padding-left: 1.5em;
}

.legal-content .legal-updated {
    color: #888;
    font-size: 0.85em;
    margin-bottom: 0;
}

.legal-content a {
    color: var(--color-blue-accent);
}

.legal-modal-content {
    /* Fill the (wider) legal modal; the .legal-content 780px cap is for the standalone legal pages. */
    max-width: 100%;
    max-height: 65vh;
    overflow-y: auto;
    padding-right: 10px;
}

/* The three legal modals (Privacy Policy / Terms of Service / About us) share one width, wider on
   large screens. :has(.legal-modal-content) matches only these modals, so every other modal keeps
   its default Bootstrap size. */
.modal-dialog:has(.legal-modal-content) {
    max-width: 720px;
}

@media (min-width: 992px) {
    .modal-dialog:has(.legal-modal-content) {
        max-width: 920px;
    }

    .legal-modal-content {
        max-height: 75vh;
    }
}

/* ── Help-page modal ────────────────────────────────────── */

/* A help page is long-form prose in a modal, which is the problem the legal modals above already
   solve, so it gets the same treatment rather than a second mechanism: :has() widens ONLY this
   modal, and the height cap plus scroll keeps the end of a long page on screen instead of below
   the fold. The hook is the .help-page-content class HelpPageModal puts on its body. */
.help-page-content {
    /* Was an inline 1.2em on the wrapper, which .modal-body p then multiplied by its own 1.2em, so
       paragraphs rendered at ~1.44em while list items stayed at 1.2em and looked shrunken. The size
       moves here, and the two rules below flatten every child to it, so nothing compounds and a
       nested list cannot balloon. 1.44em kept paragraphs exactly the size they already were.

       IT READS THE MODAL PROSE TOKEN NOW, because a help page IS modal prose: it is served inside
       .modal-body like every other dialog, and 1.44em against the modal's 1.45em was two spellings of one
       decision that happened to land a tenth of a pixel apart (15.55px against 15.66px). A reader meeting
       a help page and a confirmation in the same session saw two sizes for no reason they could perceive,
       and raising one without the other would have turned that gap from pointless into visible. The
       children stay flattened to 1em OF THIS, so the no-compounding property above is unaffected. */
    font-size: var(--modal-prose-size);
    max-height: 65vh;
    overflow-y: auto;
    padding-right: 10px;

    /* A long token (a URL, a hyphen-free compound) would otherwise set a min-content width the
       modal cannot shrink below, and .modal clips horizontally rather than scrolling, so it would
       be lost off the right edge rather than reachable. */
    overflow-wrap: break-word;
}

/* Why the modal was cut off on the right on a phone, and it is NOT the dialog being too wide:
   below 576px Bootstrap gives .modal-dialog width:auto plus margins, so the max-width above is not
   even in play. The culprit is that .modal-dialog-centered is display:flex, which makes
   .modal-content a flex ITEM, and a flex item defaults to min-width:auto, meaning it refuses to
   shrink below its content's min-content width. Combine that with .modal's overflow-x:hidden and
   the excess is CLIPPED rather than scrollable, which is exactly how it presents. min-width:0 lets
   it shrink to the space available. Scoped to this modal so no other modal's layout moves. */
.modal-dialog:has(.help-page-content) .modal-content {
    min-width: 0;
}

/* The horizontal chrome stacks up on a narrow screen: dialog margin, the 10px .modal-content
   border, .modal-body's 1rem padding each side, this element's own padding, and a list's 2rem
   indent, before a word of prose is drawn. On a 375px phone that is over a quarter of the width,
   and the nested list on the Trading Allowance page pays it twice. */
@media (max-width: 575.98px) {
    .modal-dialog:has(.help-page-content) .modal-body {
        padding-left: 0.5rem;
        padding-right: 0.5rem;
    }

    .help-page-content {
        padding-right: 4px;
    }

    .help-page-content ul,
    .help-page-content ol {
        padding-left: 1.25rem;
    }
}

/* The .modal-body ancestor is named deliberately rather than for decoration: a bare
   .help-page-content p ties with .modal-body p at (0,2,0), so which one won would depend on source
   order, and moving either block would silently bring the compounding back. At (0,3,0) this wins
   wherever it sits in the file. */
.modal-body .help-page-content p,
.modal-body .help-page-content li,
.modal-body .help-page-content h3,
.modal-body .help-page-content h4 {
    font-size: 1em;
}

/* Headings keep their rank by weight, not by size, now that the wrapper carries the size. Leaving
   them out of the flattening above enlarged every heading on every page including the pre-existing
   ones (an h3 went 1.40em -> 1.68em), which is a change to signed-off pages nobody asked for. */
.modal-body .help-page-content h3,
.modal-body .help-page-content h4 {
    font-weight: bold;
    margin: 18px 0 6px 0;
}

.help-page-content li {
    margin: 6px 0;
}

.modal-dialog:has(.help-page-content) {
    max-width: 720px;
}

@media (min-width: 992px) {
    .modal-dialog:has(.help-page-content) {
        max-width: 920px;
    }

    .help-page-content {
        max-height: 75vh;
    }
}

/* ── Footer legal links (Razor Pages) ───────────────────── */

#footer-page .footer-legal-links {
    text-align: center;
    margin-top: 10px;
    font-size: 0.8em;
    opacity: 0.6;
}

#footer-page .footer-legal-links a {
    color: inherit;
    text-decoration: none;

    /* Detach the hover underline from the glyph descenders, so it reads as an affordance rather
       than as text decoration. Declared on the BASE rule, not the :hover one, so it governs the
       underline wherever it appears.

       0.25em is THE value for this job, not a preference: calculator.css states it as the offset
       "used consistently across all blue underline-on-hover elements ... to keep the pattern
       uniform", and .explore-breadcrumb-link above sits on these same server-rendered pages at
       0.25em pointing back at that sheet. This footer renders directly beneath that breadcrumb. */
    text-underline-offset: 0.25em;
}

#footer-page .footer-legal-links a:hover {
    text-decoration: underline;
}

#footer-page .footer-legal-links .footer-legal-sep {
    margin: 0 8px;
}

/* ── Showcase publish modal ─────────────────────────────── */

.showcase-publish-section {
    margin-bottom: 18px;
}

.showcase-publish-label {
    font-size: 1.2em;
    font-weight: bold;
    color: #555;
    margin-bottom: 8px;
}

.showcase-plan-summary {
    font-size: 1.1em;
    color: #555;
    margin-top: 5px;
}

.showcase-plan-summary-title {
    font-weight: bold;
    margin-top: 10px;
}

.showcase-plan-summary-description {
    color: #777;
    font-size: 0.95em;
}

.showcase-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
}

.showcase-tag {
    display: inline-block;
    background: var(--color-pill-action-shadow);
    color: #333;
    padding: 2px 10px;
    border-radius: 12px;
    font-size: 1em;
}

.showcase-update-notice {
    margin-top: 12px;
    padding: 10px 15px;
    background: var(--color-success-row-light);
    border-radius: 8px;
    font-size: 1.1em;
    color: var(--color-green-dark);
}

/* --------------------------------------------------------*/
/* SMALL SCREEN RESPONSIVE */
/* --------------------------------------------------------*/

@media (max-width: 576px) {
    .nav-tabs {
        flex-wrap: nowrap;
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: none;
        justify-content: flex-start !important;
    }

    .nav-tabs::-webkit-scrollbar {
        display: none;
    }

    .nav-tabs .nav-item {
        white-space: nowrap;
    }

    .nav-link {
        font-size: 1.1em;
        padding: 6px 8px;
    }

    /* BOTH TEXT LINES GO, leaving the silhouette alone (user, Sep 2026). Below this width the tab strip is
       nowrap with overflow-x scrolling (see .nav-tabs above), so every pixel the badge takes is a pixel the
       strip has to scroll for, and a lone silhouette is the affordance every other app uses for "your
       account" at this size.

       THE WARNING DOES NOT GO WITH THEM, which was the objection to hiding the tier and is the reason for
       the rule below rather than a reason not to. The tint moves onto the glyph, so an Anonymous visitor is
       still told on the surface where they are most likely to be casually anonymous. Both words are one tap
       away on /Profile.

       THE CONTAINER GOES, not just the two words in it (Sep 2026). Hiding only the words left an empty
       .identity-badge-lines in the flex row, still spending its 10px margin and the badge's 5px gap to the
       right of the silhouette: dead width on the one surface where the strip is scrolling for every pixel. */
    .identity-badge-lines {
        display: none;
    }

    /* Scaled with the tabs beside it. They drop to 1.1em here, so a badge left at the full baseline would
       tower over a strip that is already short of room.

       NO RIGHT MARGIN (user, Sep 2026): the badge is the last thing on the bar, so a margin there only
       pushed the silhouette off the screen edge and took width from the tab strip. The padding stays,
       since it is the tap target rather than spacing. */
    .identity-badge {
        font-size: 1em;
        padding: 6px 8px;
        margin-left: 6px;
        margin-right: 0;
    }

    .module-description {
        display: none;
    }

    .module-summary-box {
        padding: 2px 4px;
        margin: 3px 2px;
    }

    .module-title .module-name {
        font-size: 1.6em;
    }

    .module-side-column .fa-2x {
        font-size: 1.5em;
    }

    .module-side-column .fa-3x {
        font-size: 2em;
    }

    .checkbox-module-toggle {
        font-size: 1.3em;
    }

    .module-summary-number {
        font-size: 0.9em;
    }

    .module-summary-description {
        font-size: 0.75em;
        white-space: nowrap;
    }

    .module-body {
        overflow: hidden;
        min-width: 0;
    }

    .module-summary {
        flex-wrap: wrap;
    }

    .module-header {
        overflow: hidden;
    }

    .price-box {
        font-size: 1em;
        padding: 6px 8px;
        opacity: 0.85;
    }
}
/* Fluid table: shared padding/sizing on desktop, stacked cards on small screens. */
.fluid-table td {
    padding: 2px 10px;
    font-size: 1.2em;
}

.fluid-table td:first-child {
    font-weight: bold;
    font-size: 1.3em;
}

/* Movements table (/Results Year & Month): the first column (Source) is a module
   name like the Target column, so it keeps the same 1.2em size rather than the
   enlarged 1.3em the generic fluid-table first-column rule applies. The
   .movements-table.fluid-table pair (specificity 0,3,1) beats that rule. */
.movements-table.fluid-table td:first-child {
    font-size: 1.2em;
}

.arrow.arrow-placeholder {
    border-color: transparent;
    border-width: 0 0.8em 0.8em 0.8em;
}

.amount-with-change {
    white-space: nowrap;
}

@media (max-width: 768px) {
    .fluid-table {
        display: block;
        width: 100%;
        border-collapse: separate;
    }

    .fluid-table thead {
        display: none;
    }

    .fluid-table tbody,
    .fluid-table tr {
        display: block;
        width: 100%;
    }

    .fluid-table tr {
        margin-bottom: 10px;
        border: 1px solid #ddd;
        border-radius: 4px;
        padding: 4px 0;
    }

    .fluid-table td {
        display: flex;
        justify-content: space-between;
        align-items: center;
        text-align: right;
        padding: 4px 10px;
        border: none;
        border-bottom: 1px solid #eee;
        width: auto;
        /* Card mode gives each cell the full card width and its whole purpose is
           to avoid horizontal scroll, so a cell must never be nowrap here. Override
           .right-no-wrap's white-space:nowrap (0,1,0): this .fluid-table td (0,1,1)
           wins on specificity and source order, so a long "(reason) amount" value
           (e.g. "(Cannot be sold in time) 100,440") wraps at its spaces instead of
           overflowing the card. overflow-wrap/word-break add the fallback so an
           unbroken token with no spaces (a very long number or path) still breaks
           rather than overflowing, mirroring .sweeps-td[data-label="Target"]. A
           short bare value has no reason to break, so it stays on one line. */
        white-space: normal;
        overflow-wrap: anywhere;
        word-break: break-word;
    }

    .fluid-table tr td:last-child {
        border-bottom: none;
    }

    .fluid-table td:empty {
        display: none;
    }

    .fluid-table td[data-label]::before {
        content: attr(data-label);
        font-weight: 600;
        text-align: left;
        margin-right: 12px;
        color: #555;
        /* The td is a flex row carrying overflow-wrap:anywhere for its VALUE, and the ::before is a flex
           item in it that inherits both. So a long value shrank the label to its min-content width and
           broke it one letter per line: the Strategy tab's "Why" column, whose values are whole
           sentences, rendered as a W over an h over a y beside every long reason. A column name is short
           by construction and is the thing a reader scans down the card, so it neither shrinks nor
           breaks; the value wraps instead, which is what the anywhere was always for. */
        white-space: nowrap;
        flex: 0 0 auto;
    }

    /* Group-header rows (e.g. "Income Tax", "National Insurance" labels above
       each kind in the Tax Year attribution table) are title bars, not data
       rows — suppress the column-name prefix that would otherwise read as
       "Module: Income Tax". Same logic for empty padding cells. */
    .fluid-table tr.row-group-header td::before {
        content: none;
    }

    .fluid-table tr.row-group-header td {
        justify-content: flex-start;
        text-align: left;
        border-bottom: none;
    }
}

/* Admin Sessions tab — pulse animation for bi-directional timeline <-> events table link */
@keyframes admin-pulse-anim {
    0%   { box-shadow: 0 0 0 0 rgba(13, 110, 253, 0.7); filter: brightness(1.4); }
    100% { box-shadow: 0 0 0 12px rgba(13, 110, 253, 0);  filter: brightness(1); }
}

.admin-pulse {
    animation: admin-pulse-anim 1.0s ease-out;
}

/* Drop the motion-bearing animation for users who've asked for reduced motion.
   Substitute a static highlight so the orientation cue is preserved — the
   request is "no motion", not "no feedback". */
@media (prefers-reduced-motion: reduce) {
    .admin-pulse {
        animation: none;
        background-color: rgba(13, 110, 253, 0.15);
    }
}

/* Plan-navigation link affordance — emitted by PlanNavigationLinkComponent
   and TokenSequenceRenderer (see DF.Claude/Features/PlanNavigationLink.md).
   Inherits surrounding text size + colour so a link inside an item-info row
   renders smaller/lighter, one inside a header renders bigger. */
a.plan-nav-link {
    color: inherit;
    font-size: inherit;
    font-weight: inherit;
    text-decoration: underline;
    text-decoration-style: dotted;
    text-underline-offset: 0.25em;
    cursor: pointer;
}

a.plan-nav-link:hover {
    text-decoration-style: solid;
}

/* Brief gentle pulse applied to the navigated-to target element after the
   click cascade settles. Uses box-shadow (not background-color) so the
   animation isn't overridden by the target div's existing stripe background:
   .module-block.odd/even have specificity 0,2,0 which beats this single-class
   rule (0,1,0). Sibling to .admin-pulse so the two surfaces can be tuned
   independently. */
@keyframes plan-nav-link-flash-anim {
    0%   { box-shadow: 0 0 0 4px rgba(13, 110, 253, 0.75); filter: brightness(1.2);  }
    65%  { box-shadow: 0 0 0 4px rgba(13, 110, 253, 0.75); filter: brightness(1.15); }
    100% { box-shadow: 0 0 0 12px rgba(13, 110, 253, 0);   filter: brightness(1);    }
}

/* Hold a solid blue ring for the first ~65% (~1.6s) so the user has time to spot
   the target after a cross-page jump, then expand + fade over the remainder. The
   JS strips the class after a matching timeout, so keep the two in sync. Used by
   the link-navigation jump, where the longer hold helps locate the target. */
.plan-nav-link-flash {
    animation: plan-nav-link-flash-anim 2.5s ease-out;
}

/* Shorter sibling for the module-add highlight: the user is already looking at the
   module they just created, so a full 2.5s hold reads as sluggish. Same keyframe,
   a quick 0.6s pulse rather than a lingering hold. Keep in sync with the JS strip
   timeout (flashStripMs). */
.plan-nav-link-flash-short {
    animation: plan-nav-link-flash-anim 0.6s ease-out;
}

@media (prefers-reduced-motion: reduce) {
    .plan-nav-link-flash,
    .plan-nav-link-flash-short {
        animation: none;
        box-shadow: 0 0 0 4px rgba(13, 110, 253, 0.35);
    }
}

.alert-warning {
    font-size: 1.3em;
}

/* Observations panel (Analytics/Observations) */
/* Pills laid out as a wrapping flex row at the top of the Analysis page.
   Palette mirrors .contingency-header.warning / .information so the panel
   reads consistently with the existing GetAdditionalInfo boxes elsewhere. */
.observations-panel {
    margin: 30px 0 80px 0;
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: flex-start;
    justify-content: center;
}

.observations-panel-pricing {
    align-items: center;
    justify-content: center;
    padding: 16px 0;
}

.observation-pill {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 10px 14px;
    border-radius: 12px;
    border: 1px solid;
    line-height: 1.35;
    flex: 0 0 320px;
    max-width: 320px;
    text-align: center;
}

.observation-pill.warning {
    color: var(--color-black);
    border-color: var(--color-red);
    background-color: var(--color-pill-error-light);
}

.observation-pill.warning .observation-title {
    color: var(--color-red);
}

.observation-pill.information {
    color: var(--color-black);
    border-color: var(--color-black);
    background-color: var(--color-pill-neutral-light);
}

.observation-pill .observation-title {
    font-weight: 700;
    font-size: 1.3em;
}

.observation-pill .observation-explanation {
    font-size: 1.2em;
    opacity: 0.85;
}

/* Sweep name link in the table: blue, no underline by default,
   underlined on hover. Same colour as the global anchor so the link
   reads as clickable without the constant underline visual weight. */
.sweep-name-link {
    color: #1f77b4;
    text-decoration: none;
}

.sweep-name-link:hover {
    text-decoration: underline;
}

/* Sweeps table — base sizing on desktop, then card-stack on phones. */

/* Cell padding — Bootstrap 5.x hardcodes `padding: .5rem .5rem` on
   `.table>:not(caption)>*>*` (it does NOT use --bs-table-cell-padding-*
   variables in this build), so we have to override the same selector
   shape with higher specificity by chaining `.sweeps-table.table` instead
   of just `.table`. (0, 2, 1) > Bootstrap's (0, 1, 1). */
.sweeps-table.table > :not(caption) > * > * {
    padding: 10px;
}

/* Actions cell: separate the Run button from the Clone / Delete icon cluster that
   follows it. Scoped to .sweeps-actions (the row's cell) so the editor's own Run
   button, which is not in the table, is untouched. Deliberately NOT keyed on "Run"
   specifically: this is the gap before the icon cluster, so a second button added
   to this cell should inherit it rather than sit flush against the icons.

   The ~720px block further down takes it back off where it would be wrong (a
   read-only plan draws no Actions cell at all), which is why it is a margin and
   not a flex gap on the cell: the cell is a real <td>, and
   making it display: flex on desktop would drop it out of the table layout
   algorithm and break column alignment (the ~720px block does exactly that, but
   only because it deliberately collapses the table into cards). */
.sweeps-actions > .btn {
    margin-right: 10px;
}

/* Override Bootstrap's default striped color so the alternating row tint
   matches the light gray used by the results details-table (var(--color-row-stripe-even)). */
.sweeps-table.table-striped > tbody > tr:nth-of-type(odd) > * {
    --bs-table-striped-bg: var(--color-row-stripe-even);
    background-color: var(--color-row-stripe-even);
    color: inherit;
}

/* Active-editing row: thick black border around all cells so the user
   can see at a glance which sweep the editor below is showing. We use
   box-shadow on the cells (rather than border on the row) because tables
   collapse borders unpredictably across browsers, and box-shadow paints
   on top without pushing layout — first/last cells get rounded outer
   corners so the highlight reads as one continuous frame.
   Background-colour is also overridden here (alice-blue) and the
   selector chains both .sweeps-table and .table-striped so the
   specificity wins over the odd-row striping rule above. */
.sweeps-table.table-striped > tbody > tr.sweeps-row-editing > td {
    background-color: var(--color-pill-action-light);
    box-shadow:
        inset 0 1px 0 #000,
        inset 0 -1px 0 #000;
}

.sweeps-table.table-striped > tbody > tr.sweeps-row-editing > td:first-child {
    background-color: var(--color-pill-action-light);
    box-shadow:
        inset 0 1px 0 #000,
        inset 0 -1px 0 #000,
        inset 1px 0 0 #000;
}

.sweeps-table.table-striped > tbody > tr.sweeps-row-editing > td:last-child {
    background-color: var(--color-pill-action-light);
    box-shadow:
        inset 0 1px 0 #000,
        inset 0 -1px 0 #000,
        inset -1px 0 0 #000;
}

/* Header: white font on black, uniform with the app's other data tables. Chains
   .sweeps-table.table to beat Bootstrap's default thead cell styling. */
.sweeps-table.table > thead > tr > th.sweeps-th {
    background-color: #000;
    color: #fff;
}

.sweeps-th {
    font-size: 1.5em;
    vertical-align: middle;
    text-align: center;
}

/* A data cell that selects its sweep on click (every column except Actions on a
   non-selected row) shows a hand cursor. */
.sweeps-selectable-cell {
    cursor: pointer;
}

/* Hovering a non-selected row tints the whole row the same light blue a selected
   (editing) row uses, so the click affordance reads at a glance. */
.sweeps-table.table-striped > tbody > tr:not(.sweeps-row-editing):hover > td {
    background-color: var(--color-pill-action-light);
}

.sweeps-td {
    font-size: 1.2em;
    vertical-align: middle;
    text-align: center;
}

/* Cap the Target column width so a deep target path doesn't squeeze the other
   columns. The text wraps inside its cell with overflow-wrap: anywhere so even
   path-like strings break onto a new line. */
.sweeps-td[data-label="Target"] {
    max-width: 220px;
    overflow-wrap: anywhere;
    word-break: break-word;
}

/* Status column: left-align the cell content (the coloured dot + label) so the
   status dots line up flush at the left edge of the column. The "Status" header
   stays centered like every other header (.sweeps-th above). */
.sweeps-td[data-label="Status"] {
    text-align: left;
}

/* On screens narrower than ~720px, collapse the table into a stack of
   "cards" — one per sweep — so columns reflow vertically instead of
   forcing horizontal scroll. Each cell shows its column name as a
   prefix from the data-label attribute the C# code attaches. */
@media (max-width: 720px) {
    .sweeps-th { display: none; }

    .table.table-striped {
        border: 0;
    }

    /* In card-stack mode every cell is full-width, so the desktop
       Name / Target column-width cap (max-width: 220px above) must be
       lifted — otherwise long sweep / target names render in a narrow
       column with empty space to the right of them, while the other
       cells go edge-to-edge. */
    .sweeps-td[data-label="Name"],
    .sweeps-td[data-label="Target"] {
        max-width: none;
    }

    .sweeps-td {
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 12px;
        padding: 6px 8px;
        border-top: 0;
    }

    .sweeps-td::before {
        content: attr(data-label);
        font-weight: 700;
        color: #555;
        flex-shrink: 0;
    }

    /* The card-stack cell is a flex row with its own 12px gap, so the desktop
       margin would compound on one side only (22px before the Clone icon against
       12px before Delete). The gap already does the separating here. */
    .sweeps-actions > .btn {
        margin-right: 0;
    }

    .table-responsive tbody tr {
        display: block;
        margin-bottom: 12px;
        border: 1px solid #ddd;
        border-radius: 6px;
        background: white;
    }

    .table-responsive {
        overflow-x: visible;
    }
}

/* Run-blocking overlay z-index band, shared by every run surface (Sweeps, Cones,
   Suggestions) so the three cannot drift apart, the bug that left Cones/Suggestions
   at 1000/1001, BELOW the sticky nav, while Sweeps sat at 1040/1050 above it.
   The backdrop MUST exceed the sticky top nav (Bootstrap .sticky-top, z-index 1020)
   or the nav stays clickable + un-dimmed mid-run; the lifted "live" card must exceed
   the backdrop. (The Wizard sits higher still, .app-backdrop-overlay-card at 9500,
   because it must also clear open modals / dropdowns.) */
:root {
    --run-backdrop-z: 1040;
    --run-foreground-z: 1050;
}

/* Shared full-viewport "work in progress" backdrop. Used by the Wizard, the
   Sweep run, and the Cones run to dim + blur everything behind a foreground
   card that owns the user's attention. Visual treatment is identical across
   the three sites; only the z-index and any layout extras (wizard's flex +
   overflow scroll-container) live on the per-site modifier classes.
   IMPORTANT: this class sets NO z-index. Adopt it only paired with a
   z-index-setting modifier (`.run-backdrop` for a run surface, or
   `.app-backdrop-overlay-card` for the wizard). Without one, the backdrop
   inherits the default stacking-context level and sits behind page content
   — the "block the rest of the page" effect won't fire. */
.app-backdrop {
    position: fixed;
    inset: 0;
    background-color: rgba(255, 255, 255, 0.4);
    backdrop-filter: blur(8px);
}

/* Shared run-blocking backdrop modifier. Pairs with .app-backdrop; carries only
   the z-index (see :root band above). Used by every run surface (Sweeps, Cones,
   Suggestions) so the dim / blur / click-block behind the run modal is identical. */
.run-backdrop {
    z-index: var(--run-backdrop-z);
}

/* Shared "run in progress" modal card. ONE fixed, top-anchored, centred card used
   by all three run surfaces, so the running view sits at the same viewport position
   everywhere (previously Sweeps floated at the top while Cones / Suggestions lifted
   in-flow and sat lower). Fixed (not in-flow) keeps it scroll-independent and above
   the sticky nav; max-height + overflow lets a tall run view (the Cones hero cone)
   scroll inside the card rather than spill off a short viewport. */
.run-modal-card {
    position: fixed;
    top: 16px;
    left: 50%;
    transform: translateX(-50%);
    z-index: var(--run-foreground-z);
    width: calc(100% - 32px);
    max-width: 760px;
    max-height: calc(100vh - 32px);
    overflow-y: auto;
    background: white;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
}

/* Cones tab — Monte Carlo banded ribbon chart with streaming snapshots.
   Three states: Empty (skeleton chart + explainer), Running (progress overlay
   + Cancel), Cached (live bands + Recompute). Pure inline-SVG chart via
   BlazorRenderer — no external charting dependency. */
.cones-tab {
    max-width: 1100px;
    margin: 0 auto;
}

/* During a run the Cones root renders as the shared .run-modal-card (not .cones-tab),
   and the backdrop as .app-backdrop .run-backdrop — see those shared rules above. */

/* Overlay-card composition — adds a higher z-index (9500, see below; raised from within
   Design / other tabs while modals or dropdowns may be open) plus the flex +
   overflow layout that hosts a central scrollable card. The wizard uses this;
   any future overlay-card surface (e.g. a future full-page settings panel)
   should adopt the same composition rather than inlining the rules.
   Five declarations as a bundle — z-index + scroll-container layout — so the
   name describes the composition, not just the overflow behaviour. */
.app-backdrop-overlay-card {
    /* 9500: above the fixed price box, its charts and the floating add-module button (all 9000), which drew
       OVER the wizard on every screen until Sep 2026 (a stale "Final net worth GBP 0", since pricing is
       suspended while the wizard is open); below the tooltip / popover layers (9998+), which must still show
       inside it. */
    z-index: 9500;
    overflow-y: auto;
    display: flex;
    justify-content: center;
    align-items: flex-start;
}

/* Shared run-progress header + bar (Sweeps / Cones / Suggestions), rendered by RunProgressRenderer:
   a centred headline over a 600px gray track + blue fill. One markup + look, replacing the three
   per-feature copies (Sweeps' inline styles + the old .cones-progress-* / .suggestions-progress-*
   classes) so the running progress cannot drift. */
.run-progress-header {
    text-align: center;
    margin: 8px 0 18px;
}

.run-progress-track {
    width: 600px;
    max-width: 100%;
    height: 14px;
    background: #eee;
    border: 1px solid #ccc;
    border-radius: 3px;
    overflow: hidden;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    margin: 0 auto 18px;
}

.run-progress-fill {
    height: 100%;
    background: #1f77b4;
    transition: width 0.2s;
}

/* ─────────────────────────────────────────────────────────────────────────
   Suggestions page ("explore suggestions"): the ranked result pills, the run
   progress popup, and the pre-apply confirm card. Mirrors the Cones
   running-lift + progress-bar treatment.
   ──────────────────────────────────────────────────────────────────────── */
.suggestions-page {
    max-width: 900px;
    margin: 0 auto;
}

/* During a run the Suggestions root renders as the shared .run-modal-card (not
   .suggestions-page), and the backdrop as .app-backdrop .run-backdrop — see above. */

.suggestions-running {
    text-align: center;
}

/* Best-so-far, shown under the progress bar while a run streams in: the single leading result, so it
   is the focal point of the run and rendered spaciously as a light card, larger than a result pill. */
.suggestions-best-so-far {
    margin: 0 auto 18px;
    max-width: 600px;
    padding: 20px 28px;
    background: #f8f9fa;
    border: 1px solid #e9ecef;
    border-radius: 10px;
    text-align: center;
}

.suggestions-best-so-far-label {
    color: #888;
    font-size: 1.1em;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    margin-bottom: 8px;
}

.suggestions-best-so-far-title {
    font-weight: 700;
    font-size: 1.6em;
    line-height: 1.25;
    color: #212529;
    margin-bottom: 8px;
}

.suggestions-best-so-far-impact {
    font-size: 1.35em;
    color: #212529;
}

.suggestions-best-so-far-impact b {
    color: var(--color-blue);
}

.suggestions-actions {
    display: flex;
    justify-content: center;
    margin: 18px 0;
}

/* Result pill: a bordered card. Only positive (clean) suggestions ever show here, so the
   styling is black and white; the first (best-ranked) pill gets a slightly darker border. */
/* Tiled results: cards up to 400px wide, wrapping and centred so a partial row sits
   half-centred (long lines are hard to read across a wide stretch). */
.suggestions-pills {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: flex-start;
    gap: 16px;
}

/* Card background + border match a collapsed module header card (.module.module-view):
   #BBBBBB border, --color-surface-light background, no shadow. */
.suggestions-pill {
    flex: 0 1 400px;
    max-width: 400px;
    border: 1px solid #BBBBBB;
    border-radius: 8px;
    padding: 20px 24px;
    background-color: var(--color-surface-light);
    font-size: 1.15em;
}

/* The tier stripe: an edge-to-edge coloured band at the very top of the card (negative margins pull it
   out to the card edges past the pill's own padding), carrying the bulb glyph + the tier label. The tier
   is signalled HERE, by the stripe's colour + label, so no icon competes beside the title and the inline
   pill keeps the bulb unambiguously. */
.suggestions-pill-tier {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    margin: -20px -24px 14px;
    padding: 12px 12px;
    border-radius: 7px 7px 0 0;
    /* currentColor = the tier's own text colour, so the divider is green under an Optimisation stripe
       and amber under a Restructuring stripe, never a green line under amber. */
    border-bottom: 1px solid currentColor;
    font-weight: 700;
    font-size: 1.4em;
}

.suggestions-pill-tier-icon {
    width: 20px;
    height: 20px;
    flex: 0 0 auto;
}

.suggestions-pill-tier-label {
    line-height: 1;
}

/* Optimisation: green, a free no-downside win. */
.suggestions-pill-tier-optimise {
    background-color: var(--color-green-light);
    color: var(--color-green-dark);
}

/* Restructuring: amber, a fix that costs something (amber reads as "weigh this", not the alarm of red). */
.suggestions-pill-tier-restructure {
    background-color: var(--color-banner-warning-light);
    color: var(--color-banner-warning-dark);
}

/* The recommended card pops via a crisp dark ring (a box-shadow, so the card box size is unchanged and
   it never nudges its siblings out of the flex row). */
.suggestions-pill-best {
    border-color: #212529;
    box-shadow: 0 0 0 1px #212529;
}

/* The best-next-move endorsement. Tier-neutral (dark, not green/amber) so it reads as "do this one"
   regardless of the card's tier stripe. Centred below the stripe, above the title. */
.suggestions-pill-top {
    display: block;
    width: fit-content;
    margin: 0 auto 10px;
    padding: 6px 12px;
    border-radius: 12px;
    background-color: #212529;
    color: #fff;
    font-size: 1.1em;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

/* Header: the imperative headline, centred (the bulb now lives in the tier stripe above). */
.suggestions-pill-header {
    display: flex;
    align-items: center;
    justify-content: center;
    margin-bottom: 6px;
}

/* Title: the short imperative headline, bold and centred. text-align keeps a
   title that wraps to two lines centred, since the flex header only centres the
   title group, not the wrapped text inside the title span. */
.suggestions-pill-title {
    font-weight: 700;
    font-size: 1.35em;
    color: #212529;
    text-align: center;
}

/* Impact: the £ figure, centred and prominent, below the title. */
.suggestions-pill-impact {
    text-align: center;
    color: #212529;
    font-size: 1.2em;
    margin-top: 15px;
    margin-bottom: 15px;
}

/* The emphasised money in the impact renders blue, matching the price box (a positive net
   worth is blue there). The only <b> in the impact string is the £ amount. */
.suggestions-pill-impact b {
    color: var(--color-blue);
}

/* The green solvency key phrase ("Insolvency avoided" / "Insolvency delayed"). One coloured key token per
   outcome line: the phrase is green (matching the goals-gained check), any continuation after it (e.g.
   " by 3 years and 2 months") stays black via the container's colour. */
.suggestions-pill-solvency {
    color: var(--color-green-dark);
    font-weight: 600;
}

/* Cured shows a solvency line above a money line; collapse the doubled 15px margins into one normal gap. */
.suggestions-pill-impact + .suggestions-pill-impact {
    margin-top: 0;
}

/* The named goal-cost block, between the impact headline and the rationale. One line per goal;
   the check / cross verdict icon carries its own baked-in colour (green gained, dark-red given-up), the
   goal name stays near-black. Only the gained (check) lines appear today, clean-tier suggestions regress
   no goal; the given-up (cross) lines light up once the structural tier ships. */
.suggestions-pill-goals {
    text-align: center;
    margin-bottom: 14px;
}

.suggestions-goal {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 6px;
}

/* The gained / lost glyph: the Design page's own goal-verdict icons, the green Check.svg and the dark-red
   Cross.svg (GoalComponent's accent-verdict-badge), each rendered at its baked-in colour. Sized small so it
   sits inline with the goal name; the gained / lost class is only a marker, the SVG carries the colour. */
.suggestions-goal-icon {
    width: 1.1em;
    height: 1.1em;
    flex-shrink: 0;
}

.suggestions-goal-name {
    color: #212529;
}

/* Description: the advisor "why", centred below the impact. */
.suggestions-pill-rationale {
    text-align: center;
    color: #444;
    margin-bottom: 14px;
}

/* Always-open "what will change" section, separated from the headline above. */
.suggestions-pill-changes-section {
    margin-top: 14px;
    padding-top: 14px;
    border-top: 1px solid #e9ecef;
}

.suggestions-pill-changes-title {
    font-weight: 600;
    font-size: 1.1em;
    text-align: center;
    margin-bottom: 6px;
}

.suggestions-pill-changes {
    margin: 0 0 16px 1.2em;
}

/* Status line for an applied / reverted card ("The changes have been applied. +£X"), centred,
   replacing the impact + rationale + changes detail once the card has been acted on. */
.suggestions-pill-status {
    text-align: center;
    color: #212529;
    font-size: 1.1em;
    margin: 12px 0 16px 0;
}

.suggestions-pill-status b {
    color: var(--color-blue);
}

/* Action row: a primary (filled) button beside a secondary (ghost) button, centred. The page skin
   of the inline pill's "Apply the changes | Ignore" link pair. */
.suggestions-pill-actions {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}

.suggestions-card-btn {
    min-width: 96px;
}

/* Ghost secondary (Ignore / Accept and close): the same weight as the primary but hollow, so the
   two read as a pair without a second filled call-to-action competing with the primary. */
.suggestions-card-btn-ghost {
    background-color: transparent;
    border: 1px solid var(--color-blue);
    color: var(--color-blue);
}

.suggestions-card-btn-ghost:hover:not(:disabled) {
    background-color: var(--color-blue);
    color: #fff;
}

/* Grayed sibling: a card whose impact was scored against the pre-apply plan, so it is dimmed and its
   actions disabled until the applied card is reverted or a fresh run re-scores. */
.suggestions-pill-stale {
    opacity: 0.5;
}

.suggestions-pill-stale .suggestions-card-btn {
    cursor: not-allowed;
}

/* The "run again to refresh" note shown below the cards while a change stands applied / accepted. */
.suggestions-refresh-note {
    margin-top: 18px;
}

/* Price box on the Suggestions page: centred above the pills / empty state, so an apply's
   "last change" confirms the promised impact. */
.suggestions-price-box {
    display: flex;
    justify-content: center;
    margin-bottom: 12px;
}

/* Chart legend / path count, rendered below the svg (centred). */
/* Band-percentile key area, shown once above the tab bar (global, identical for every tab), not
   under each cone. The wrapper carries the spacing + centring; the inner lines sit tight. */
.cones-bands {
    text-align: center;
    margin: 20px 0 16px 0;
}

.cones-bands-key {
    color: #000;
    font-size: 1.4em;
}

/* Each key word wears a sample of the line it names: a top border in that edge's colour, which comes
   from the inline style since it is per-part. Every part is solid; the worse edge was dashed until Aug
   2026, when the dash was removed as a product decision (band direction is now colour-only, recorded as
   a known gap in MontecarloCones.md). Padding separates the word from its own rule; the top margin
   keeps the rule off the line above.

   TOP border rather than underline: a coloured underlined word reads as a hyperlink, and this key
   sits on a page carrying real plan-navigation links. */
/* The horizontal margin is the ONLY thing separating one part from the next: the interpuncts that
   used to do it are gone, because each part's own coloured rule already delimits it and the dot was
   a second delimiter doing the first one's job. Generous, so the four rules read as four samples
   rather than one broken line. */
.cones-bands-key-part {
    display: inline-block;
    padding-top: 2px;
    margin: 4px 18px 0 18px;
}

/* The fifth item on the key's one line, evenly spaced with the four outcome parts but deliberately
   NOT one of them: no coloured rule, grey rather than an outcome colour, and not bold.

   The transparent top border is not decoration, it reserves the same 3px the parts spend on their
   coloured rule, so all five sit on one baseline without a hand-tuned offset that would drift the
   day the rule's width changes. Margins match .cones-bands-key-part exactly, which is what makes
   the spacing even. */
.cones-bands-width {
    display: inline-block;
    border-top: 3px solid transparent;
    padding-top: 2px;
    margin: 4px 18px 0 18px;
    font-size: 1em;
    color: #444;
}

/* The (i) beside the band-width statement, whose popup explains what a percentile is and that a tenth
   of the scenarios land outside each edge.

   Sized and centred exactly like the Assumptions table's icon (.label-tooltip-host), and for the same
   reason: img.help-icon is 1.2em, so inside this 1.4em key it would resolve to ~27px and dwarf the
   sentence it annotates. Fixed at 13px instead, so the icon stays a small annotation whatever the key's
   size is retuned to.

   The icon's own div must be inline, not the block it is by default: it sits INSIDE an inline-block
   span, where a block child would break the line. inline-flex rather than inline-block so the glyph
   centres in its circle. The container stays inline-block deliberately, so the sentence's own spaces
   survive; making IT the flex container would turn each text run into an anonymous flex item and
   collapse "Bands are p10 to p90" into one unspaced run.

   div.help-icon's own margin-left: 5px is the gap before the icon, and is already the right side for a
   trailing icon, so nothing here restates it. */
.cones-bands-width div.help-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    vertical-align: middle;
    max-height: 21px;
}

.cones-bands-width img.help-icon {
    height: 13px;
}

/* The run's outcome as a proportional rule at the top of every tab, above the scalar pills: green for
   the share of scenarios that stay solvent, red for the share that run out of money.

   Thin on purpose. Zero is the common case, so a full-height green banner would make "nothing is
   wrong" the loudest thing on the page of every solvent plan; a hairline is quiet at 0% and
   impossible to miss at 34%. Colours come from the inline style so ChartPalette stays the one source
   of colour truth, and they are the SAME green and red the band edges use, so the page says "green is
   better" in one voice. */
/* Bounded and centred rather than full-bleed. A rule spanning the whole content area reads as a
   divider between the tab bar and the content; at 500px it reads as an object with a label, which is
   what it is. The top margin is what separates it from the tab strip above, so it belongs to the tab
   the reader just clicked rather than looking like part of the strip. */
.cones-outcome-bar {
    max-width: 500px;
    margin: 26px auto 18px auto;
}

/* 11px, not 9px, because Bootstrap's reboot sets box-sizing: border-box globally: the 1px border eats
   INTO the height rather than adding to it, so a bordered 9px track would leave 7px of colour and
   read thinner than the unbordered one it replaced. The border is what stops a pale green rule
   dissolving into the page background at 0%, which is the state it spends most of its life in. */
.cones-outcome-bar-track {
    display: flex;
    height: 11px;
    border: 1px solid var(--color-black);
    border-radius: 5.5px;
    overflow: hidden;
}

.cones-outcome-bar-good {
    /* flex-basis is per-render (the solvent share) and set inline. */
    min-width: 0;
}

/* The floor that makes a small failure visible: without it a 0.1% share is a sub-pixel sliver on any
   realistic width, so the bar would say "all fine" about a run in which something went wrong. The
   green segment shrinks to pay for it (flex-shrink 1, set inline). Overstating a tiny share is the
   safe direction, because the label below carries the exact figure.

   The 1% geometric floor in OutcomeBarGeometry is what actually does this; the min-width is its
   belt-and-braces at a narrow viewport, where 1% of a squeezed track is under a pixel again. */
.cones-outcome-bar-bad {
    flex: 1 1 auto;
    min-width: 3px;
}

/* Sized to match a cone tile's caption title (.cones-chart-block .chart-caption-title, 1.21em), so
   the bar's label and the chart titles below it read as one level of the page rather than two. */
.cones-outcome-bar-label {
    margin-top: 8px;
    text-align: center;
    font-size: 1.21em;
    color: #444;
}

/* Only when something actually failed. A bold black "0% of these scenarios run out of money" would
   report the best possible outcome as a warning. */
.cones-outcome-bar-label-alarming {
    font-weight: 700;
    color: var(--color-black);
}

/* One centred row of caution pills ("do not trust what you are looking at"), side by side when a view
   has more than one. Wraps on a narrow screen rather than squeezing, so a three-pill row degrades to
   a stack instead of unreadable slivers. */
.notice-pill-row {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 10px;
    margin: 15px 0;
}

/* The pill itself. Fill is the price box's insolvency-badge yellow (--color-caution-strong, paired
   with black in ChartPalette), so a caution here and the insolvency badge there read as one object
   rather than two house styles.

   This replaced three separately-invented notices: a red cancelled note, an orange stale notice and
   an orange insolvency sentence. Orange on white is weak for prose, and the shape now carries the
   alarm that colour was being asked to carry, so the text can be plain black and stay legible.

   The #444 border is what stops a bare yellow block reading as a highlight rather than a badge; it
   is the same grey already used across this stylesheet for secondary rules and muted text. */
.notice-pill {
    background-color: var(--color-caution-strong);
    color: var(--color-black);
    border: 2px solid #444;
    border-radius: 10px;
    padding: 10px;
    font-weight: 700;
    /* rem, NOT em. A shared component must not size itself relative to whatever happens to contain
       it: at 1.4em the identical pill rendered visibly smaller on Sweeps than on Cones, because the
       two pages differ in base font size, and the tighter text made the (absolute) 10px padding look
       tighter too. rem pins it to the root so every surface gets the same pill. */
    font-size: 1.4rem;
    text-align: center;
}

/* Shared chart caption — a bold title and a muted description rendered directly below a
   chart. Used by both the Sweeps charts (SweepsComponent.DrawChart) and the Cones cones
   (ConesComponent.AddChartCaption). The centring is supplied by the chart wrapper
   (Sweeps' .info-text, Cones' .cones-chart-block), matching the existing Sweeps idiom
   rather than baking text-align into these generic classes. */
.chart-caption-title {
    font-weight: 700;
}

.chart-caption-description {
    color: #888;
}

/* The Cones tab strip reuses the shared .picker-add-tab* CSS (see AddTabBar), so no
   bespoke .cones-tab-bar / .cones-tab-button rules are needed here. */

.cones-chart-container {
    margin: 8px 0;
}

/* The scalar summary sits as a row at the top of the tab, above the cones. Each scalar group is
   a boxed "parent pill"; the row centres them (matching the centred band key + cones) and wraps
   on narrow screens. */
.cones-scalar-row {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 12px;
    margin-top: 24px;
    margin-bottom: 24px;
    font-size: 0.95em;
}

/* Each scalar group (Terminal NW, Peak NW, Probabilities) is its own boxed "parent pill":
   a rounded, bordered card holding a centred title above its percentile / mean child pills.
   flex 0 1 auto sizes each box to its content (the wider of the title or the pill row) and does
   NOT grow, so a box never stretches to fill the row (a single group would otherwise span the
   whole width); flex-shrink 1 still lets it narrow on small screens. */
.cones-scalar-group {
    flex: 0 1 auto;
    /* Nudge the whole box (title + pills) up a little; the children are all em-based so they
       scale together off this. */
    font-size: 1.1em;
    padding: 12px 14px;
    background: #f8f9fa;
    /* #bbb rather than the #dee2e6 it shipped with: against the near-white #f8f9fa fill the lighter
       grey barely read as an edge, so the group looked like a tinted patch rather than a box. */
    border: 1px solid #bbb;
    border-radius: 8px;
}

.cones-scalar-title {
    font-size: 1.2em;
    color: #333;
    font-weight: 600;
    margin-bottom: 10px;
    text-align: center;
}

.cones-pill-row {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    justify-content: center;
}

.cones-pill {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    min-width: 52px;
    padding: 4px 10px;
    background: #fff;
    /* 1.5px, not 1px: the distribution pills override border-color with their outcome tint
       (PillTint, set inline by AddPill), and at 1px the green / red edge was too thin to read as
       the signal it carries. The width stays here because only the colour is per-pill. */
    border: 1.5px solid #ccc;
    border-radius: 10px;
}

.cones-pill-label {
    font-size: 0.95em;
    color: #888;
    line-height: 1.25;
    margin-bottom: 3px;
}

.cones-pill-value {
    font-size: 1.1em;
    font-weight: 600;
    color: #222;
    font-variant-numeric: tabular-nums;
    line-height: 1.25;
}

.cones-scalar-empty {
    margin: 0;
    color: #888;
    font-style: italic;
}

/* One cone + its caption, stacked uniformly down the tab (Sweeps-style). No border /
   background — the chart sits flush on the page; text-align centres the caption (and the
   responsive svg, which also has margin: 0 auto) beneath the cone. */
.cones-chart-block {
    margin-top: 24px;
    text-align: center;
}

/* Match the Sweeps caption look. In Sweeps the caption divs are DOUBLE-nested in .info-text
   (an outer wrapper from AddEditor / AddRunModal, plus DrawChart's own inner one), each
   font-size: 1.1em, so they render at 1.1 * 1.1 = 1.21em, and pick up .info-text div {
   padding: 5px }. .cones-chart-block has neither, so the captions looked smaller and unpadded.
   Replicate the effective size + padding here, scoped to Cones, so the shared .chart-caption-*
   classes stay free of the em-compounding they'd cause inside Sweeps' .info-text. */
.cones-chart-block .chart-caption-title,
.cones-chart-block .chart-caption-description {
    font-size: 1.21em;
    padding: 5px;
}

/* Secondary charts tile below a full-width primary chart, two-per-row, packed CENTRED rather
   than spread to the row edges. Shared by both the Cones cones (the holdings decomposition) and
   the Sweeps charts. Flexbox + justify-content: center keeps a row's tiles close together with
   just the gap between them (a CSS grid's repeat(2, 1fr) instead centres each chart inside its
   own half-row cell, pushing them apart); it also centres a lone last tile, e.g. the third Cones
   holding. flex-wrap drops to one-per-row when two no longer fit. The gap handles inter-tile
   spacing, so tiles drop their own margin-top; the container carries the margin-top that
   separates it from the primary. */
.chart-secondary-grid {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 16px;
    margin-top: 24px;
}

/* Fixed-ish basis matching the secondary svg's 400px cap, so the caption text wraps within the
   tile rather than stretching the flex item to its (very wide) max-content. flex-grow 0 keeps
   the tiles at their natural width so they stay packed in the centre instead of stretching to
   the edges; flex-shrink 1 lets them narrow on small screens (replacing the old grid media
   query). Targets all direct children so it covers both tile types (Cones .cones-secondary-block
   and Sweeps .info-text). */
.chart-secondary-grid > * {
    flex: 0 1 400px;
}

/* Cones secondary tiles still carry .cones-chart-block (for the caption sizing scope), so reset
   that block's margin-top inside the grid; Sweeps tiles are plain .info-text and drop their
   margin-top inline. */
.cones-secondary-block {
    margin-top: 0;
}

/* Per-plan run-counter badge (Cones / Sweeps / Suggestions): a small muted tally shown at the top
   of each page once the user has run at least once. Rendered by RunCounterBadge. */
.run-counter-badge-row {
    text-align: center;
    margin: 20px 0 2px;
}

.run-counter-badge {
    display: inline-block;
    padding: 4px 8px;
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.05);
    color: #6c757d;
    font-size: 1.1em;
    font-weight: 500;
    white-space: nowrap;
}

.cones-actions {
    margin-top: 18px;
    text-align: center;
    /* Mirror .info-text's 1.1em so the button's own 1.1em compounds to the same effective
       size as the Sweeps "+ New sweep" button (which sits inside the 1.1em .info-text). */
    font-size: 1.1em;
}

/* Match the Sweeps "+ New sweep" button: btn-lg padding + font-size 1.1em, content-width
   (no min-width), centred by .cones-actions text-align. */
.cones-actions button {
    font-size: 1.1em;
}

/* Searchable text item (SearchableTextBoxComponent): typeahead dropdown. */
.searchable-input {
    width: 100%;
}

.searchable-suggestions {
    position: absolute;
    z-index: 1000;
    left: 0;
    right: 0;
    max-height: 22rem; /* ~10 rows before scrolling */
    overflow-y: auto;
    background: #fff;
    border: 1px solid #ced4da;
    border-top: none;
    border-radius: 0 0 4px 4px;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08);
}

.searchable-suggestion-row {
    padding: 6px 10px;
    cursor: pointer;
}

.searchable-suggestion-row:hover,
.searchable-suggestion-row-active {
    background: #eef4ff;
}

/* Data-export progress modal (GDPR right of access) */
.export-progress-track {
    width: 100%;
    height: 10px;
    background: #e9eef5;
    border-radius: 5px;
    overflow: hidden;
    margin: 0.8em 0 0.4em;
}

.export-progress-bar {
    height: 100%;
    background: #4a86e8;
    border-radius: 5px;
    transition: width 0.2s ease;
}

.export-progress-status {
    color: #6b7684;
    font-style: italic;
}

/* ─── ONBOARDING HINTS ─── */
/* First-run coaching boxes (coachmarks + signposts). A single 2px black border on a cornsilk background, in the
   same shape as the other bordered boxes; a plain black icon and black links. See DF.Claude/Features/OnboardingHints.md. */
.onboarding-hint {
    position: relative;
    max-width: 520px;
    /* 26px top/bottom clears the 22px triangle so it never overlaps the element it points at (e.g. the "Send us
       feedback" line the footer up-arrow points at). */
    margin: 26px auto;
    padding: 15px;
    border: 2px solid #000000;
    border-radius: 0.5em;
    background-color: cornsilk;
    /* rem (root-relative), not em, so the box is the same size in every host and never compounds with a larger
       ancestor font-size such as #footer-page's 1.4em (which was blowing the footer box up to ~1.8em). */
    font-size: 1.3rem;
    letter-spacing: normal;
}

.onboarding-hint-inner {
    display: flex;
    align-items: center;
    gap: 12px;
}

.onboarding-hint-icon {
    width: 40px;
    height: 40px;
    flex-shrink: 0;
}

.onboarding-hint-body {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 6px;
    text-align: center;
}

.onboarding-hint-title {
    font-weight: bold;
    font-size: 1.25em;
    margin-bottom: 10px;
    color: #000000;
}

.onboarding-hint-text {
    color: #333333;
}

.onboarding-hint-links {
    margin-top: 6px;
    font-size: 1.1em;
}

.onboarding-hint-links a {
    color: #000000;
    text-decoration: underline;
    text-decoration-style: dotted;
    text-underline-offset: 0.3em;
    cursor: pointer;
}

.onboarding-hint-links a:hover {
    text-decoration-style: solid;
}

.onboarding-hint-cta {
    display: inline;
    margin-left: 5px;
    font-weight: bold;
    color: #000000;
    text-decoration: underline;
    text-decoration-style: dotted;
    text-underline-offset: 0.3em;
    cursor: pointer;
    white-space: nowrap;
}

.onboarding-hint-cta:hover {
    text-decoration-style: solid;
}

.onboarding-hint-sep {
    color: #999999;
    margin: 0 4px;
}

.onboarding-hint.up::before {
    content: '';
    pointer-events: none;
    position: absolute;
    top: -22px;
    left: 50%;
    transform: translateX(-50%);
    width: 0;
    height: 0;
    border-left: 18px solid transparent;
    border-right: 18px solid transparent;
    border-bottom: 22px solid #000000;
}

.onboarding-hint.down::after {
    content: '';
    pointer-events: none;
    position: absolute;
    bottom: -22px;
    left: 50%;
    transform: translateX(-50%);
    width: 0;
    height: 0;
    border-left: 18px solid transparent;
    border-right: 18px solid transparent;
    border-top: 22px solid #000000;
}

.onboarding-hint.right::after {
    content: '';
    pointer-events: none;
    position: absolute;
    right: -22px;
    top: 50%;
    transform: translateY(-50%);
    width: 0;
    height: 0;
    border-top: 18px solid transparent;
    border-bottom: 18px solid transparent;
    border-left: 22px solid #000000;
}

/* ProjectMenu / PlanMenu sit on the top switcher bar and PlanWizard sits over the /Design wizard prompt.
   All three hosts left-align their own content (a flex-start action-bar, a flex-start banner), so these boxes
   left-align to sit over them instead of centering in the wide content area, and their triangle moves near the
   left edge to point at the switcher's dropdown / the prompt's first link rather than at box-centre.

   The 44px offset was tuned for the switcher bar and now serves two geometries: the prompt's links start further
   in (a 2px margin plus the .banner-box 15px inset) than the switcher's content, so the triangle lands inside the
   link text rather than at its leading edge. Both point at the link, which is what the triangle has to achieve,
   but a third host with a different inset needs its own offset rather than this one. */
.onboarding-hint.onboarding-hint-left {
    margin-left: 15px;
    margin-right: auto;
}

.onboarding-hint.onboarding-hint-left.up::before,
.onboarding-hint.onboarding-hint-left.down::after {
    left: 44px;
    transform: none;
}

/* There is no -right alignment. It existed for the wizard banner, which is left-aligned since Sep 2026; the rules
   were deleted with the OnboardingHintAlignment.Right member that selected them. Note .onboarding-hint.right
   above is a different thing entirely: the signpost's right-POINTING triangle, which is alive and in use. */

/* --------------------------------------------------------*/
/* TIMELINE */
/* --------------------------------------------------------*/
/* The /Timeline page: a plan's life story as one scrollable spine.
   See DF.Claude/Features/Timeline.md.

   The vertical axis is event INDEX rather than elapsed time; the .timeline-gap spacers
   are what stop that index spine lying about the quiet stretches. Rows carry a min-height
   for rhythm but are deliberately NOT a fixed height: a row shows its occurrence's
   message, and those run from nothing to several lines.

   The wealth ribbon lands later as a fourth grid column between the gutter and the icon,
   which is why the row is a grid rather than a flex row: each row's ribbon segment is then
   exactly as tall as its own row because grid makes it so, which is what lets the height
   vary at no cost. Adding it is one column in the template, not a re-layout. */

.timeline {
    display: flex;
    flex-direction: column;
    max-width: 860px;
    margin: 0 auto;
    padding: 10px 0 40px 0;
    /* One number scales the whole control: everything below sizes in em off this.
       The root anchor is 10.8px (html, body), so 1.55em is ~16.7px and the sub-lines
       land in the 12 to 14px band. Deliberately ABOVE the 1.2em (~13px) the nav tabs
       and the module cards use, because those are chrome around content whereas these
       rows ARE this page's content. At the 1.2em the cards use, the row title rendered
       at 10.8px, i.e. smaller than the nav above it. */
    font-size: 1.55em;

    /* ONE definition of the column template, consumed by both the row and the gap spacer below.
       They must stay identical or the spacer's dots leave the icons' axis. The wealth ribbon is the
       second column, added here, which is the single edit this variable exists to keep singular. */
    --timeline-columns: 118px 80px 52px 1fr;

    /* Every scrap of VERTICAL space between one entry and the next, named rather than inlined, because
       the wealth ribbon has to cancel exactly these to run continuously. A row's cell stretches to its
       CONTENT box, so each of these would otherwise show up as a white band across the river: 10px
       between two rows of one month group, and 22px at a month boundary. Declared here so the padding
       and the negative margin that undoes it are the same number by construction rather than by two
       authors agreeing; TestTimelineComponent.TheRibbonCancelsTheRowSpacing pins that they stay paired. */
    --timeline-row-pad-y: 5px;
    --timeline-gap-pad-y: 2px;
    --timeline-group-margin: 6px;
    --timeline-group-border: 1px;
    --timeline-group-pad-top: 10px;

    /* Each slice reaches one pixel INTO the next, so consecutive ones overlap instead of merely
       touching. Rows are sized in ems off a 10.8px root, so their boundaries land on fractional device
       pixels and each element's painted box rounds independently: touching exactly still leaves an
       occasional hairline of page white across the river. An overlap cannot be seen because the fills
       are opaque (see the ribbon colours) -- with the alpha they used to carry, this same overlap would
       have drawn a DARKER line instead, which is the bug it is now safe to fix. */
    --timeline-ribbon-overlap: 1px;
}

/* The four tracks are 250px of FIXED width plus 42px of gaps before the content column gets anything,
   and the ribbon added 80px of that. On a phone that is most of the viewport, so the titles wrap to
   ribbons of their own. Narrowed rather than dropped: the ribbon is the page's only picture of the
   plan's shape, and a column that vanishes on the device the page is most read on is not a column.
   One override, because the template is one variable -- which is the whole reason it is one. */
@media (max-width: 720px) {
    .timeline {
        --timeline-columns: 74px 44px 40px 1fr;
    }
}

.timeline-row {
    display: grid;
    grid-template-columns: var(--timeline-columns);
    align-items: center;
    gap: 14px;
    min-height: 70px;
    padding: var(--timeline-row-pad-y) 8px;
}

/* A month group opens with a hairline, so a run of rows sharing one date reads as
   one block without repeating the date on each. */
.timeline-row.timeline-group-start {
    border-top: var(--timeline-group-border) solid var(--color-surface-light);
    margin-top: var(--timeline-group-margin);
    padding-top: var(--timeline-group-pad-top);
}

.timeline-row:first-child.timeline-group-start {
    border-top: none;
    margin-top: 0;
}

.timeline-gutter {
    text-align: right;
    line-height: 1.25;
}

/* The AGE is the primary line and the date the secondary one, inverting what you would
   expect from a chronology. The page's subject is one person's life, and the reader relates
   to "aged 43" instantly where "Aug 2039" costs them a subtraction. The date is kept as the
   corroborating detail, not deleted, because a plan is also a calendar commitment. */
.timeline-age {
    font-weight: bold;
    font-size: 0.92em;
    white-space: nowrap;
}

.timeline-date {
    font-size: 0.76em;
    color: var(--color-gray-dark);
    white-space: nowrap;
    margin-top: 5px;
}

/* The DEFAULT chip is deliberately NEUTRAL, which reverses the design doc's original
   "inherit the module card's event pill" rule. On a card, the event pill's orange sits beside
   a red warning, a blue action and a green suggestion, so the colour discriminates within a
   set. Here every row is an event, so the same orange discriminates nothing while keeping its
   alarm connotation, and it spends the loudest hue on the baseline, leaving the milestone and
   insolvency rows below nothing to stand out against.

   The module's own icon is the identifier; colour is reserved for rows where it MEANS
   something. See DF.Claude/Features/Timeline.md. */
.timeline-chip {
    /* Centred in its column, so the chip's axis is the column's. Without this the 46px chip sits at
       the start of the 52px track (centre 23px) while the gap spacer's dots use margin:0 auto
       (centre 26px), and the spine visibly kinked by 3px at every quiet stretch, against the very
       claim the spacer's own comment makes. */
    justify-self: center;
    /* Anchors .timeline-subject-badge, which hangs off the chip's bottom-right corner. */
    position: relative;
    width: 46px;
    height: 46px;
    border-radius: 50%;
    /* 2px is the default; 3px is the deliberate exception. THE CRITERION IS WHOSE ROW IT IS: a chip
       goes heavy when it reports on something the USER declared (either goal chip) or on the plan
       failing (insolvency), and stays light when the app merely decided the row was interesting (the
       neutral default, and the milestone accent). Weight used to mean insolvency ALONE, on the argument
       that the one row which shouts needs the heaviest ring; that reservation was freed when insolvency
       took a black ring and a saturated fill, which distinguish it without help from weight.

       BLACK, not gray-dark. The grey ring read as a disabled or placeholder circle rather than a
       deliberate one, which is the wrong thing to say about the ordinary module events that make up most
       of the page: they are the life story, not the chrome around it. Black also gives the accented
       chips something to be accents AGAINST, where grey-on-white made blue and purple look like the only
       rows that had been finished. The quiet on this page comes from the fill (a near-white neutral)
       rather than from a faint outline. */
    border: 2px solid var(--color-black);
    background-color: var(--color-pill-neutral-light);
    display: flex;
    align-items: center;
    justify-content: center;
}

.timeline-icon {
    width: 26px;
    height: 26px;
}

/* The subject badge: a second, smaller glyph on the chip's corner                                */
/* saying WHICH child fired, for the modules whose one generic event puts the same module glyph   */
/* on a wedding and a funeral. Anchors on the chip, hence position: relative there.               */
/* White fill and a ring so it reads as a badge ON the chip rather than a smudge inside it, and   */
/* so it stays legible over the accented chip fills (milestone, goal, insolvency).                */
.timeline-subject-badge {
    position: absolute;
    right: -3px;
    bottom: -3px;
    width: 20px;
    height: 20px;
    padding: 2px;
    box-sizing: border-box;
    border-radius: 50%;
    border: 1px solid var(--color-black);
    /* Literal white: there is no --color-white token, and .contingency sets its own the same way.
       NOT --color-pill-neutral-light, which is the default chip's own fill, so the badge would
       vanish into the very chip it is meant to sit on. */
    background-color: white;
}

/* Colour earns its place here: a net-worth milestone is the one row a user came to find.
   Against the neutral default above it reads as an event of a different order. */
.timeline-chip-milestone {
    border-color: var(--color-money-in);
    background-color: var(--color-pill-action-light);
}

/* A moment the plan worked out: the day the last debt clears, and its siblings to come.

   PURPLE, and a fourth colour rather than reusing the milestone blue above, because the two derived
   families answer different questions: a rung is a QUANTITATIVE crossing ("your money passed a number"),
   these are qualitative life moments. Giving both one colour would say they were one idea.

   The pin glyph, not the hue, is what marks the class, which is what makes the blue/purple pair safe:
   they converge for a deuteranope (purple is blue plus red), and a reader who cannot separate them still
   has a pin against a chart icon. Same argument that lets green sit beside amber below.

   BORROWED TOKEN, stated so it is not a surprise: --color-pill-curiosity-* is named for the Tips
   Curiosity pill, so restyling Tips restyles this chip. Accepted rather than aliased, since an alias
   would break just as silently (it resolves to the same value) while reading as though it did not. If
   the two ever need to differ, the fix is a genuinely independent token, not an indirection.

   PillCuriosityDark at 7.3:1 rather than CategoricalPurple at 3.4:1, so this chip carries the same weight
   as the green (7.4:1) and the amber (5.5:1) it sits among rather than reading as the faint one; it also
   arrives with its own matched pale fill, so the dark-ring/pale-fill pair needs no new token. */
.timeline-chip-calculated {
    border-color: var(--color-pill-curiosity-dark);
    background-color: var(--color-pill-curiosity-light);
}

/* A goal the user set, reached. The one row on this page they personally asked to be told about, which
   is why it takes the third and last colour the page spends.

   The TICK carries the verdict, not the green: green and amber are a deuteranopia-confusable pair, so a
   page that leant on hue alone would say nothing to a reader who cannot separate them. With a tick and a
   cross doing the work, the colour is reinforcement and the meaning survives without it.

   GreenDark rather than Green, which is the house partner for the pale fill and, at 7.4:1 against
   Green's 3.8:1, sits at a comparable weight to the amber below rather than reading as the lighter of
   two chips that are meant to be peers. */
.timeline-chip-goal-reached {
    border-color: var(--color-green-dark);
    border-width: 3px;
    background-color: var(--color-green-pale);
}

/* A goal broken: a guardrail that fell, or a target reached too late.

   AMBER, DELIBERATELY NOT RED. A missed goal is disappointing rather than an emergency, so it must not
   borrow the register of the one row that shouts. The app's existing light-surface warning pair
   (.banner-box.warning) reads 5.5:1 on white, and pairs with GreenDark above at the same weight.

   Note the insolvency chip below no longer spends red either, so the only red left on this page is the
   ribbon's negative fill, meaning IN DEBT. That is a fact about net worth; insolvency is a fact about
   cash, and the two are genuinely different. */
.timeline-chip-goal-broken {
    border-color: var(--color-banner-warning-dark);
    border-width: 3px;
    background-color: var(--color-banner-warning-light);
}

/* Running out of money: the one row on the page that should shout, and the reason every ordinary row is
   neutral.

   YELLOW ON BLACK, matching the price box's insolvency badge, so the app says ONE thing about one fact
   on both surfaces. This reverses two earlier calls on this page, both recorded here because the
   arguments against were real and it is the black border that answers them.

   The first objection was contrast: CautionStrong #FFD400 is 1.4:1 on white, fainter than the neutral
   chips it has to beat, which is why the price box (surface #0D1021) can spend it and a white page
   cannot. A 3px BLACK border settles that -- the shape is defined at 21:1 and the yellow is read against
   the ring rather than against the page. ChartPalette documents CautionStrong's pair as Black for
   exactly this reason.

   The second was that the fill inverts this page's chip grammar (pale fill, coloured ring). It does, and
   that is now the point: the one row that should shout is the one row built differently. Yellow-on-black
   is also the universal hazard pairing, and unlike the red it replaces it separates by LIGHTNESS, so it
   survives any colour vision.

   No icon inversion here, unlike the solid-red disc this briefly was: the glyph is a black-fill SVG and
   black on #FFD400 is ~14:1, so it is already the most legible it can be. */
.timeline-chip-insolvency {
    border-color: var(--color-black);
    border-width: 3px;
    background-color: var(--color-caution-strong);
}

/* BLACK, not the red it was. With the chip now yellow-on-black there is no red left in the insolvency
   treatment for a red title to belong to, and a lone red word beside a yellow badge would read as a
   third signal rather than as part of the same one. Black at 21:1 is also simply the most emphatic a
   word can be on white; the weight comes from the chip and the rule, and the title stops competing. */
.timeline-row.timeline-insolvency .timeline-title {
    color: var(--color-black);
}

/* The two BOOKENDS, the plan's first and last day. They take a FILLED BLACK chip (its rule is below,
   beside the icon knock-out), and no HUE of their own: giving punctuation a colour is exactly what the
   neutral-default rule above exists to prevent, since a colour spent here is a colour the milestone and
   insolvency rows no longer stand out against. Black is not one, which is what makes the fill available.

   This comment used to say the bookends took the default chip "with no rule of their own", and it went
   on saying it for thirty lines above the rule that gave them one.

   An earlier cut dropped the chip to a bare hairline ring on the grounds that a bookend is not a moment.
   True, but it made them read as broken images rather than as quiet rows: on a spine of circles, the two
   rows missing a circle look like a rendering fault, not like restraint. The quiet comes from the title
   instead, which is where it costs nothing. */
.timeline-row.timeline-boundary .timeline-title {
    color: var(--color-gray-dark);
}

/* The title is inside a <b> for every kind, so the bookends unbold it here rather than the renderer
   branching on kind: one CSS rule beats a conditional in the render tree. */
.timeline-row.timeline-boundary .timeline-title b {
    font-weight: normal;
}

/* The bookend chip is FILLED black with its glyph knocked out white, so the plan's first and last day
   read as unmistakable punctuation: solid stops at each end of the spine, where every row between them
   is an outlined circle on white.

   Black is the one treatment available here that spends no COLOUR, which is what the neutral-default
   rule above is protecting: a hue given to punctuation is a hue the milestone, goal and insolvency rows
   no longer stand out against. It is also why this does not compete with the insolvency row, which is
   loud by being YELLOW on black rather than by being dark.

   brightness(0) then invert(1) rather than a bare invert(1): the first forces the glyph to pure black
   whatever it actually is, so the second lands on pure white for any icon, not only one already authored
   at #000000. The two bookend glyphs are, but a filter that depends on that is one sanitisation pass away
   from producing a grey flag nobody notices. */
.timeline-row.timeline-boundary .timeline-chip {
    background-color: var(--color-black);
    border-color: var(--color-black);
}

.timeline-row.timeline-boundary .timeline-chip .timeline-icon {
    filter: brightness(0) invert(1);
}

/* A forced rung (zero, 100k, a million) USED to take a heavier ring here. Removed, because 1px is the
   worst available amount of emphasis: too small to read as deliberate, large enough to notice, so a
   reader comparing "reached GBP 100k" against "reached GBP 200k" saw an inconsistency rather than a
   landmark. Reported as a bug, which is what a failed emphasis signal looks like.

   The rule beneath it was worse: `font-weight: bold` on `.timeline-title`, whose content is already
   inside a <b> for every kind (see the bookend rule above, which has to reach INTO that <b> to unbold
   it). So it changed nothing at all, and the entire visible weight of "hero" was that one pixel.

   Every ladder rung is a landmark already, the ladder emitting nothing else, so singling three of them
   out was a distinction without a difference. If it is ever wanted back it needs a signal a reader can
   name without a colour picker, not a pixel. */

.timeline-content {
    min-width: 0;
}

/* Line one of a row: "Property 1: Mortgage finished". The source and the title share it, so an
   ordinary row is two lines rather than three. */
.timeline-title {
    line-height: 1.35;
}

/* The source reads as the quieter half of the title line: same size, lighter colour, normal weight
   against the title's bold. Inline by necessity, since it sits inside the title's flow. When it is a
   link it also carries .plan-nav-link, whose dotted underline goes solid on hover; that rule
   inherits colour and size, so it picks these up rather than fighting them. */
.timeline-source {
    color: var(--color-gray-dark);
    font-weight: normal;
}

.timeline-message {
    font-size: 0.85em;
    margin-top: 2px;
    line-height: 1.35;
}

/* The gap spacer: deliberately quiet, and deliberately NOT a row height that scales
   with the gap. A proportional axis was rejected (whole screens of nothing), so the
   spacer states the elapsed years in words instead of in pixels. */
/* The SAME column template as .timeline-row, not a two-column variant, so the spacer's dots sit on
   the icons' axis and its label starts exactly under the row titles. A narrower template put the
   label under the icon column, half a column left of everything it sits between. */
.timeline-gap {
    display: grid;
    grid-template-columns: var(--timeline-columns);
    align-items: center;
    gap: 14px;
    min-height: 34px;
    padding: var(--timeline-gap-pad-y) 8px;
}

/* Deliberately smaller and paler than a row's chip: the spacer marks the spine continuing through a
   quiet stretch, so it must read as punctuation rather than as another moment. No chip ring for the
   same reason.

   The glyph's own viewBox keeps its wide horizontal margins rather than being trimmed to the ink (as
   SvgImageGuidelines 9b would normally have it): a vertical ellipsis is MEANT to be narrow, and
   trimming it to the dots would stretch three small dots into three fat circles at this render size. */
.timeline-gap-icon {
    width: 20px;
    height: 20px;
    margin: 0 auto;
    display: block;
    opacity: 0.35;
}

/* No rule of its own: the next row's .timeline-group-start hairline already draws one a few pixels
   below, and two horizontal lines that close together read as clutter rather than as structure. The
   spacer earns its separation from the extra vertical room and the smaller, paler type, not from a
   border. (It was italic until a review preferred it upright; the claim outlived the rule.) */
.timeline-gap-label {
    font-size: 0.78em;
    color: var(--color-gray-dark);
}

.timeline-empty {
    text-align: center;
    padding: 40px 20px;
    color: var(--color-gray-dark);
}

/* THE WEALTH RIBBON, the page's second column: net worth drawn down the page beside the life story, so
   every row hangs off the wealth the reader actually had at that moment. "Mortgage finished" means a
   different thing at GBP 40k than at GBP 900k, and without this the page cannot say which.

   The cell stretches to whatever height its row turns out to be, and the SVG inside it is a UNIT SQUARE
   with preserveAspectRatio="none", so the polygon coordinates are pure fractions and nothing has to
   measure anything. That is what lets a gap's slice TAPER: it is one polygon whose top edge sits at the
   wealth before the quiet stretch and whose bottom edge sits at the wealth after it.

   No axis, no labels, no ticks: the reader is meant to see SHAPE. That is also why the scale is linear
   and spans both signs at once (see RibbonScale) -- an unlabelled chart is the one kind where distorting
   the shape is indistinguishable from lying, so neither a log scale nor a per-side scale is allowed. */
.timeline-ribbon {
    align-self: stretch;
    display: flex;

    /* The river runs CONTINUOUSLY, so the cell bleeds back through the vertical spacing its own row
       adds. Without this the fill stops at each row's content box and every entry boundary shows as a
       white band: the ribbon reads as a stack of bars rather than as one shape, which is the whole
       thing it exists to be.

       Negative margins rather than removing the padding, because the padding is doing a real job for
       the three columns that carry text. And derived from the same variables the padding uses, so the
       two move together: a hand-typed -5px here would silently stop cancelling the moment the row's
       rhythm was retuned, and the symptom (thin white seams) is easy to look straight past. */
    margin-top: calc(-1 * var(--timeline-row-pad-y));
    margin-bottom: calc(-1 * var(--timeline-row-pad-y) - var(--timeline-ribbon-overlap));
}

/* A month boundary adds three more things above the row: its own larger top padding, the hairline, and
   the margin before it. The ribbon cancels all three and so passes IN FRONT of the hairline, which is
   deliberate: the divider organises the text columns, and letting it cut the river would put a white
   notch at every month group, which is the artefact this whole block removes. */
.timeline-row.timeline-group-start .timeline-ribbon {
    margin-top: calc(-1 * (var(--timeline-group-pad-top)
        + var(--timeline-group-border)
        + var(--timeline-group-margin)));
}

/* The first row has neither hairline nor margin (see below), so cancelling them would hang the ribbon
   off the top of the page by exactly their combined height. */
.timeline-row:first-child.timeline-group-start .timeline-ribbon {
    margin-top: calc(-1 * var(--timeline-group-pad-top));
}

.timeline-gap .timeline-ribbon {
    margin-top: calc(-1 * var(--timeline-gap-pad-y));
    margin-bottom: calc(-1 * var(--timeline-gap-pad-y) - var(--timeline-ribbon-overlap));
}

.timeline-ribbon-svg {
    width: 100%;
    height: 100%;
    display: block;
}

/* SOLID fills, never a fill plus opacity, and the reason is the seam it removes rather than taste.
   Each slice is its own shape, so consecutive ones ABUT: the boundary pixel row takes partial coverage
   from the slice above AND the slice below. At full opacity those two partial coverages sum to exactly
   the interior colour and the join is invisible. At 0.5 they instead composite one semi-transparent
   layer over another, so every boundary drew a faint darker line down the river, at every row, which is
   the artefact the negative-margin bleed above was wrongly blamed for.

   color-mix rather than a hand-picked lighter blue, so the ribbon still derives from the palette and a
   change to MoneyIn carries here. 50% with white is exactly what opacity 0.5 over this white page was
   computing, so the colour is unchanged and only the compositing is. The bare var() first is the
   fallback for anything without color-mix: too saturated, but a colour rather than the black an
   unparsed fill would default to. */
.timeline-ribbon-positive {
    fill: var(--color-money-in);
    fill: color-mix(in srgb, var(--color-money-in) 50%, white);
}

.timeline-ribbon-negative {
    fill: var(--color-money-out);
    fill: color-mix(in srgb, var(--color-money-out) 50%, white);
}

/* Drawn only when the plan ever went negative, so on a plan that never did there is no rule sitting at
   the column's left edge pretending to be a datum. vector-effect keeps it hairline-thin: the SVG is
   scaled non-uniformly to the cell, so a plain stroke-width would stretch with it. */
.timeline-ribbon-zero {
    stroke: var(--color-gray-dark);
    stroke-width: 1;
    vector-effect: non-scaling-stroke;
}

/* The hard rule under the insolvency row. Spans the whole rail (it is a flex child of .timeline, not a
   grid child of a row) because it separates two REGIONS of the page: drawn inside the content column it
   would read as an underline belonging to the row above it.

   BLACK, following the chip and title above it, so the whole insolvency treatment is one gesture in one
   pair of colours. It was red, and the red went when the chip did: a red rule under a yellow-on-black
   badge would be a second signal for one fact. Black also gives the heaviest rule available on white
   without introducing a hue, which is what a divider between two REGIONS wants.

   Note the page still has red, on the wealth ribbon's negative fill, and that is now unambiguous rather
   than overloaded: red there means the plan is in DEBT, a fact about net worth, while insolvency is a
   fact about CASH. They are genuinely different (a plan can be insolvent while holding equity, which is
   the common case), and they no longer share a colour.

   Its margins are SYMMETRIC. They were 4px above and 10px below, which read as the rule belonging to
   the rows beneath it rather than as a divider between two regions; a break sits between two things and
   should be equidistant from both. The total is preserved, so the page's vertical rhythm is unchanged. */
.timeline-break {
    height: 3px;
    background-color: var(--color-black);
    margin: 7px 0;
}

/* Everything past the break. The plan keeps modelling and the rows keep their content; they simply stop
   competing with what happens before the money runs out.

   Opacity rather than a colour override, so the tail keeps every kind's own treatment (a milestone's
   ring, a bookend's grey, a goal chip's green or amber) instead of needing one re-colouring rule per kind.

   THE DIM IS APPLIED PER ELEMENT, NOT TO THE ROW, and that is the whole subtlety. Opacity on a parent
   cannot be undone by a child, so a row-level rule compounds onto the sub-lines that are ALREADY the
   page's quietest grey: --color-gray-dark (#999999) is 2.85:1 on white before anything dims it, and at
   0.55 it composites to #C7C7C7, i.e. 1.69:1, under even the 3:1 large-text floor. That would erase the
   dates telling the reader WHEN the post-insolvency rows happen. Note there is no opacity that avoids
   this: the grey starts below AA, so any value makes it worse and "dim less" is not a fix.

   So the effect is carried by the elements that can afford it. The near-black title composites to
   #737373 (4.74:1) and the chip is a glyph with no text to lose. The gutter's date and age, a spacer's
   label, and a bookend's grey title stay where the rest of the page already puts them. The region still
   reads as secondary, and the date column staying crisp is a bonus rather than a compromise: the
   chronology remains scannable through the part of the plan that went wrong.

   TestTimelineComponent.TheTailDim_IsNeverAppliedToTheRowItself keeps this shape. */
.timeline-row.timeline-after-insolvency .timeline-chip,
.timeline-row.timeline-after-insolvency .timeline-message,
.timeline-row.timeline-after-insolvency:not(.timeline-boundary) .timeline-title,
.timeline-gap.timeline-after-insolvency .timeline-gap-icon {
    opacity: 0.55;
}


/* ── The showcase gallery's two narrowing toggles ──────────────────────────────────────────────────
   Reuses the .user-mode-toggle / .user-mode-pill pill row wholesale (the Lite/Standard/Expert bar, and
   the movements filter above the Results chart), so this adds only the layout that puts two of those
   rows side by side and centres them over the grid. They wrap on a narrow viewport rather than
   compressing, since a pill row that shrinks stops reading as a set of choices. */
/* The page's three status messages: load failed, nothing published yet, and nothing matching the
   filter. Centred because everything else on this page is (the hint, the toggles, every card), so a
   left-aligned paragraph under a centred toggle row reads as a rendering mistake rather than a state.
   The `> p` child combinator catches exactly these three: every other paragraph here is nested inside
   a card. */
.showcases-page > p {
    text-align: center;
    margin-top: 24px;
}

.showcases-page .showcase-gallery-filters {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px 24px;
    margin-bottom: 18px;
}

/* ── The Strategy tab's bands chart ────────────────────────────────────────────────────────────
   View 1 of the tab, ABOVE the reasons strip and on the same axis of fiscal years. A couple gets
   SMALL MULTIPLES, one chart per person: a household has no tax-free allowance, so there is no
   honest way to sum two people into one bar (StrategyAuditPanel.md answer 18).

   The row is a flex pair so the two charts share the width and a reader compares them side by side;
   a single chart takes the whole row. They wrap on a narrow viewport rather than shrinking below
   something readable, since a 40-year bar chart at half a phone's width is not a chart.

   ORDER MATTERS HERE, as it does for the strip below. Every selector in this block is a single
   class, so specificity TIES and the later rule wins. Keep any reset (a card that wanted to drop a
   border, say) ABOVE the rules it must not beat -- the strip's own .strategy-strip-clickable block
   had to be moved above its three family colours for exactly this reason, and getting it wrong
   blanked the whole strip rather than failing visibly.                                           */
.strategy-bands-row {
    display: flex;
    flex-wrap: wrap;
    /* Wide enough that a couple's two charts read as TWO, which is the whole claim the small multiples
       make: a household has no tax-free allowance, so two charts that look like one panel invite exactly
       the summing the split exists to prevent. 16px was not a gutter, it was a seam. */
    gap: 40px;
    align-items: flex-start;
}

.strategy-bands-card {
    /* Grow to fill, shrink no further than half the row less the gap, and never below 320px: two
       charts sit side by side on a desktop and stack on a phone. */
    flex: 1 1 320px;
    min-width: 0;
}

/* ── The Strategy tab's reasons strip ──────────────────────────────────────────────────────────
   One row per person per rung, one cell per FISCAL YEAR, coloured by the strongest outcome that
   year held. A cell is a year rather than a month because a 35-to-100 plan is 780 months, which
   is 1.4px per cell on this column: see DF.Claude/Features/StrategyAuditPanel.md answer 14.

   The cells are FLEX-GROWN rather than fixed-width, so a 3-year fixture and a 60-year plan both
   fill the row and the strip always lines up with whatever sits above it. min-width keeps a long
   plan's cells clickable; the row scrolls rather than shrinking below that.                    */
.strategy-strip {
    margin: 6px 0 4px;
}

.strategy-strip-row {
    display: flex;
    align-items: center;
    gap: 8px;
    /* Stated on the TOP, which is .strategy-strip-legend's own note applied one level down: a gap goes on
       the block that OPENS a thing, so each row carries its own distance from whatever precedes it.
       That is what makes it safe here, because an opened year's months band IS a .strategy-strip-row
       (it carries .strategy-strip-months too). A shared margin-BOTTOM would have pushed that band a full
       rung's distance off the rung it belongs to, so it would have read as another rung rather than as
       that one's detail. */
    margin-top: 10px;
}

.strategy-strip-label {
    /* Widened with the font: "Personal Allowance fill, Your partner" is the longest label a couple
       produces, and it ellipsises out of a 190px track at this size. The cost is track width, which a
       40-year plan can afford (about 45px a year cell on a desktop column). */
    flex: 0 0 280px;
    text-align: right;
    /* It names the rung, and since the person toggle went it is also the only thing saying WHOSE rung a
       row is, so it was the smallest text on a tab it is load-bearing for. */
    font-size: 1.15rem;
    /* --color-text-secondary (#666666, 5.74:1), matching .strategy-strip-legend and
       .div-graph-description so the strip's three
       pieces of text read as one. It was --color-gray-steel, which is #F5F7FA: a near-white SURFACE token
       used as a TEXT colour, so the labels rendered at about 1.05:1 and were invisible on a white page.
       The same misuse as the cell fills this file fixed two rules down, and ChartPalette's own comment
       warns about this exact token ("the name says steel, the value is a panel background").
       It matters more since the person toggle went: this label is now the ONLY thing that says whose rung
       a row is, so an unreadable one makes a couple's four rows indistinguishable in pairs. */
    color: var(--color-text-secondary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Named -track rather than -cells because a class that is a PREFIX of another (.strategy-strip-cell)
   makes every substring count over the markup wrong, which is how the strip's own test first failed. */
/* A GRID, not a wrapping flex row, and the difference is a defect rather than a preference.

   As flex with `flex: 1 0 22px` the cells that overflowed onto a second line GREW to fill it, because
   flex-grow applies per line: a forty-year plan drew eighteen squares and then a handful of wide
   rectangles beneath them, which is pixel-for-pixel what an OPENED year's months band looks like. Every
   row appeared to have a year already selected. Nothing was selected.

   Grid has no per-line growth: `auto-fit` + `minmax(min(22px, 100%), 1fr)` sizes one set of columns for
   the whole track, so the last line's cells sit in the same column tracks at the same width as the first
   line's, and a short final line simply leaves the remaining columns empty.

   The min(22px, 100%) clamp is a deliberate trade, not only uniformity with the other auto-sized grids
   (TestAutoSizedGridTrackClamp holds every one of them to it). 22px is the smallest cell a person can
   point at, so the clamp only ever bites on a track narrower than ONE cell: it then draws one cell
   narrower than 22px inside the track instead of a 22px cell overflowing past it. The narrow-width media
   query below is what keeps the track wide enough for that never to happen in practice.

   Wrapping at all, rather than scrolling: a sideways scrollbar on a heat-map hides data behind a gesture
   nobody makes, and avoiding it was what squeezed the cells to a few pixels. Alignment across rows
   survives because every row carries a cell for every fiscal year of the plan (a rung with no row that
   year gets a padding cell), so all rows hold the same count and break at the same points.

   Not scrolling also fixes the hover, and that is the CAUSE rather than a coincidence: overflow-x: auto
   forces the computed overflow-y away from visible, so the track clipped the hover OUTLINE top and
   bottom and left only its left and right edges showing. */
.strategy-strip-track {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(22px, 100%), 1fr));
    flex: 1 1 auto;
    gap: 2px;
}

.strategy-strip-cell {
    /* A grid ITEM now: the track's columns decide the width, so the cell states only its height. It kept
       a flex basis through the grid change and that was harmless but misleading, since nothing read it.
       The 8px it had before was not a cell a person could see, let alone point at. */
    min-width: 0;
    height: 22px;
    border-radius: 2px;
    /* A bordered cell and a plain one must occupy the SAME box. "could not act" carries a dashed
       outline and "acted" carries none, so without this a row's cells differ in height by 2px
       depending on what happened that year, which reads as a rendering fault rather than as data. */
    box-sizing: border-box;
}

/* The opened year's twelve months: the same label gutter as the rung row above, so the band lines up
   with that row's TRACK, and shorter cells so it reads as a sub-band rather than another rung. It is
   deliberately NOT confined under the year cell it came from -- at forty years a cell is about 24px on
   this track, so twelve months inside one would be 2px each. The cell is marked open instead
   (.strategy-strip-open). */
.strategy-strip-months .strategy-strip-cell {
    height: 14px;
}

/* The months belong to the rung ABOVE them, so they sit at a fraction of the rung spacing. This must stay
   below .strategy-strip-row: both selectors are one class, so the tie is broken by source order. */
.strategy-strip-months {
    margin-top: 2px;
}

/* No italic: the label is a year, not an aside, and the band it heads is already set apart by its
   shorter cells and its position under the row. */
.strategy-strip-months .strategy-strip-label {
    font-weight: bold;
}

/* A year cell OPENS its months, so it is a real <button> rather than a div wearing role="button": Enter
   and Space are then native, which is what removes the keyboard trap a hand-rolled onkeydown +
   preventDefault introduced (see RenderStripCell). Being a button it arrives with UA styling the strip
   does not want, so everything the browser adds is reset back to the div it replaced: appearance is
   unchanged, only the semantics moved.

   IT MUST SIT BEFORE THE FAMILY RULES BELOW. Every selector here is a single class, so specificity
   ties and ORDER decides; with this block after them, background:none would beat
   .strategy-strip-drew / -idle and blank the whole strip. Each family then re-states the background it
   wants. (It named a third rule, .strategy-strip-blocked, until the families merged to two.) */
.strategy-strip-clickable {
    cursor: pointer;
    appearance: none;
    -webkit-appearance: none;
    padding: 0;
    margin: 0;
    border: none;
    background: none;
    font: inherit;
    color: inherit;
    /* A flex CHILD, like the div it replaced: without this a button shrink-wraps its (empty) content and
       the year cells collapse to nothing. */
    display: block;
}

/* Two families, not twelve outcomes (answer 15). Acted is the only saturated one: the inaction is
   muted because a reader is scanning for WHERE IT ACTED, and a strip where everything competes for
   attention shows nothing. */
.strategy-strip-drew {
    background: var(--color-green-dark);
}

/* A REAL FILL, not a near-white one. This was --color-surface-light (#FAFAFA), a SURFACE token and
   near-white by design, so against a white page it rendered as a blank box.

   There was a third rule here, .strategy-strip-blocked, a transparent cell with a dashed edge that
   separated "could not act" from "nothing to do". The families merged to two in Sep 2026 and
   FamilyClass can now only return drew or idle, so it was unreachable and is deleted rather than left
   to read as live. The distinction it drew is now one sentence per technique on its own row
   (.strategy-strip-unavailable), which is the thing a reader can act on. */
.strategy-strip-idle {
    background: var(--color-gray-light);
}

/* A year this rung had no row in. It holds the column open so the rows stay aligned and shows
   nothing, because "no row" is not an outcome and colouring it as one would be a wrong number. */
.strategy-strip-none {
    background: transparent;
}

.strategy-strip-row .strategy-strip-cell:hover {
    outline: 2px solid var(--color-black);
}

.strategy-strip-clickable:focus-visible {
    outline: 2px solid var(--color-black);
    outline-offset: 1px;
}

/* The cell whose months are open. It carries the marker because the band below cannot be narrowed to sit
   under it (see .strategy-strip-months above), so this is the ONLY thing connecting the two -- which is
   why it must not look like the focus ring. It did: both were a 2px black outline, so tabbing across the
   strip made a focused cell indistinguishable from the open one, for exactly the users the keyboard work
   was for. The ring stays an OUTLINE (outside the box, transient); open is an INSET box-shadow plus a
   downward caret (inside the box, persistent, and pointing at the band it opened). */
.strategy-strip-open {
    box-shadow: inset 0 0 0 2px var(--color-black);
    position: relative;
}

.strategy-strip-open::after {
    content: "";
    position: absolute;
    left: 50%;
    top: 100%;
    transform: translateX(-50%);
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    border-top: 4px solid var(--color-black);
}

/* Top and CENTRED, matching `legend: { position: "top", alignment: "center" }`, which is what
   charting.js passes for every Google chart on this page. It used to hang off
   .div-graph-description below the strip, which both put it in the wrong place and gave it that
   rule's 70% measure and 20px bottom margin, neither of which a legend wants. The colour and size
   are restated here because they were the only things worth inheriting. */
.strategy-strip-legend {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    flex-wrap: wrap;
    /* The tab stacks three blocks (charts, strip, table) and they ran together. The gap goes on the
       block that OPENS each one, so each carries its own separation from whatever precedes it. */
    margin-top: 40px;
    margin-bottom: 8px;
    color: var(--color-text-secondary);
    font-size: 1.2em;
}

.strategy-strip-legend .strategy-strip-cell {
    flex: 0 0 14px;
    min-width: 14px;
    height: 14px;
    margin-left: 10px;
}

/* The year-by-year flow chart, view 3. The wrapper exists to carry the block's own separation from the
   reasons strip above it, which is .strategy-strip-legend's stated convention applied one block down:
   the gap goes on the element that OPENS a block, so each carries its distance from whatever precedes
   it rather than depending on what came before. ChartCardRenderer emits no outer element of its own, so
   without this the chart's legend sits hard against the strip's caption. */
.strategy-flow-chart {
    margin-top: 40px;
}

/* A technique this plan cannot use at all: its name, then a sentence, and NO cells. The absence of cells
   is the statement, which is why the row keeps the label gutter and simply has nothing after it: a row
   that did nothing still has cells, so the two can never be confused. It replaces a full row of dashed
   squares that said "could not act" once per fiscal year, forty-five times, while the fact a reader could
   act on ("there is no ISA") was said nowhere. */
.strategy-strip-unavailable {
    /* --color-text-secondary (#666666, 5.74:1), the same grey the strip's label and legend carry, so the three read as one voice
       rather than as an error the tab is reporting. It is a fact about the plan, not a warning. */
    color: var(--color-text-secondary);
    font-size: 1.05rem;
    font-style: italic;
    align-self: center;
}

/* ── The Strategy tab at narrow widths ─────────────────────────────────────────────────────────
   One breakpoint for the whole tab: 768px is where .fluid-table stops being a table and becomes a
   stack of cards, so it is also where the strip stops being a gutter plus a track. Two breakpoints
   would mean a band of widths in which half the tab had reflowed and half had not.             */
@media (max-width: 768px) {
    /* THE LABEL MUST STOP BEING A GUTTER. It is a fixed 280px, sized for the longest name a couple
       produces; that is a third of a 768px row and essentially ALL of a 390px one. At phone width it
       left about 14px of track, so repeat(auto-fit, minmax(22px, 1fr)) resolved to ONE column and a
       forty-five-year plan drew a single 22px-wide ribbon running forty-five cells down the page,
       per rung. It is what made the tab 13,000px tall.

       Stacked, the label is a heading and the track gets the full row width. Measured at a 390px
       viewport, where the row is 294px: twelve columns, so that same forty-five-year plan wraps to
       four readable lines per rung instead of one forty-five-cell ribbon. */
    .strategy-strip-row {
        flex-direction: column;
        align-items: stretch;
        gap: 2px;
    }

    /* align-self is the CROSS axis, which the rule above just turned horizontal: the centring that
       sits the reason beside its row name on a wide screen would centre it under a left-aligned name
       here. Same reasoning as the flex-basis note below. */
    .strategy-strip-unavailable {
        align-self: start;
    }

    .strategy-strip-label {
        /* flex-basis is the MAIN axis, which the line above just turned vertical: the 280px it carries
           would otherwise become a 280px-tall label. */
        flex: 0 0 auto;
        text-align: left;
        /* nowrap + ellipsis existed to keep the gutter exactly one line tall so the cells beside it
           stayed aligned. Stacked there is nothing to align to, and a truncated rung name on the
           narrow layout is the one place the full name is least guessable. */
        white-space: normal;
        overflow: visible;
    }

    /* The year band is a HEADING, not a data row. Its two cells are the fiscal year and the ages it
       names, and the second one sits in the Why column, so the stacked layout prefixed it with that
       column's name and the band read "Why: Aged 58". Same reason .fluid-table tr.row-group-header
       suppresses the prefix.

       Dropping the ::before also drops the ages to the LEFT, which is deliberate rather than a side
       effect worth correcting: the cell is a flex row set to space-between, so with the label gone its
       one remaining item sits at the start. The band then reads as a two-line heading, the year over
       the ages, which is what a card wants; the wide layout's right-aligned ages exist to span a
       five-column table row, and there is no row left to span. */
    .details-table tr.strategy-year td::before {
        content: none;
    }
}

/* ── The Strategy tab's movement list ──────────────────────────────────────────────────────────
   View 3 of the tab: one row per movement identity per fiscal year, banded by year and expandable
   where a row stands for several months (StrategyAuditPanel.md "Shape").

   The band is a BACKGROUND, never a text colour. --color-gray-steel is a near-white panel fill
   (1.07:1 against white) and has already been mistaken for a foreground twice on this tab, so the
   greys here are stated literally alongside the .details-table rules they sit with.             */
/* Three sizes, descending, because the list has three levels and the eye should be able to tell them
   apart without reading: the YEAR band heads a block, a MOVEMENT row is a thing that happened, and a
   MONTH row is one instalment of it. The band must clear 1.2em to be the largest, because a movement
   row's When cell is a td.key and that rule is already 1.2em -- at 1.15em the heading rendered SMALLER
   than the rows beneath it, which is what made it read as one more shaded row. */
.details-table tr.strategy-year td {
    background-color: #F0F2F5;
    font-weight: bold;
    font-size: 1.45em;
    padding-top: 10px;
    padding-bottom: 10px;
    border-top: 1px solid #DDDDDD;
}

/* A row that folded more than one month opens onto them, so it is a control and says so. */
.details-table tr.strategy-fold { cursor: pointer; }

.details-table tr.strategy-fold:hover td { background-color: #F7F9FB; }

/* A row of one carries no chevron, and its When text must still start where a chevron row's text does.
   The space is reserved by rendering the SAME glyph and hiding it, rather than by a hand-tuned
   padding-left: a guessed 24px left the lone month visibly further left than the span above it, and any
   guess would drift again the moment the cell's font size moved (it just did, twice). An invisible
   chevron is exactly as wide as a visible one, by construction. */
.details-table td.strategy-fold-none::before {
    content: "\25B8\00A0";
    visibility: hidden;
}

/* The months an opened row expands to: indented under their parent and smaller than it, the third and
   quietest level.

   The quieting colour is scoped to the FIRST cell on purpose. As `tr.strategy-fold-month td` it was
   specificity (0,2,2) against `.details-table .red`'s (0,2,0), so it won, and every outflow in an opened
   row rendered grey where the same amount one level up rendered red. Colour on this table means
   direction; a layout rule must not overrule it. */
.details-table tr.strategy-fold-month td:first-child {
    color: #555555;
    /* Readable, but still under the 1.2em a movement row's When cell carries (td.key), because the three
       levels of this list are meant to step DOWN: year band, movement, month. At 1.2em a month would tie
       with the row it belongs to and the step would be gone. */
    font-size: 1.1em;
    padding-left: 32px;
}

/* The amounts on a month row are the quietest numbers in the list: they are the breakdown of the figure
   on the row above, so they read as subordinate to it rather than as peers. */
.details-table tr.strategy-fold-month td { font-size: 0.9em; }

/* The movement list opens the tab's third block; see .strategy-strip-legend for why the separation
   lives on the opening element. */
.strategy-panel .table-responsive { margin-top: 45px; }

/* The movement list's footnote: why a row's parts can differ from its total. Small and grey because it
   is a caveat rather than content, and it sits UNDER the table so a reader meets it after noticing the
   thing it explains. Deliberately not .div-graph-description, which is 1.2em and sized for a chart
   caption: at that size a footnote reads as a heading. --color-text-secondary is #666666, 5.74:1 on
   white, the same grey the strip's label and legend use, so the tab's quiet text is one colour. */
.strategy-table-note {
    max-width: 70%;
    margin: 10px auto 0;
    text-align: center;
    color: var(--color-text-secondary);
    font-size: 0.9em;
}

/* -------------------------------------------------------------------------------------------------
   THE RETURN DOOR

   The header's Sign in button. It lives HERE rather than in landing.css, and that is a correctness
   matter rather than a filing one: _Layout serves app.css to all 42 content pages while landing.css is
   linked by Landing.cshtml alone, so a door styled there would render unstyled on the 41 pages the door
   exists for -- the calculators, which carry the organic search traffic a returner arrives on.

   THE HEADER BUTTON IS DELIBERATELY QUIET. It sits on 42 content pages, most of them calculators
   whose job is a single number, and on the landing page it must not compete with the primary CTA.
   A returner is looking for it and will find it; a stranger must be able to ignore it, which is what
   a nav-bar affordance is for and what a line of prose under the hero would not have been.
   ------------------------------------------------------------------------------------------------- */

.explore-header-signin {
    margin-left: auto;
    align-self: center;
    padding: 10px 22px;
    border: 1px solid var(--color-gray-light, #DDDDDD);
    border-radius: 6px;
    font-size: 2rem;
    font-weight: 600;
    /* BLACK at rest (user, 2026-09-20; it was --color-dark-blue). The hover and focus state below keeps
       the accent blue, so the colour change on hover is now a real signal rather than navy going
       slightly bluer. */
    color: var(--color-black, #000000);
    text-decoration: none;
    white-space: nowrap;
    background: #FFFFFF;
}

.explore-header-signin:hover,
.explore-header-signin:focus-visible {
    border-color: var(--color-blue-accent, #12509E);
    color: var(--color-blue-accent, #12509E);
    text-decoration: none;
}

/* The identity badge that REPLACES the opener above once the probe settles on a tier. It occupies the same
   grid cell, so it carries the same margin-left: auto and align-self that put the door at the right edge:
   without them the badge would land in the middle column's flow and shove the wordmark off the page axis.

   Sized smaller than the opener on purpose. The opener is a call to action and wants the weight; the badge
   is a statement of fact with a link attached, and at 2rem it competed with the wordmark beside it. */
.lp-identity-badge {
    margin-left: auto;
    align-self: center;
    /* TWO LINES (user, Sep 2026): the greeting, then the silhouette, the tier and its glyph. A COLUMN of
       two flex children rather than one wrapping row, because a wrapping row reports its unwrapped width
       as its max-content and the grid track would size to that. CENTRED, the greeting over the line
       beneath it (user, Sep 2026), at every width. */
    display: flex;
    flex-direction: column;
    align-items: center;

    /* NO BORDER AND NO BACKGROUND (user, Sep 2026), so the greeting reads as part of the header rather than
       as a control boxed beside the wordmark. The opener it replaces is a button and wants the frame; this
       is a statement of fact with a link attached.

       The padding stays. It is the click target, not decoration, and it is the only route to /Profile from
       this page. */
    padding: 8px 16px;
    text-decoration: none;
    color: var(--color-black, #000000);
    white-space: nowrap;
}

/* THE SAME HOVER AS THE APP'S BADGE and as a menu item: the text goes to the link-hover blue. Set on the
   CHILDREN, because each carries its own colour and would override an inherited one. */
.lp-identity-badge:hover,
.lp-identity-badge:focus-visible {
    text-decoration: none;
}

/* A REAL FOCUS RING, and it is now load-bearing rather than belt-and-braces. Focus used to be signalled by
   the border changing colour; with the border gone that rule had nothing left to colour, so a keyboard user
   would have had no way to tell where they were. Matches the app badge's ring. */
.lp-identity-badge:focus-visible {
    outline: 2px solid var(--color-blue-accent, #12509E);
    outline-offset: 2px;
}

/* Same as the app badge: the tint is an at-rest state, so the hover takes all three tiers to the link
   colour together rather than leaving one behind. */
.lp-identity-badge:focus-visible .lp-identity-badge-icon,
.lp-identity-badge:focus-visible .lp-identity-badge-welcome,
.lp-identity-badge:focus-visible .lp-identity-badge-tier,
.lp-identity-badge:focus-visible .identity-badge-glyph {
    color: #0A58CA;
}

/* Hover only where a pointer can hover, for the sticky-tap reason on .identity-badge's hover rule. */
@media (hover: hover) {
    .lp-identity-badge:hover .lp-identity-badge-icon,
    .lp-identity-badge:hover .lp-identity-badge-welcome,
    .lp-identity-badge:hover .lp-identity-badge-tier,
    .lp-identity-badge:hover .identity-badge-glyph {
        color: #0A58CA;
    }
}

/* MATCHED TO THE APP BADGE IN RENDERED PIXELS, which is not the same as matching its numbers. The app
   badge sizes in em off a 1.2em baseline, this one in rem, and html/body here is font-size: 10.8px, so the
   app tier lands at 1.2 x 1.6 x 10.8 = 20.7px while this one at 1.7rem was 18.4px: same intent, about 11%
   smaller on screen. The values below are the app's rendered sizes converted back into rem. */
.lp-identity-badge-icon {
    font-size: 2.15rem;
    color: var(--color-gray-dark, #555555);
}

/* The greeting and the tier share a size, one line above the other. The difference
   between them is colour and weight, exactly as in the app's badge: grey and regular against black and
   semibold. Sizing them apart made the silhouette look like it belonged to one of them. */
.lp-identity-badge-welcome {
    font-size: 1.9rem;
    color: var(--color-gray-dark, #555555);
    margin-bottom: 5px;
}

/* The second line: silhouette, tier, tier glyph. Its font-size is the tier's, so the glyph's em (the
   shared .identity-badge-glyph mask from the app badge) sizes against the word it follows. */
.lp-identity-badge-who {
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 1.9rem;
}

/* Smaller than the app badge's 1.4em: here the glyph shares a line with the silhouette rather than
   trailing a line the silhouette spans, and at 1.4em it out-sized the silhouette beside it. 1.1em of
   1.9rem is about the silhouette's own rendered size. */
.lp-identity-badge .identity-badge-glyph {
    width: 1.1em;
    height: 1.1em;
    color: var(--color-black, #000000);
}

/* BLACK, matching the app. Blue read as a link inside something that is already one. */
/* No font-size of its own: it inherits .lp-identity-badge-who's, so the phone rule sizes the whole second
   line, glyph included, with one declaration. */
.lp-identity-badge-tier {
    font-weight: 600;
    color: var(--color-black, #000000);
}

/* Anonymous only, matching .identity-badge-tier-at-risk in the app's chrome. On the tier LINE rather than
   the block, because the other two values are not a risk and a tinted block would say they were. */
.lp-identity-badge-tier-at-risk {
    color: var(--color-identity-at-risk);
}

/* The silhouette and the tier glyph carry it too, matching the app badge. The glyph is keyed on the ROOT
   class because it is the tier line's sibling, not its child, so the tier's own class cannot reach it. */
.lp-identity-badge-at-risk .lp-identity-badge-icon,
.lp-identity-badge-at-risk .identity-badge-glyph {
    color: var(--color-identity-at-risk);
}

/* A THREE-COLUMN GRID, which is what it takes to centre the brand on the PAGE's axis while the door sits
   at the right edge. The outer columns are both 1fr, so grid keeps them equal whatever the door measures,
   and the brand in the middle lands on the true centre rather than in the middle of the space the door
   left over. A flex row with space-between cannot do this: it centres nothing, and the near-miss version,
   giving the brand flex: 1, puts it half the door's width off-axis, which looks like a bug.

   The empty first column is the price, and it is deliberate: one unused grid track buys a centred
   wordmark with no extra element in the markup.

   Replacing a flex row: app.css's own earlier .explore-header .container sets flex-direction: column, and
   an earlier version of this block set only display and align-items, so the column survived and the door
   rendered on its own LINE below the brand. A later rule overrides the properties it names and inherits
   the rest, which is why display is restated here rather than assumed. */
.explore-header .container {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: 16px;
}

.explore-header-brand {
    grid-column: 2;
}

/* Third column, pinned to its right edge. margin-left: auto on the element itself is a flex idiom and
   does nothing in a grid track, so the placement is justify-self here. */
.explore-header-signin {
    grid-column: 3;
    justify-self: end;
}

/* NARROW SCREENS give the centring up rather than overflow. Below 640px a 2rem door beside a wordmark
   cannot both fit and stay centred, and an off-screen brand is worse than an off-centre one: the columns
   collapse to brand-then-door, which is the ordinary mobile header. The audience for this product reads
   it on a phone, so this is the common case rather than the edge. */
@media (max-width: 640px) {
    .explore-header .container {
        grid-template-columns: 1fr auto;
    }

    /* THE WORDMARK SCALES WITH THE SCREEN so the unbreakable name still fits beside the door (user,
       2026-09-24): at a page's own size the logo and name needed more than a phone leaves once the door
       takes its column, and on the landing page the 441px they took widened the whole layout past the
       screen. 5.4vw is 19.4px at 360; the logo follows in em. */
    .explore-header-brand {
        grid-column: 1;
        justify-self: start;
        font-size: min(var(--explore-header-size, 2.2em), 5.4vw);
        gap: 10px;
    }

    /* THE TWO WORDS STACK (user, Sep 2026), so the wordmark gives the badge beside it the width it was
       short of: side by side they took the column and pushed the badge flush against the screen edge.
       A column of the two word spans, both beside the logo; the whitespace between them is dropped by
       the flex container, which is what a stacked lockup wants. Line-height 1 so the pair sits within the
       logo's height rather than standing taller than it. */
    .explore-header-name {
        display: flex;
        flex-direction: column;
        line-height: 1;
    }

    .explore-header-signin {
        grid-column: 2;
        font-size: 1.35rem;
        padding: 8px 14px;
    }

    /* THE BADGE GETS THE SAME MOBILE TREATMENT AS THE DOOR IT REPLACES, and the bug was that it did not.
       This block shrank `.explore-header-signin` and stopped there, but landing-signin.js hides that
       opener and reveals `.lp-identity-badge` in its place the moment the identity probe settles, so
       every returning visitor on a phone got a control sized for a desktop. It is the same defect shape
       as a rule written for one of two things that swap places: correct for the state it was written in,
       and unreached in the state that replaces it.

       The two-line layout is now the DESKTOP design too (user, Sep 2026), so what this block used to
       spend on forcing a wrap (a wrapping row, a 100% basis on the greeting, a max-width to make the
       wrap happen at all) is gone, and so is the end alignment: both lines are centred at every width.
       What stays is the phone's own choices: a tighter gap and no inline padding.

       THE INLINE PADDING IS THE ONE WORTH DEFENDING, because the base rule calls that padding the click
       target rather than decoration, and this drops half of it. What is left is the anchor's own text
       box, 40px tall and about 150px wide at this size: comfortably past the 24px minimum WCAG 2.2 asks
       for and past Apple's 44px on the axis that is tight. The horizontal half was never the constraint;
       it is reclaimed because 32px of it is what pushes the badge off a 390px screen. */
    .lp-identity-badge {
        padding-inline: 0;
    }

    /* 1.35rem, the "Sign in" opener's own phone size above, down from 1.5rem when the tier glyph joined
       this line (Sep 2026): measured at a 390px viewport, the line at 1.5rem was 141px and ran 10px past
       the screen edge, the glyph being the part that did not fit. */
    .lp-identity-badge-who {
        gap: 6px;
        font-size: 1.35rem;
    }

    .lp-identity-badge-welcome {
        font-size: 1.35rem;
    }

    .lp-identity-badge-icon {
        font-size: 1.7rem;
    }
}

/* ONLY THE STRIP STAYS. The nav holds the brand and the door and nothing else, so sticking it pins
   exactly those; the breadcrumbs are a sibling nav below and scroll away with the page.

   The z-index is above ordinary content and BELOW the sign-in dialog, which is a top-layer <dialog> and
   so is unaffected by either. The background is repeated here because a sticky element with a
   transparent fill shows the page sliding through it: landing.css repaints it for that page's surface. */
.explore-header {
    position: sticky;
    top: 0;
    z-index: 20;
    background: #FFFFFF;

    /* A soft edge rather than a rule, and it is needed BECAUSE of the stickiness rather than in spite of
       it. body.plain-page deliberately drops this header's border-bottom, which was free when the header
       simply sat at the top of the document and never overlapped anything; now that content scrolls
       underneath it, a borderless strip on those pages would have no boundary at all and text would
       appear to slide out of nothing. A shadow gives the separation without reinstating the rule that
       page deliberately removed. */
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}

/* The crumbs keep the exact spacing the header used to give them from inside its column: 10px below the
   brand, 25px above the content. Without the :has() the two margins would stack and push them 35px down,
   which is the sort of change that reads as "the page moved" rather than as a header restructure. */
.explore-breadcrumb-bar {
    margin-bottom: 25px;
}

.explore-header:has(+ .explore-breadcrumb-bar) {
    margin-bottom: 10px;
}

/* -------------------------------------------------------------------------------------------------
   THE SIGN-IN PANEL

   The setup dialog's mirror twin, and it shares that dialog's measurements deliberately: 560px max,
   centred on both axes, 1px rule, surface fill. A returner and a stranger are looking at the same
   product, so the two panels must read as one object rather than two designs.

   THE THREE CARDS ARE IDENTICAL IN STRUCTURE and differ only in what fills their control slot. The
   Anonymous card differs from the other two by COLOUR ALONE when it cautions, and every value in that
   state is an existing token: ChartPalette's BannerWarningLight / BannerWarningDark, already shipping as
   .banner-box.warning. Nothing is mixed for this panel.

   Amber rather than green, deliberately: green would read "this is the good one", which endorses staying
   at a tier whose plan is one browser-clear from gone. Amber says pay attention here, which is honest
   about both risks at once.
   ------------------------------------------------------------------------------------------------- */

.lp-signin {
    max-width: 560px;
    width: calc(100% - 32px);
    /* CAPPED TO THE SCREEN AND SCROLLING INSIDE (user, 2026-09-24, stuck on a phone). With no cap the
       modal grew to its content, 903px against a 640px screen, and the browser centred it anyway: its top
       sat 131px ABOVE the viewport, taking the close button with it, and nothing could scroll, since a
       modal blocks the page behind it and the dialog itself had nothing to scroll. The same cap the setup
       panel carries (landing.css, .lp-setup), dvh with a vh fallback for the phone's address bar. */
    max-height: calc(100vh - 32px);
    max-height: calc(100dvh - 32px);
    overflow-y: auto;
    border: 1px solid var(--color-gray-light, #DDDDDD);
    border-radius: 4px;
    padding: 0;
    background: #FFFFFF;
}

.lp-signin::backdrop {
    backdrop-filter: blur(3px);
    background: rgba(0, 0, 0, 0.25);
}

/* position: relative is what the close button anchors to. Without it the x escapes to the viewport and
   lands in the page's top-left corner, which is the failure mode of every absolutely-positioned control
   whose parent forgot to establish a containing block. */
.lp-signin-panel {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 18px;
    padding: 28px;
}

.lp-signin-head {
    display: flex;
    flex-direction: column;
    gap: 5px;
    text-align: center;
}

/* Black rather than the brand dark-blue: it sits directly above three black card titles, and a fourth
   colour at the top made the panel read as two designs stacked. */
.lp-signin-title {
    margin: 0;
    font-size: 3rem;
    font-weight: 700;
    color: #0A1017;
}

/* The landing acceptance line (_LegalAcceptanceLine.cshtml), in both dialogs. Small and quiet, but a full sentence a
   visitor can read: it is the notice their click accepts. The links keep the link blue and take the house dotted
   underline, as the in-app prompt's do. */
.lp-legal-line {
    margin: 0;
    font-size: 1.6rem;
    text-align: center;
    line-height: 1.5;
    color: #555555;
}

.lp-legal-line a {
    text-decoration: underline;
    text-decoration-style: dotted;
    text-underline-offset: 0.3em;
}

.lp-legal-line a:hover {
    text-decoration-style: solid;
}

/* #999 rather than #DDD, and through the token because GrayDark IS #999999: at #DDD the border was
   nearly the same value as the #FAFAFA fill, so a card read as a tint rather than as a bounded object and
   three of them looked like one panel with gaps in it. */
.lp-tier-card {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--color-gray-dark, #999999);
    border-radius: 10px;
    padding: 18px;
    background: var(--color-surface-light, #FAFAFA);
}

/* Title then glyph, both centred, exactly as /Profile's .tier-card-title lays them out. The glyph is a
   MASK tinted to currentColor rather than an <img>, which is what lets the caution state tint it for
   free by changing the title's colour. */
.lp-tier-title {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 11px;
    /* 23.8px (user, 2026-09-20; it was 2rem / 21.6px), so the card's name still leads now that the panel
       has lost its subtitles and gained a 21.6px key field under one of them. */
    font-size: 2.2rem;
    font-weight: 700;
    color: #0A1017;
}

.lp-tier-icon {
    width: 30px;
    height: 30px;
    flex: none;
    background-color: currentColor;
    -webkit-mask-image: var(--tier-icon);
    mask-image: var(--tier-icon);
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-position: center;
    mask-position: center;
    -webkit-mask-size: contain;
    mask-size: contain;
}

/* Not bold and gray: these are a caption under the card's own title, so weight here competed with the
   title above and made every card read as two headings.

   THE GRAY IS #5B6B73, NOT #999999, and that is a contrast fix rather than a taste one. Measured against
   both card grounds: #999999 is 2.73:1 on the #FAFAFA card and 2.50:1 on the #E7F1FF highlighted one,
   where WCAG AA wants 4.5:1 for text below 24px, and this is well below it. (That sentence used to read
   "1.4rem is 22.4px", which assumed a 16px root; the root is 10.8px, so 1.4rem is 15.12px. The arithmetic
   was wrong and the CONCLUSION survives it, since a smaller size is further below the 24px line, not
   nearer it. The rule now reads the shared prose token in any case.) On a panel deliberately sized up
   for readers with presbyopia, a gray that fails contrast undoes the sizing: the text is bigger and no
   easier to read. #5B6B73 measures 5.30:1 and 4.85:1, passing on BOTH grounds, and is still visibly a
   caption rather than body copy.

   It borrows --color-slate-badge, which is named for the showcase badge that first needed it. The palette
   has no readable muted-TEXT gray: GrayDark is #999999 (decorative) and GrayLight is #DDDDDD. Reusing the
   one existing value that passes beats minting a second token with identical bytes; a role-named alias
   would be the tidier answer the day a third surface wants it. */
.lp-tier-sub {
    text-align: center;
    font-weight: 400;
    font-size: 1.4rem;
    color: var(--color-slate-badge, #5B6B73);
    margin-top: 6px;
}

/* BLACK, and shared by all three cards since 2026-09-20: it is the second sentence of each card's
   two-sentence explainer, the one that tells the visitor what to DO. Body copy rather than a caption
   because of what the Anonymous card puts here: landing-signin.js replaces that card's line with the
   leave-behind warning when the probe reports a profile in this browser, and that is the one thing on
   the panel a visitor can lose by choosing wrongly. A caption grey would understate it, and the class
   cannot be split by card without the three explainers rendering at two different weights. */
/* The card explainer leads directly into the consent dialog, so it is sized with it. At 1.4rem it sat
   BELOW that dialog's prose, which is the wrong way round: the sentence that explains the choice was
   quieter than the dialog it opens. */
.lp-tier-detail {
    display: block;
    /* Centred (user, 2026-09-25), like the card title above it and the button below: each explainer is
       one short sentence now, so it reads as part of the card's centred column rather than as a
       paragraph. */
    text-align: center;
    font-size: var(--modal-prose-size);
    line-height: var(--modal-prose-line-height);
    color: #0A1017;
    margin-top: 12px;
}

/* Bigger than the body text: it holds a 34-character code a visitor is checking character by character
   against something they wrote down, which is the one field on this panel where legibility is the job. */
.lp-tier-input {
    box-sizing: border-box;
    width: 100%;
    /* Centred (user, 2026-09-25), the key and its format hint alike, on the card's centred column; the
       /Profile restore dialog's paste field (.recovery-key-box input.form-control) does the same. */
    text-align: center;
    min-height: 52px;
    margin-top: 8px;
    padding: 0 12px;
    border: 1px solid var(--color-gray-light, #DDDDDD);
    border-radius: 4px;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    /* 21.6px (user, 2026-09-20; it was 1.15rem). A key is 28 symbols in seven dash-separated groups, so 34
       characters on screen (the two figures are the same key, and both appear in this file), and the
       visitor is checking it character by character against something they pasted or copied out, so it is the one field on this
       panel that wants to be bigger than its label rather than smaller.

       Written as 2rem rather than the 2em the user first asked for, at their suggestion, and the two are
       the SAME 21.6px here: nothing between `html, body { font-size: 10.8px }` and this input sets a
       font-size, so em's parent and rem's root are the same element today. rem keeps it that way if a
       card ever gains one, and matches every other size in this block. */
    font-size: 2rem;
    /* NO extra tracking. A monospace face already spaces generously, and at 21.6px the 0.02em this
       carried was about 15px across a 34-character key, which was precisely enough to push the last
       character out of the field. Measured: with it, a pasted key rendered as "...FT7Z-QHV" with the
       final Y clipped. */
    letter-spacing: normal;
}

/* The 34-character format hint, in the same lighter grey the in-app recovery-key paste field already uses
   (`.recovery-key-box input.form-control::placeholder`): the browser default is too dark for ghost text
   that is a shape rather than a value, and at 21.6px there is a lot of it. Same colour and the same
   `opacity: 1`, which is not decoration: Firefox dims placeholders by default, so without it the chosen
   grey is not the grey that renders. */
.lp-tier-input::placeholder {
    color: #BBBBBB;
    opacity: 1;
}

/* NEVER SMALLER THAN THE PROSE IT INTERRUPTS, the rule .modal-warning already states further up and
   which this and .lp-signin-error both violated for their whole lives. Both were a flat 1rem, so the
   sentence telling a visitor their sign in had FAILED rendered at 10.8px: the smallest text on the panel,
   and 29% smaller than the .lp-tier-detail explainer directly above it that invited the action. Measured
   in Chromium against DEV, 2026-09-24. */
.lp-tier-error {
    margin-top: 8px;
    font-size: var(--modal-prose-size);
    color: var(--color-red, #DC3545);
}

/* 1.45rem and 56px tall. The people who most need a retirement plan are 50+ and reading this with
   presbyopia, often on a phone at arm's length, so the panel is sized for them rather than for whoever
   is looking at it on a desk monitor. */
/* BLUE, both of them (user, 2026-09-20). "Continue without signing in" was a white secondary button and
   "Restore profile" the blue primary; they are now the same blue, the app's ordinary action colour. The
   Google button keeps its own treatment, as its brand guidelines require. */
/* SIZED WITH THE PROSE, never below it: see --modal-prose-size. At a flat 1.45rem this sat 15% below the
   explainer above it and below the consent dialog's own prose, on the two most consequential controls the
   door has (a card's call to action, and the consent dialog's Confirm). */
/* THE COLOUR, HOVER, PRESS AND FOCUS RING ARE BOOTSTRAP'S, not this rule's (user, 2026-09-25: the two
   buttons had no hover at all). They carry `btn btn-primary` in the markup, the class every other blue
   button on the site uses, so they darken on hover exactly as Plan & Play does, from the one set of
   --bs-btn-* values "THE PRIMARY BLUE" above recolours. This rule painted its own blue and nothing
   else, which is how the two buttons were left without the states every sibling has. The consent
   dialog's buttons made the same move earlier (see .lp-signin-consent-actions); what stays here is only
   what this panel sizes differently.

   TWO CLASSES, and the second is load-bearing: app.css's `button.btn { font-weight: 500 }` is (0,1,1),
   which outranks a lone class, so the moment these became .btn their 600 silently dropped to 500 (Tahoma
   has no 500, so it rendered regular). The same trap caught .lp-setup-act .lp-setup-start a day earlier. */
.lp-tier-card .lp-tier-action {
    margin-top: 12px;
    font-size: var(--modal-prose-size);
    min-height: 56px;
    font-weight: 600;
}

/* (5) The Google button is full width inside a card, so its content needs centring; the shared class is
   inline-flex for its other callers and left-packs by default. Scoped to the panel so /Profile's own
   Google button is not restyled without anyone looking at it. */
/* font-weight is restated because the SHARED .btn-google-signin sets 500 for its other callers, and this
   block overrode only the size: the Google button sat beside two 600 siblings at 500, which reads as the
   least important of three equal choices. Everything the panel needs from that class it now names.
   THE SIZE TOO, the same miss one property along (user, 2026-09-25): it stayed at the flat 1.45rem that
   .lp-tier-action was moved off, so its label rendered about 15% smaller than the two blue buttons above
   it. It now takes the same token they do. */
.lp-tier-card .btn-google-signin {
    justify-content: center;
    width: 100%;
    min-height: 56px;
    margin-top: 12px;
    font-size: var(--modal-prose-size);
    font-weight: 600;
}

/* (--lp-tier-action-primary is DELETED as of 2026-09-20: every action in the panel is the blue it used
   to add, so the modifier had nothing left to say. Removed from the markup too.) */

/* (3) The highlighted card's action needed promoting to blue while the base action was white. Since
   2026-09-20 every action is blue, so that override is DELETED; the card's own tint still marks it. Its
   original point survives and is now structural rather than a rule: every choice in this panel is the
   same kind of act, so none of them reads as secondary because of which card it sits on. */

/* THE HIGHLIGHT. One treatment, two jobs, because they are the same claim: "this is the card about you".
   It marks the tier this browser last signed in with, and it marks the Anonymous card when that browser
   holds a profile the other options would leave behind.

   BLUE, NOT AMBER, and that is a correction rather than a preference. The amber was the warning-banner
   pair (#856404 on #FFF3CD), and on a panel where nothing has gone wrong it read as an error: a visitor
   opening a sign-in door and meeting a yellow card assumes they have already broken something. The card is
   pointing, not warning. Blue is the same colour the app uses for the primary action, so it reads as
   "start here".

   Colour only: the card keeps its structure and its border WIDTH, so nothing reflows when it appears or
   moves between cards. */
.lp-tier-card-highlight {
    border-color: var(--color-blue-accent, #12509E);
    background: var(--color-blue-light, #E7F1FF);
}

/* The title stays near-black. Tinting it blue as well made the whole card one colour and lost the
   distinction between the card's name and the sentence explaining it. */
.lp-tier-card-highlight .lp-tier-detail {
    color: #0A1017;
}

/* (6) DISABLED STAYS BLUE (user, 2026-09-20), dimmed rather than greyed. Restore is disabled until what
   is typed could be a key, and the user asked for it to remain the same blue while it waits.

   THIS REVERSES PART OF A DELIBERATE FIX, so the reasoning it replaces is kept rather than deleted. The
   rule used to swap the button to grey, because "a disabled button must read as NOT YET rather than as
   broken", and it was written after a disabled Restore inside a HIGHLIGHTED Key card rendered as a live
   blue button: the one person for whom that card is highlighted is a returner whose last sign-in was a
   recovery key, i.e. exactly the person who would have been shown a dead button that looked pressable.

   What keeps that failure closed is the OPACITY rather than the hue. The button is visibly not-yet
   (0.55), carries `cursor: not-allowed`, and the `disabled` attribute still makes it unfocusable and
   unclickable and announces as dimmed to assistive tech. A full-saturation disabled button would
   reintroduce the 2026-09-18 defect outright; if that is ever wanted, it is this one declaration, and
   this paragraph is the argument against it.

   The specificity note that used to live here is gone with its subject: the highlight override was
   deleted the same day, so there is no longer a competing (0,2,0) rule for source order to arbitrate.

   CONTRAST, raised in review and deliberately left: 0.55 over the card's white puts white-on-blue at
   roughly 2.2:1, under the 4.5:1 body-text floor. WCAG 2.2 exempts a disabled control from 1.4.3 for
   exactly this reason, the dimming IS the affordance, and the state is not a dead end a visitor can be
   stranded in: it clears the moment a key is typed into the field directly above, and the button is
   never the only thing on the card saying so. Raising the opacity to clear 4.5:1 would make the
   disabled button read as live, which is the 2026-09-18 defect this rule exists to prevent. */
.lp-tier-action:disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

/* The way out. Escape covers the keyboard; this covers touch, which has no Escape key. Small and quiet:
   an escape hatch rather than an offered path, so it must not compete with the three cards.

   Geometry mirrors landing.css's .lp-setup-close exactly (34px at 8/10, 1.6rem glyph). The COLOURS are
   rebuilt from palette tokens rather than reused: that rule is styled from --lp-muted / --lp-rule-faint,
   which landing.css defines and which do not exist on the other 41 pages this panel renders on. */
.lp-signin-close {
    position: absolute;
    top: 8px;
    right: 10px;
    width: 34px;
    height: 34px;
    padding: 0;
    border: 0;
    border-radius: 4px;
    background: transparent;
    color: var(--color-gray-dark, #999999);
    font-size: 1.6rem;
    line-height: 1;
    cursor: pointer;
}

.lp-signin-close:hover,
.lp-signin-close:focus-visible {
    background: var(--color-gray-light, #DDDDDD);
    color: #0A1017;
}

/* Panel-level errors: a Google start that failed, or an arrival this browser cannot prove it started.
   Separate from .lp-tier-error, which belongs to the recovery-key card, because writing one into the
   other tells the visitor the thing they did NOT do has gone wrong. */
/* THE CONSENT STEP ON THE DOOR. It swaps in where the three tier cards were, inside the same dialog, so
   it inherits .lp-signin-panel's column layout and padding and needs only what that does not give it.
   Its prose, checkbox row and red warning are the SHARED .modal-consent rules above, which is the point:
   the door and /Profile say the same words about the same gesture, so they read from one place.

   LEFT-aligned against the panel head's centring: this is two or three full sentences about what is
   about to happen to the visitor's work, and centred body text is harder to scan because every line
   starts somewhere different. The head above it stays centred. */
.lp-signin-consent {
    text-align: left;
}

/* Side by side, confirm last, so the affirmative sits where the eye finishes. Wraps rather than
   overflows on a narrow phone, where two buttons of this width do not fit on one row. (It read "two 56px
   buttons" until Sep 2026, when these stopped being .lp-tier-action and took the shared dialog height of
   48px; 56px is now only the CARDS' action. Measured at 375px after the change: no overflow, no page
   scroll.)

   THE CANCEL IS SUBORDINATE, not equal, and that has not changed: giving leaving and proceeding identical
   weight on the one gesture that can close a profile is what this guards against. What changed is HOW. It
   was an outlined-blue override on top of .lp-tier-action, because that class paints every action the same
   solid blue; the buttons now carry `btn btn-secondary` and `btn btn-primary`, so the asymmetry comes from
   the same grey-against-blue pair every other dialog in the app uses and the door needs no override of its
   own. The rule is gone rather than retuned. */
.lp-signin-consent-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 12px;
    /* The in-app dialog footer's 20px (see .modal-footer), so the door's buttons sit as far from its
       question as every other dialog's do. */
    margin-top: 20px;
}

/* The panel's failure line, sized with the key card's for the reason given there. */
.lp-signin-error {
    font-size: var(--modal-prose-size);
    line-height: var(--modal-prose-line-height);
    color: var(--color-red, #DC3545);
    text-align: center;
}
