/* Self-hosted fonts (docs/todo.md TODO-079): no request to Google before the
   visitor makes a cookie choice. Latin subset from Fontsource, SIL OFL, with
   the licences beside them in static/fonts/. DM Sans is the variable cut, so
   the 400/500/650/700 the site asks for all come from one file. Source Serif 4
   carries exactly the two styles app/layout.py's docstring explains: roman
   600 for h1 and italic 200 for .brand-tagline. */
@font-face {
  font-family: "DM Sans";
  font-style: normal;
  font-weight: 100 1000;
  font-display: swap;
  src: url("/fonts/dm-sans-latin-wght-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Source Serif 4";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/fonts/source-serif-4-latin-600-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Source Serif 4";
  font-style: italic;
  font-weight: 200;
  font-display: swap;
  src: url("/fonts/source-serif-4-latin-200-italic.woff2") format("woff2");
}

:root {
  /* without this, browsers render scrollbars (and other native UI) in their
     light default regardless of the page's dark theme */
  color-scheme: light dark;
  --bg: #f6f7f9;
  --surface: #ffffff;
  --surface-2: #f1f3f6;
  /*
  --border: #dfe3e8;
  --text: #1a2233;
  --muted: #5c6675;
  */
  --border: #dfe3e8;
  --text: #112057; /* Changed from #1a2233 */
  --muted: #5c6675;

  --faint: #8b94a3;
  --grid-line: rgba(15, 23, 42, 0.055); /* very faint chart gridlines, subservient to the data */
  --accent: #4f46e5;
  --accent-soft: #eef2ff;
  --accent-light: #6869ee;
  /* translucent, unlike --accent-soft: the paste box's syntax colouring sits
     behind the (invisible-text) textarea, so an opaque selection colour would
     black out the highlighted text underneath instead of just tinting it */
  --select-bg: rgba(79, 70, 229, .28);
  --gain: #15803d; /* Darkened emerald */

  --gain-soft: #e8f5ec;
  --loss: #b91c1c; /* Darkened crimson */

  --loss-soft: #fbeeec;
  --purple-soft: #f1ecfa;
  --purple: #7a5cc0;
  --teal: #0d8f80;
  --teal-soft: #d7f2f0;
  --orange: #b06e14;
  --orange-soft: #fef8f0;
  --fuchsia: #d946ef;
  --fuchsia-soft: #fdf5fe;
  --blue: #1d63c9;
  --blue-soft: #f3f8fe;
  --radius: 10px;
  --radius-sm: 8px;
  --mono: ui-monospace, "Cascadia Code", "SF Mono", Consolas, monospace;
  /* site-wide top bar — deliberately fixed navy/near-black rather than
     theme-driven, so it reads as chrome rather than page content */
  --nav-bg: #1e293b;
  --nav-text: #f8fafc;
  --nav-link: #cbd5e1;
  --nav-border: rgba(255, 255, 255, .12);
  --shadow-sm: 0 1px 2px rgba(15, 23, 42, .06);
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, .1), 0 4px 6px -4px rgba(0, 0, 0, .1);

  /* Design system: CTA button colors */
  --btn-primary: #3B82F6;
  --btn-primary-hover: #2563EB;
  --btn-primary-active: #1D4ED8;
  --btn-text: #FFFFFF;
  --btn-focus-glow: rgba(59, 130, 246, 0.4);
  /* the outline CTA's stroke and label — see #edit-data / #start-again below.
     Light: the filled CTA's own blue, so the two read as one control family. */
  --btn-outline: #3B82F6;
  --btn-outline-hover: #2563EB;
  --btn-secondary-border: #64748b;       /* slate-500 */
  --btn-secondary-text: #1e293b;         /* slate-800 */
  --btn-secondary-border-hover: #0f172a; /* slate-900 */
  --btn-secondary-text-hover: #0f172a;   /* slate-900 */
  --btn-secondary-bg-hover: #e2e8f0;     /* slate-200 */
  --btn-secondary-shadow-hover: 0 4px 10px -2px rgba(15, 23, 42, 0.25);
  /* the results' Export / Copy / Print buttons (button.btn-export, .btn-print) */
  --btn-tool-bg: #ffffff;
  --btn-tool-text: #3B82F6;
  --btn-tool-border: #3B82F6; /* the button blue (owner, 2026-10-21) */
  --btn-tool-bg-hover: #eef4ff;
  --btn-tool-border-hover: #3B82F6;
  --hero-bg: #1E293B;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0f1420;
    --surface: #171e2e;
    --surface-2: #1f2739;
    --border: #2c3650;
    --text: #e8ecf3;
    --muted: #9aa5b8;
    --faint: #6c7690;
    --grid-line: rgba(255, 255, 255, 0.07); /* very faint chart gridlines, subservient to the data */
    --accent: #818cf8;
    --accent-soft: #1e1b4b;
    --accent-light: #93a0fa;
    --select-bg: rgba(129, 140, 248, .35);
    --gain: #4cc272;
    --gain-soft: #16301f;
    --loss: #ef7565;
    --loss-soft: #3a1f1b;
    --purple-soft: #292040;
    --purple: #b39ae8;
    --teal: #4dd0be;
    --teal-soft: #123a3d;
    --orange: #e0a04a;
    --orange-soft: #3a2a14;
    --fuchsia-soft: #35143b;
    --blue: #5ea8f5;
    --blue-soft: #13273f;
    --nav-bg: #060c18;
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);
    --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, .6);
    --btn-primary: #3B82F6;
    --btn-primary-hover: #2563EB;
    --btn-primary-active: #1D4ED8;
    --btn-text: #FFFFFF;
    --btn-focus-glow: rgba(59, 130, 246, 0.4);
    /* Dark: --btn-primary is a mid-tone blue that works filled, under white
       text, but as a stroke and label on a near-black page it reads muddy
       (5.00:1, technically a pass and visibly heavy). This is the palette's
       own --blue for dark, 7.36:1 on --bg, hover lighter again at 10.20:1. */
    --btn-outline: #5ea8f5;
    --btn-outline-hover: #93c5fd;
    --btn-secondary-border: #94a3b8;       /* slate-400 */
    --btn-secondary-text: #e2e8f0;         /* slate-200 */
    --btn-secondary-border-hover: #f1f5f9; /* slate-100 */
    --btn-secondary-text-hover: #f8fafc;   /* slate-50 */
    --btn-secondary-bg-hover: #475569;     /* slate-600 */
    --btn-secondary-shadow-hover: 0 4px 10px -2px rgba(0, 0, 0, 0.5);
    /* dark: the same family on the dark card colour, text and strokes lifted */
    --btn-tool-bg: #171e2e;
    --btn-tool-text: #3B82F6;
    --btn-tool-border: #3B82F6;
    --btn-tool-bg-hover: #1c2a44;
    --btn-tool-border-hover: #3B82F6;
  }
}

/* manual theme toggle overrides the OS preference in either direction */
:root[data-theme="light"] {
  color-scheme: light;
  --bg: #f6f7f9;
  --surface: #ffffff;
  --surface-2: #f1f3f6;
  /*
  --border: #dfe3e8;
  --text: #1a2233;
  --muted: #5c6675;
  */

  --border: #dfe3e8;
  --text: #112057; /* Changed from #1a2233 */
  --muted: #5c6675;
  --faint: #8b94a3;
  --grid-line: rgba(15, 23, 42, 0.055); /* very faint chart gridlines, subservient to the data */
  --accent: #4f46e5;
  --accent-soft: #eef2ff;
  --accent-light: #6869ee;
  --select-bg: rgba(79, 70, 229, .28);
  /*
  --gain: #157f3d;
  --gain-soft: #e8f5ec;
  --loss: #c0392b;
  */
  --gain: #15803d; /* Darkened emerald */
  --gain-soft: #e8f5ec;
  --loss: #b91c1c; /* Darkened crimson */

  --loss-soft: #fbeeec;
  --purple-soft: #f1ecfa;
  --purple: #7a5cc0;
  --teal: #0d8f80;
  --teal-soft: #d7f2f0;
  --orange: #b06e14;
  --orange-soft: #fef8f0;
  --fuchsia-soft: #fdf5fe;
  --blue: #1d63c9;
  --blue-soft: #f3f8fe;
  --nav-bg: #1e293b;
  --shadow-sm: 0 1px 2px rgba(15, 23, 42, .06);
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, .1), 0 4px 6px -4px rgba(0, 0, 0, .1);
  --btn-primary: #3B82F6;
  --btn-primary-hover: #2563EB;
  --btn-primary-active: #1D4ED8;
  --btn-text: #FFFFFF;
  --btn-focus-glow: rgba(59, 130, 246, 0.4);
  /* the outline CTA's stroke and label — see #edit-data / #start-again below.
     Light: the filled CTA's own blue, so the two read as one control family. */
  --btn-outline: #3B82F6;
  --btn-outline-hover: #2563EB;
  --btn-secondary-border: #64748b;       /* slate-500 */
  --btn-secondary-text: #1e293b;         /* slate-800 */
  --btn-secondary-border-hover: #0f172a; /* slate-900 */
  --btn-secondary-text-hover: #0f172a;   /* slate-900 */
  --btn-secondary-bg-hover: #e2e8f0;     /* slate-200 */
  --btn-secondary-shadow-hover: 0 4px 10px -2px rgba(15, 23, 42, 0.25);
  /* the results' Export / Copy / Print buttons (button.btn-export, .btn-print) */
  --btn-tool-bg: #ffffff;
  --btn-tool-text: #3B82F6;
  --btn-tool-border: #3B82F6; /* the button blue (owner, 2026-10-21) */
  --btn-tool-bg-hover: #eef4ff;
  --btn-tool-border-hover: #3B82F6;
  --hero-bg: #1E293B;
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #0f1420;
  --surface: #171e2e;
  --surface-2: #1f2739;
  --border: #2c3650;
  --text: #e8ecf3;
  --muted: #9aa5b8;
  --faint: #6c7690;
  --grid-line: rgba(255, 255, 255, 0.07); /* very faint chart gridlines, subservient to the data */
  --accent: #818cf8;
  --accent-soft: #1e1b4b;
  --accent-light: #93a0fa;
  --select-bg: rgba(129, 140, 248, .35);
  --gain: #4cc272;
  --gain-soft: #16301f;
  --loss: #ef7565;
  --loss-soft: #3a1f1b;
  --purple-soft: #292040;
  --purple: #b39ae8;
  --teal: #4dd0be;
  --teal-soft: #123a3d;
  --orange: #e0a04a;
  --orange-soft: #3a2a14;
  --fuchsia-soft: #35143b;
  --blue: #5ea8f5;
  --blue-soft: #13273f;
  --nav-bg: #060c18;
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, .6);
  --btn-primary: #3B82F6;
  --btn-primary-hover: #2563EB;
  --btn-primary-active: #1D4ED8;
  --btn-text: #FFFFFF;
  --btn-focus-glow: rgba(59, 130, 246, 0.4);
  /* Dark: --btn-primary is a mid-tone blue that works filled, under white
     text, but as a stroke and label on a near-black page it reads muddy
     (5.00:1, technically a pass and visibly heavy). This is the palette's
     own --blue for dark, 7.36:1 on --bg, hover lighter again at 10.20:1. */
  --btn-outline: #5ea8f5;
  --btn-outline-hover: #93c5fd;
  --btn-secondary-border: #94a3b8;       /* slate-400 */
  --btn-secondary-text: #e2e8f0;         /* slate-200 */
  --btn-secondary-border-hover: #f1f5f9; /* slate-100 */
  --btn-secondary-text-hover: #f8fafc;   /* slate-50 */
  --btn-secondary-bg-hover: #475569;     /* slate-600 */
  --btn-secondary-shadow-hover: 0 4px 10px -2px rgba(0, 0, 0, 0.5);
  --btn-tool-bg: #171e2e;
  --btn-tool-text: #3B82F6;
  --btn-tool-border: #3B82F6;
  --btn-tool-bg-hover: #1c2a44;
  --btn-tool-border-hover: #3B82F6;
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }
/* Clear the fixed header when an in-page link lands on a section, or the
   heading sits underneath it and the reader arrives at a section whose title
   they cannot see.

   Structural rather than a list of ids, which is what this was. The list went
   stale twice without anything noticing, because nothing about adding a
   <section id> tells you a stylesheet elsewhere needs the id too: #crypto and
   formatting.html's #intro, #delimiters and #spreadsheet were all silently
   short, and #currency made it four. Every anchor target on these pages is a
   `section.prose[id]`, so matching that shape covers the ones that exist and
   the ones nobody has written yet.

   Safe to widen: scroll-margin-top affects only where an anchor or
   scrollIntoView comes to rest, never layout. 76px is the 59px header plus a
   little air. */
section.prose[id] { scroll-margin-top: 76px; }

body {
  margin: 0;
  padding-top: 59px;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.55 "DM Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

/* plain prose links otherwise fall back to the browser's default blue/purple,
   which reads too dark against a dark background — theme them explicitly */
a { color: var(--accent); }
a:visited { color: var(--purple); }
a:hover { text-decoration: underline; }

.container { max-width: 1060px; margin: 0 auto; padding: 0 20px; }

header.site {
  background: var(--nav-bg);
  border-bottom: 1px solid var(--nav-border);
  position: fixed; top: 0; left: 0; right: 0; z-index: 10;
}
header.site .container {
  display: flex; align-items: center; justify-content: space-between;
  height: 58px;
}
.brand { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 17px; color: var(--nav-text); text-decoration: none; }
/* .brand is now a link (id="brand-link"), so the global a:visited rule
   above — same specificity as a bare .brand, but declared later — would
   otherwise repaint a visited "/" purple instead of the header's white/navy
   nav-text; nav.site a below sidesteps this the same way. */
.brand:visited { color: var(--nav-text); }
.brand:hover { text-decoration: none; }
.brand svg { width: 28px; height: 28px; }
/* The wordmark. Heavier and tighter than the 17px/600 .brand sets above, so
   "GainsEngine" carries the bar at 58px without the mark growing; the second
   half takes the logo's own amber, which is what ties a one-word name back to
   the icon beside it. The literal #F0A02C is deliberate and theme-independent,
   like the navy it sits on (6.81:1 there, 9.1:1 on dark's --nav-bg) — the
   header is chrome, so it does not restate itself per theme. */
.brand-text { font-size: 19px; font-weight: 700; letter-spacing: -.022em; }
.brand-engine { color: #F0A02C; }
/* The tagline after the wordmark — "GainsEngine" no longer says what the site
   does, and this is what says it. Source Serif 4's italic 300, so it reads as
   a different voice from the sans wordmark rather than a smaller version of
   it; the 300 is the whole point, since anything heavier competes with the
   700 beside it. app/layout.py's docstring covers why the <link> loads a real
   italic cut rather than letting the browser shear the roman.

   Nested inside .brand-text, not beside it, so the 480px rule below takes the
   tagline away with the wordmark and leaves the mark alone. Tracking is reset
   to normal: the wordmark's -.022em is a display setting for a tight sans
   lockup and it closes up an italic serif at this size. */
.brand-tagline {
  font-family: "Source Serif 4", "Times New Roman", Times, serif;
  font-size: 17px; font-weight: 200; font-style: italic;
  letter-spacing: normal; color: var(--nav-link);
  white-space: nowrap;
  /* Lifted off the wordmark's baseline so the two read as separate voices
     rather than one run of type. Deliberately `position`, not the more
     idiomatic `vertical-align: 2px`: that grows .brand-text's line box, and
     .brand-text is a flex item of .brand (align-items: center), so the taller
     item gets re-centred and the whole lockup — mark included — drops about a
     pixel. A relative offset moves these three words and nothing else. */
  position: relative; top: -1px;
}
/* The tagline is the first thing to go when the ribbon runs out of room, and
   it goes well above the nav's own 760px breakpoint: the full NAV_ITEMS row
   is still showing between there and here, and brand-plus-nav overflows the
   1060px container with three more words in it. At 800px the lockup broke
   "for UK / CGT" onto a second line and shoved the mark off the bar's centre
   line — hence both this rule and the nowrap above, which stops the wrap from
   being the failure mode if this width is ever lowered. At 910px the two
   sides leave ~80px of slack; lengthening either the tagline or NAV_ITEMS
   spends it. */
@media (max-width: 900px) {
  .brand-tagline { display: none; }
}
.brand .tag {
  font-size: 12px; font-weight: 500; color: var(--nav-link);
  border: 1px solid var(--nav-border); border-radius: 99px; padding: 2px 10px;
}
/* flex container so the text links and the round icon buttons (envelope,
   theme toggle — taller than a line of text) share one vertical centre line
   instead of falling back to inline-layout baseline alignment */
nav.site { display: flex; align-items: center; }
nav.site a {
  color: var(--nav-link); text-decoration: none; font-size: 14px; font-weight: 500; margin-left: 20px;
  transition: color .12s;
}
nav.site a:hover { color: var(--nav-text); }
nav.site a[aria-current="page"] { color: var(--nav-text); font-weight: 600; }

.theme-toggle,
#envelope-button {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px;
  background: transparent; border: 1px solid var(--nav-border); border-radius: 99px;
  color: var(--nav-link); cursor: pointer; flex-shrink: 0;
  transition: background .12s, color .12s, border-color .12s;
}
#envelope-button { margin-left: 18px; }
.theme-toggle { margin-left: 8px; }
.theme-toggle:hover,
#envelope-button:hover {
  background: rgba(255, 255, 255, .1); border-color: rgba(255, 255, 255, .4);
  color: var(--nav-text); text-decoration: none;
}
.theme-toggle svg,
#envelope-button svg { display: block; width: 17px; height: 17px; }
.theme-toggle .ic-sun { display: none; }

/* icon reflects the action clicking will deliver: moon in light mode (switch to dark),
   sun in dark mode (switch to light). OS preference by default, overridden by
   an explicit data-theme choice in either direction */
@media (prefers-color-scheme: dark) {
  html:not([data-theme]) .theme-toggle .ic-sun { display: block; }
  html:not([data-theme]) .theme-toggle .ic-moon { display: none; }
}
html[data-theme="dark"] .theme-toggle .ic-sun { display: block; }
html[data-theme="dark"] .theme-toggle .ic-moon { display: none; }
html[data-theme="light"] .theme-toggle .ic-sun { display: none; }
html[data-theme="light"] .theme-toggle .ic-moon { display: block; }

/* checkbox-hack hamburger: the checkbox is invisible but stays in the tab
   order (not display:none) so a keyboard user can still focus and toggle it;
   its <label>, styled as the round icon button, is the only visible control.
   Both nav-toggle-btn and mobile-menu are display:none above the breakpoint
   below, so the checkbox has no visible effect on wide viewports. */
.nav-toggle-input { position: absolute; opacity: 0; width: 32px; height: 32px; pointer-events: none; }
.nav-toggle-btn {
  display: none; align-items: center; justify-content: center;
  width: 32px; height: 32px; margin-left: 8px;
  background: transparent; border: 1px solid var(--nav-border); border-radius: 99px;
  color: var(--nav-link); cursor: pointer; flex-shrink: 0;
  transition: background .12s, color .12s, border-color .12s;
}
.nav-toggle-btn:hover {
  background: rgba(255, 255, 255, .1); border-color: rgba(255, 255, 255, .4);
  color: var(--nav-text);
}
.nav-toggle-btn svg { display: block; width: 17px; height: 17px; }
.nav-toggle-btn .ic-menu-close { display: none; }
.nav-toggle-input:checked ~ nav.site .nav-toggle-btn .ic-menu-open { display: none; }
.nav-toggle-input:checked ~ nav.site .nav-toggle-btn .ic-menu-close { display: block; }

.mobile-menu { display: none; }

/* below this width the full NAV_ITEMS text-link row (see layout.py) no
   longer fits beside the brand and the icon buttons — collapse it to just
   the "Calculator" link plus the hamburger, which reveals the rest
   (#mobile-menu, a duplicate of the hidden .nav-extra links) as a dropdown */
@media (max-width: 760px) {
  .nav-extra { display: none; }
  .nav-toggle-btn { display: inline-flex; }
  .mobile-menu {
    display: none;
    position: absolute; top: 58px; left: 0; right: 0;
    flex-direction: column;
    background: var(--nav-bg); border-bottom: 1px solid var(--nav-border);
    padding: 4px 20px 8px;
  }
  .nav-toggle-input:checked ~ .mobile-menu { display: flex; }
  .mobile-menu a {
    color: var(--nav-link); text-decoration: none; font-size: 14.5px; font-weight: 500;
    padding: 12px 0; border-top: 1px solid var(--nav-border);
  }
  .mobile-menu a[aria-current="page"] { color: var(--nav-text); font-weight: 600; }
}

/* narrower still: the brand wordmark next to "Calculator" + the three icon
   buttons no longer fits either — drop to just the logo mark (still links
   home) so the collapsed nav has room */
@media (max-width: 480px) {
  .brand-text { display: none; }
}

main.container { padding: 20px 20px 60px; }

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 22px;
  margin-bottom: 24px;
  box-shadow: var(--shadow-sm);
}

/* the calculator/results sections sit directly on the page canvas — no
   enclosing card, so each bento card and the table container carries its
   own bg/border/shadow instead of inheriting a wrapper's */
.dash-section { margin-bottom: 32px; }

h1 { font-family: "Source Serif 4", Georgia, serif; font-size: 30px; font-weight: 600; line-height: 1.14; letter-spacing: -.01em; margin: 0 0 6px; }
h2 { font-size: 18px; font-weight: 600; margin: 0 0 12px; }
.sub { color: var(--muted); margin: 0 0 20px; font-size: 14.5px; }

/* ---------- progress stepper ---------- */
/* Import data → Review & edit → View results, as a full-width band directly
   under the fixed header. It stands in for the hero rather than sitting above
   it — the two are never on screen together, which is why there is no first
   visit on which this appears. See #stepper in content/pages/index.html and
   syncStepper() in app.js, which is what shows it and sets the state classes.

   In light mode it takes the hero's own navy, so it continues the header's
   dark chrome exactly as the pitch it replaced did, divided from it by the
   header's own bottom hairline and ending against the much lighter page below
   — which is why the band draws no rule of its own there. Everything inside it
   is therefore light-on-dark, and every colour it uses is a --step-* variable
   so the dark theme below can restate the lot in one place. */
.stepper {
  --step-size: 44px;   /* marker diameter; the connector maths keys off this */
  --step-gap: 12px;    /* clear space between a marker and its connector line */
  --step-surface: var(--hero-bg);
  --step-rule: transparent;
  --step-done-bg: #cbd5e1;
  --step-done-fg: var(--hero-bg);
  --step-upcoming-bg: transparent;
  --step-upcoming-border: #64748b;
  --step-upcoming-fg: #94a3b8;
  --step-line: #475569;
  --step-label: #f8fafc;
  --step-label-muted: #94a3b8;
  --step-caption: #cbd5e1;
  --step-eyebrow: #F0A02C;
  background: var(--step-surface);
  border-bottom: 1px solid var(--step-rule);
  padding: 20px 20px 18px;
}
/* Dark mode reverses the relationship: rather than continuing the header, the
   band takes the page's own background and belongs to the page, with the
   header's darker --nav-bg reading as the edge above it (a band a few shades
   off a dark page reads as a slab laid on it, which is also why .hero below
   drops its navy there for the nav's own colour). That leaves nothing to mark where the bar ends, so unlike
   light mode it keeps a bottom hairline. The rest is the theme's own text and
   border tokens. Completed steps stay light slate in both, since a dark marker
   on a dark band would disappear, and the amber current step needs no
   restating at all: it holds its contrast on either.
   Written twice, as .hero is: once for the OS preference with a guard so an
   explicit "light" choice still wins, once for the toggle. */
@media (prefers-color-scheme: dark) {
  html:not([data-theme]) .stepper {
    --step-surface: var(--bg);
    --step-rule: var(--border);
    --step-done-fg: var(--bg);
    --step-upcoming-bg: transparent;
    --step-upcoming-border: var(--border);
    --step-upcoming-fg: var(--faint);
    --step-line: var(--border);
    --step-label: var(--text);
    --step-label-muted: var(--faint);
    --step-caption: var(--muted);
    --step-eyebrow: var(--orange);
  }
}
html[data-theme="dark"] .stepper {
  --step-surface: var(--bg);
  --step-rule: var(--border);
  --step-done-fg: var(--bg);
  --step-upcoming-bg: transparent;
  --step-upcoming-border: var(--border);
  --step-upcoming-fg: var(--faint);
  --step-line: var(--border);
  --step-label: var(--text);
  --step-label-muted: var(--faint);
  --step-caption: var(--muted);
  --step-eyebrow: var(--orange);
}

/* Reaching the results is the end of the bar's job, so it leaves, after a
   beat, by sliding up behind the fixed header (opaque, and above it on
   z-index). It does so as a layer over the page (BUG-138): app.js pins it
   where it already was (.is-floating, with its measured top, left and width),
   so the results take their final place at once and nothing under the bar
   moves. The slide is a transform, which moves the bar alone; the margin-top
   transition it replaced re-laid out the whole results page on every frame
   and made large results judder. z-index 8: under the header (10) and the
   results sidebar (9), over the page. */
.stepper.is-floating { position: absolute; z-index: 8; box-sizing: border-box; }
.stepper.is-lifting { transform: translateY(-100%); transition: transform 2s ease-in; }
.stepper.is-lifted { display: none; }

.stepper-inner {
  max-width: 1060px; margin: 0 auto; padding: 0 20px;
  display: flex; flex-direction: column; align-items: center; gap: 14px;
}
.stepper-steps {
  list-style: none; margin: 0; padding: 0;
  display: flex; width: 100%; max-width: 620px;
}
.stepper-step {
  position: relative; flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  text-align: center;
}
/* The connector belongs to the step on its right: it runs from the centre of
   the previous marker to the centre of this one, less a radius and a gap at
   each end. Every step is flex: 1 1 0, so the 50% offsets land on the same
   points whatever the labels are — which is why it is a ::before rather than
   an element somebody has to keep in sync in the markup. */
.stepper-step + .stepper-step::before {
  content: ""; position: absolute;
  top: calc(var(--step-size) / 2 - 1px);
  right: calc(50% + var(--step-size) / 2 + var(--step-gap));
  left: calc(-50% + var(--step-size) / 2 + var(--step-gap));
  height: 2px; border-radius: 1px;
  background: var(--step-line);
  transition: background-color .2s ease;
}
/* a connector is travelled once the step before it is finished, which is true
   of the line into the current step as much as of one between two done steps */
.stepper-step.is-done::before,
.stepper-step.is-current::before { background: var(--step-done-bg); }

.stepper-marker {
  width: var(--step-size); height: var(--step-size); border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  border: 2px solid var(--step-upcoming-border);
  background: var(--step-upcoming-bg); color: var(--step-upcoming-fg);
  transition: background-color .2s ease, border-color .2s ease, box-shadow .2s ease, color .2s ease;
}
.stepper-step.is-done .stepper-marker {
  background: var(--step-done-bg); border-color: transparent; color: var(--step-done-fg);
}
/* the amber of the logo's own bars (and the hero eyebrow's dot), on the navy
   the logo pairs it with — the one colour on the site that cannot be mistaken
   for a completed step at a glance */
.stepper-step.is-current .stepper-marker {
  background: #F0A02C; border-color: transparent; color: #1e293b;
  box-shadow: 0 0 0 5px rgba(240, 160, 44, .18);
}
.stepper-marker svg { width: 20px; height: 20px; }
/* A completed step swaps its stage icon for a tick, so "done" is not carried
   by the marker's fill colour alone. Both icons sit in the markup and the
   swap is pure CSS, which is why syncStepper() only ever changes classes. */
.stepper-icon-check { display: none; }
.stepper-step.is-done .stepper-icon-stage { display: none; }
.stepper-step.is-done .stepper-icon-check { display: block; }

.stepper-label {
  font-size: 13px; font-weight: 600; line-height: 1.3;
  color: var(--step-label-muted); transition: color .2s ease;
}
.stepper-step.is-done .stepper-label,
.stepper-step.is-current .stepper-label { color: var(--step-label); }

.stepper-caption { margin: 0; text-align: center; }
.stepper-eyebrow {
  display: block; margin-bottom: 3px;
  font-size: 11px; font-weight: 650; letter-spacing: .08em; text-transform: uppercase;
  color: var(--step-eyebrow);
}
.stepper-caption-text { font-size: 14px; color: var(--step-caption); }

/* the "Completed:" / "Current step:" prefixes, which exist so the state is
   not carried by colour alone — read out, never shown */
.stepper-sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

/* ---------- ordered rule flow ----------
   examples.html's matching-rules list, drawn as the same left-to-right run of
   markers and connectors the progress bar above uses. Deliberately the same
   shape: a reader who has run a calculation has already learnt to read it,
   and these three rules ARE a sequence — each one only gets what the one
   before it did not match.

   The geometry is .stepper's, connector maths included, so the two stay in
   step if that changes. What differs is the load. .stepper names three states
   of one journey in two or three words each and tracks which you are in;
   this names three rules that are all always true, in application order, and
   each needs a full sentence. Hence: no is-done/is-current states, numbered
   markers instead of icons, and a heading plus a paragraph per step rather
   than one line.

   The numbers come from a CSS counter rather than the markup, so the <ol>
   keeps the real ordering semantics and a screen reader is not read "1. 1
   Same day". Every colour is a page token — it sits on the canvas, not in the
   navy band, so there is no light/dark inversion to restate here. */
.rule-flow {
  --step-size: 40px;
  --step-gap: 12px;
  counter-reset: rule-step;
  list-style: none; margin: 0 0 22px; padding: 0;
  display: flex;
}
.rule-flow > li {
  counter-increment: rule-step;
  position: relative; flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  text-align: center; padding: 0 14px;
}
/* the connector belongs to the step on its right and runs centre to centre,
   less a radius and a gap at each end — see the note on .stepper-step's copy
   of this. The li's own padding does not disturb it: an absolutely positioned
   child resolves its offsets against the padding box, whose centre is still
   the marker's centre. */
.rule-flow > li + li::before {
  content: ""; position: absolute;
  top: calc(var(--step-size) / 2 - 1px);
  right: calc(50% + var(--step-size) / 2 + var(--step-gap));
  left: calc(-50% + var(--step-size) / 2 + var(--step-gap));
  height: 2px; border-radius: 1px;
  background: var(--border);
}
.rule-flow-marker {
  width: var(--step-size); height: var(--step-size); border-radius: 50%;
  display: flex; align-items: center; justify-content: center; flex: none;
  background: var(--surface-2); border: 2px solid var(--border);
  color: var(--text);
  font-size: 15px; font-weight: 650; font-variant-numeric: tabular-nums;
}
.rule-flow-marker::before { content: counter(rule-step); }
.rule-flow-title { font-size: 14.5px; font-weight: 600; color: var(--text); line-height: 1.3; }
.rule-flow-title .aka { display: block; font-weight: 500; font-size: 13px; color: var(--muted); }
.rule-flow-text { margin: 0; font-size: 13px; line-height: 1.55; color: var(--muted); }

/* Three sentences side by side stop being readable long before the layout
   breaks — below this the run becomes a stack, and the connectors go with it
   rather than being redrawn vertically: the numbers and the reading order
   already carry the sequence. */
@media (max-width: 760px) {
  .rule-flow { flex-direction: column; gap: 20px; }
  .rule-flow > li { padding: 0; }
  .rule-flow > li + li::before { display: none; }
}

@media (max-width: 640px) {
  .stepper { --step-size: 36px; --step-gap: 8px; padding: 14px 12px 12px; }
  .stepper-inner { gap: 10px; padding: 0 8px; }
  .stepper-marker svg { width: 17px; height: 17px; }
  .stepper-label { font-size: 11px; }
  .stepper-caption-text { font-size: 13px; }
}

/* .stepper itself, not only its contents, so the lift becomes an instant
   removal rather than a two-second slide */
@media (prefers-reduced-motion: reduce) {
  .stepper, .stepper *, .stepper *::before { transition: none !important; }
}

/* ---------- first-load hero ---------- */
/* The pitch, as a full-width band above the input area. It sits outside
   main.container in the markup — a sibling of <main>, like header.site — so
   the band reaches both edges of the viewport while its contents keep the
   page's own column width. Painted with --hero-bg (the same navy as the
   Chargeable Gain card and the nav) in BOTH themes rather than a theme
   colour, so it reads as chrome; the hairline stops it and the nav above it
   merging into one tall slab. On index.html app.js hides the whole section
   once data is loaded, so there it is a first-visit pitch, not a permanent
   banner.

   The reference pages reuse this class for their own title band (see
   .hero-link below). Nothing hides it there — those pages carry no app.js —
   so the band is simply how every page in the site starts, which is the
   point: the header used to end straight onto the light canvas on
   formatting.html, and a reader arriving from the calculator met what looked
   like a different site. Anything changed below therefore moves all of them
   together, which is the intent. */
.hero {
  background: var(--hero-bg);
  border-top: 1px solid rgba(255, 255, 255, .07);
  color: #f8fafc;
}
/* ...except in dark mode, where the navy has nothing to stand out from: the
   page under it is already dark, so a band a few shades lighter reads as a
   slab laid on the page rather than as the top of it. There it takes the
   nav's own --nav-bg (#060c18), so the header and the hero read as one piece
   of chrome, with the hairline above as the only seam between them.
   Written twice, as .dropzone-icon is: once for the OS preference with a
   guard so an explicit "light" choice still wins, once for the toggle. */
@media (prefers-color-scheme: dark) {
  html:not([data-theme]) .hero { background: var(--nav-bg); }
}
html[data-theme="dark"] .hero { background: var(--nav-bg); }
.hero > .container { padding-top: 34px; padding-bottom: 40px; }
/* Set in <head> on index.html when a saved calculation is about to be put
   back (layout.INDEX_THEME_BOOTSTRAP_SCRIPT), and cleared by app.js once it
   has been. Without it the first-visit hero and input area paint for a moment
   before app.js runs. The hero goes for good on a restore, so it is removed
   outright; the calculator is only held back, so nothing below it moves. */
html.restoring #hero { display: none; }
html.restoring main.container { visibility: hidden; }
/* the amber dot is the colour of the logo's own bars */
.hero-eyebrow {
  display: flex; align-items: center; gap: 8px; margin: 0 0 18px;
  font-size: 11px; font-weight: 650; letter-spacing: .12em; text-transform: uppercase;
  color: #cbd5e1;
}
.hero-eyebrow::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: #F0A02C; flex: none;
}
/* h1's serif face, weight and leading are inherited from the base rule above;
   only the display size and the measure are the hero's own. The measure is
   680px, not the lede's 640px, so index.html's tagline ("Smarter than a basic
   calculator. Simpler than a spreadsheet.", about 658px at 24px) sits on one
   row with a little room to spare for font rendering differences */
.hero h1 { font-size: 40px; letter-spacing: -.015em; margin: 0 0 16px; max-width: 680px; color: #f8fafc; }
/* The closing full stop of a band title, and nothing else: face, size, weight
   and tracking all stay the h1's. The amber is the logo's own literal, the
   same one .brand-engine carries in the header, so the wordmark's full stop
   and the headline's are the same mark — theme-independent for the same
   reason the rest of the band's amber is. A full stop carries no meaning of
   its own, so nothing is lost where the colour is not perceived.

   Every page's band carries it, index.html and the reference pages alike
   (each `<h1>` in content/pages/ and the FAQ's own _HERO_HTML), which is why
   the title-case pages read "Sitemap." rather than "Sitemap" — the stop is
   the device, not punctuation the title was missing. Add it to any new band
   title for the same reason. */
.hero h1 .hero-stop { color: #F0A02C; }
/* The tagline that trails the headline on index.html ("Buy it. Sell it.
   Sorted."). Smaller and unbolded so it reads as a second voice rather than a
   second sentence of the same size, while staying inside the h1 so it keeps the
   serif face, the colour and the headline's own measure and leading.

   `em`, not `px`: the size is a proportion of whatever the h1 is at this
   breakpoint, so the 40px/32px switch below carries the tagline with it and
   there is nothing to keep in sync. `normal` is against the h1's own 600
   (line 434), and does the same work the smaller size does.

   `display: block` is what puts it on its own row, rather than a `<br>` in the
   markup: the break is a property of how the tagline is set, so it belongs
   here, and the headline above it stays free to wrap on its own terms at any
   width. The top margin is in `em` for the same reason the size is, so the gap
   keeps its proportion when the h1 shrinks. */
.hero h1 .sub-text { display: block; margin-top: 0.35em; font-size: 0.6em; font-weight: normal; }
.hero .lede { margin: 0 0 22px; max-width: 640px; font-size: 15px; line-height: 1.62; color: #b7c2d4; }
/* Any link in any paragraph of the band, not just .lede's: the amber is what
   makes a link legible against the navy, and --accent (indigo light, a pale
   indigo dark) is not — so a paragraph added to a band without .lede on it
   would otherwise render its links in a colour that fails on that background
   in both themes. Keyed to the band, therefore, not to one class inside it.
   The literal #F0A02C is deliberate and theme-independent, like the navy it
   sits on: the band is chrome, so it does not restate itself per theme.

   `p a` deliberately misses the two other kinds of link the band holds —
   .rules-panel's list and its trailing link, which are `li > a` and a direct
   child, and .hero-actions' pills — so each keeps its own treatment.

   No underline at rest, matching the body copy below the band. That leans
   harder on the colour here than it does down there: amber is only 1.20:1
   against .lede's own #b7c2d4 (2.05:1 against a plain hero paragraph's
   #f8fafc), so on the band a link is told apart by hue rather than by
   lightness — well under WCAG 1.4.1's 3:1 for colour used alone. The
   underline on hover and focus is the whole of the non-colour cue, which is
   why it stays. */
.hero p a { color: #F0A02C; text-decoration: none; }
.hero p a:hover,
.hero p a:focus-visible {
  color: #ffc46b;
  text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px;
}
@media (max-width: 820px) {
  .hero h1 { font-size: 32px; }
  .hero > .container { padding-top: 26px; padding-bottom: 30px; }
}

/* ---------- the reference pages' band ----------
   Where index.html's band ends in a disclosure (.rules-pop, below), the
   reference pages have somewhere to send the reader instead, so their band
   ends in plain links wearing that disclosure's own pill. Same padding,
   radius, fill and hover as .rules-pop > summary — deliberately the same
   object, not a second pill shape a few pixels off it.

   :visited is restated because the global `a:visited { color: var(--purple) }`
   is a class-plus-pseudo-class selector too and is declared later; without
   this an already-visited link would go purple against the navy, the same
   trap .brand and nav.site a work around above. */
.hero-actions { display: flex; flex-wrap: wrap; gap: 10px; }
.hero-link {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 14px;
  font-size: 13px; font-weight: 500; color: #e2e8f0;
  background: rgba(255, 255, 255, .06);
  border: 1px solid rgba(255, 255, 255, .16); border-radius: 999px;
  text-decoration: none;
  transition: color .15s, border-color .15s, background .15s;
}
.hero-link:visited { color: #e2e8f0; }
.hero-link:hover {
  color: #fff; border-color: rgba(255, 255, 255, .28);
  background: rgba(255, 255, 255, .11); text-decoration: none;
}

/* The rules list: a disclosure inside the hero band, closed on arrival. Its
   panel opens in normal flow beneath its own pill rather than floating, as it
   did while the intro sat on the light canvas — an absolutely-positioned
   panel here would hang over the drop zone below the band instead of moving
   it down. .rules-pop is a block so the panel can take the band's full width;
   the summary stays inline-flex, so the pill is only as wide as its label. */
.rules-pop { display: block; margin: 0; }
.rules-pop:not([open]) > .rules-panel { display: none; }
.rules-pop > summary {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 14px;
  font-size: 13px; font-weight: 500; color: #e2e8f0;
  background: rgba(255, 255, 255, .06);
  border: 1px solid rgba(255, 255, 255, .16); border-radius: 999px;
  cursor: pointer; list-style: none;
  transition: color .15s, border-color .15s, background .15s;
}
.rules-pop > summary::-webkit-details-marker { display: none; }
.rules-pop > summary:hover,
.rules-pop[open] > summary { color: #fff; border-color: rgba(255, 255, 255, .28); background: rgba(255, 255, 255, .11); }
.rules-pop > summary .chev { transition: transform .15s; }
.rules-pop[open] > summary .chev { transform: rotate(180deg); }

.rules-panel {
  margin-top: 14px; padding: 18px 22px;
  background: rgba(255, 255, 255, .045);
  border: 1px solid rgba(255, 255, 255, .09); border-radius: var(--radius);
}
.rules-panel ul {
  margin: 0; padding: 0; list-style: none;
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 11px 30px;
}
.rules-panel li { display: flex; align-items: center; gap: 9px; font-size: 13px; }
.rules-panel li svg { color: #4cc272; flex: none; }
.rules-panel li a { color: #dbe3ef; text-decoration: none; }
.rules-panel li a:hover { color: #fff; text-decoration: underline; }
.rules-panel > a { display: block; margin-top: 16px; font-size: 12.5px; color: #F0A02C; text-decoration: none; }
.rules-panel > a:hover { text-decoration: underline; }

@media (max-width: 820px) { .rules-panel ul { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px) { .rules-panel ul { grid-template-columns: 1fr; } }

/* TODO-087: on desktop app.js keeps the list open and adds .rules-always-open
   (unless config.ini RULES_OPEN_ON_DESKTOP = false), so the pill has nothing
   to do and is hidden; the panel then opens straight under the lede. Keyed on
   that class so that with the setting off the pill still shows and still
   works. At 640px and below (the same mobile breakpoint app.js uses) app.js
   removes the class, so it stays a dropdown. */
@media (min-width: 641px) {
  .rules-pop.rules-always-open > summary { display: none; }
  .rules-pop.rules-always-open > .rules-panel { margin-top: 0; }
}
/* The server now sends the list open with that class (app/hmrc_rules.py
   fill()), so it paints with the rest of the hero instead of waiting for
   app.js. On a phone that would be a flash of an open list, so until app.js
   removes the class and closes it, the panel stays hidden here. */
@media (max-width: 640px) {
  .rules-pop.rules-always-open > .rules-panel { display: none; }
}

/* TODO-087: each rule's summary (from content/hmrc-rules.md, built into the
   list by app/hmrc_rules.py) opens under its link on hover or keyboard
   focus, desktop only. On a phone the list is a dropdown and a tap simply
   follows the link. The box lives inside the <li>, and its transparent top
   padding bridges the gap, so the pointer can move onto the box without it
   closing. Escape hides it (app.js, .tip-dismissed). The selectors restate
   display and list-style because .rules-panel's own ul/li rules (grid, flex)
   would otherwise reach the box's list too. */
.rules-panel li { position: relative; }
.rules-panel .rule-tip { display: none; }
@media (min-width: 641px) {
  .rules-panel li:hover > .rule-tip,
  .rules-panel li:focus-within > .rule-tip { display: block; }
  .rules-panel li.tip-dismissed > .rule-tip { display: none; }
}
.rules-panel .rule-tip {
  position: absolute; top: 100%; left: 21px; z-index: 30;
  width: 320px; padding-top: 8px;
}
/* the right-hand column opens leftwards, so the box stays on screen */
.rules-panel > ul > li:nth-child(3n) > .rule-tip { left: auto; right: 0; }
@media (max-width: 820px) {
  .rules-panel > ul > li:nth-child(3n) > .rule-tip { left: 21px; right: auto; }
  .rules-panel > ul > li:nth-child(2n) > .rule-tip { left: auto; right: 0; }
}
.rules-panel .rule-tip ul {
  display: block; margin: 0; padding: 12px 16px 12px 30px;
  list-style: disc;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lg);
}
.rules-panel .rule-tip li {
  display: list-item; font-size: 13px; line-height: 1.5;
}
.rules-panel .rule-tip li + li { margin-top: 6px; }
.rules-panel .rule-tip li::marker { color: #F0A02C; }

.pill-tabs { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
.pill-tabs button {
  border: 1px solid var(--border); background: transparent; color: var(--muted);
  font: inherit; font-size: 13.5px; padding: 7px 16px; border-radius: 99px; cursor: pointer;
  transition: background .12s, color .12s, border-color .12s;
}
.pill-tabs button:hover:not(.active) { border-color: var(--accent); background: var(--accent-soft); color: var(--text); }
.pill-tabs button.active {
  background: var(--accent-soft); color: var(--accent); border-color: transparent; font-weight: 600;
}

/* results view's own "Annual summary / Disposals / ..." tabs — matches
   .year-tabs' (the primary tax year selector) recipe exactly: a single
   white/surface strip with a border, hairline dividers between segments, and
   the selected segment filled solid in the brand accent, so the two tab rows
   read as one system rather than two different controls. Eight tabs don't
   fit a phone width, so the strip scrolls sideways inside
   .result-tabs-scroll rather than wrapping onto a second line and breaking
   the segmented shape.

   Class-scoped, not id-scoped: examples.html renders one results
   block per worked example on a single page, so every strip on that page
   needs the same recipe. index.html's own strip keeps its #result-tabs id as
   app.js's handle, and carries this class alongside it.

   Every copy also carries .carousel-track (see #result-tabs-carousel below
   and .carousel further down), which layers in the same prev/next arrow
   affordance #year-tabs already has once the strip overflows — otherwise the
   only sign an eighth tab exists is a hidden edge to swipe or shift-scroll,
   easy to miss. index.html wires its one copy from app.js's initCarousel();
   examples.html isn't served app.js, so its own inline script (the section
   filter strip and one per calculated example) carries an equivalent
   initCarousel() of its own. */
.result-tabs-scroll {
  display: flex; overflow-x: auto; margin-bottom: 16px;
  scrollbar-width: none;
}
.result-tabs-scroll::-webkit-scrollbar { display: none; }
#result-tabs-carousel { margin-bottom: 16px; max-width: 100%; min-width: 0; }
#result-tabs-carousel .result-tabs-scroll { margin-bottom: 0; }
.result-tabs {
  display: inline-flex; align-items: stretch; flex-wrap: nowrap; margin-bottom: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
  /* .result-tabs-scroll is itself a flex container, which makes this its
     flex item — without flex:none, its own overflow:hidden gives it an
     automatic minimum width of 0, so instead of forcing its parent to
     overflow (and scroll) it would just flex-shrink down to whatever width
     is available and silently clip the tabs that don't fit. */
  flex: none;
}
.result-tabs button {
  font: inherit; font-size: 14px; font-weight: 500;
  padding: 8px 16px;
  color: var(--muted);
  background: none; border: none; border-right: 1px solid var(--border);
  border-radius: 0;
  cursor: pointer; white-space: nowrap; flex: none;
  transition: color .2s, background .2s, box-shadow .2s;
}
.result-tabs button:last-child { border-right: none; }
.result-tabs button:hover:not(.active) { color: var(--text); background: var(--surface-2); }
.result-tabs button.active {
  color: var(--btn-text);
  background: var(--btn-primary);
}
.result-tabs button.active:hover { background: var(--btn-primary-hover); }
.result-tabs button.active:focus {
  outline: none;
  /* inset, unlike .year-tabs' own outer glow — an outer ring here would
     spill 2px onto the neighbouring segment's shared edge (only the
     strip's own outer edge is clipped by .result-tabs' overflow:hidden,
     not the boundary between two adjacent buttons), showing as a stray
     blue-grey line on the tab next door until focus moves away */
  box-shadow: inset 0 0 0 2px var(--btn-focus-glow);
}
/* --accent is a light indigo in dark mode, where white-on-accent has too
   little contrast — the selected segment flips to dark text instead, same
   override .year-tabs button.year.active uses */
@media (prefers-color-scheme: dark) {
  html:not([data-theme]) .result-tabs button.active { color: var(--btn-text); }
}
html[data-theme="dark"] .result-tabs button.active { color: var(--btn-text); }

/* ---------- sidebar results layout (config.ini [results] RESULTS_LAYOUT) ----------
   app/results_layout.py marks #results .layout-sidebar; without that class
   none of this applies and the tab row above is unchanged. The HTML is the
   same in both layouts.

   Above phone width the tab strip becomes a page-level sidebar: fixed to the
   left edge of the window, from under the fixed 59px header.site to the
   bottom, with the whole page (title, year tabs, cards, tables, footer)
   moved right to make room. app.js sets body.results-sidebar only while the
   results are on screen, so the page before a calculation is unchanged.

   Over 900px: a 240px sidebar of icon and name rows.
   641 to 900px: a 64px icon-only rail; each button's name is its tooltip.
   640px and below: one picker button naming the shown section, which opens
   the same buttons as a list under it (app.js sectionPicker).

   The icons and the picker are hidden in the tab layout. */
.result-tabs .tab-icon, .section-picker { display: none; }

@media (min-width: 641px) {
  body.results-sidebar { --sidebar-w: 240px; padding-left: var(--sidebar-w); }
  /* the room the sidebar takes is given back by widening the page */
  body.results-sidebar main.container { max-width: 1320px; }
  /* The footer stays lined up with header.site, which is centred on the whole
     window, not on the room beside the sidebar. The footer's 100% is the
     window less the sidebar, so 50% - 530px - half the sidebar puts its left
     edge where the header's is. Once the window is too narrow for that, the
     margin bottoms out at 0 and the footer sits against the sidebar. */
  body.results-sidebar footer.site .container {
    margin-left: max(0px, calc(50% - 530px - var(--sidebar-w) / 2));
    margin-right: 0;
  }
  #results.layout-sidebar #result-tabs-carousel {
    position: fixed; top: 59px; bottom: 0; left: 0; z-index: 9;
    width: var(--sidebar-w, 240px);
    display: block; margin: 0; padding: 18px 12px;
    overflow-y: auto;
    background: var(--surface);
    border-right: 1px solid var(--border);
  }
  #results.layout-sidebar #result-tabs-carousel .carousel-arrow { display: none; }
  #results.layout-sidebar .result-tabs-scroll { display: block; overflow: visible; }
  #results.layout-sidebar .result-tabs {
    display: flex; flex-direction: column; gap: 4px;
    background: none; border: none; border-radius: 0; overflow: visible;
  }
  #results.layout-sidebar .result-tabs button {
    display: flex; align-items: center; gap: 14px;
    width: 100%; padding: 11px 14px; text-align: left;
    font-size: 15px; color: var(--text);
    border: none; border-radius: var(--radius-sm);
    white-space: normal; line-height: 1.3;
  }
  #results.layout-sidebar .result-tabs .tab-icon { display: block; flex: none; color: var(--muted); }
  /* the selected section's name and icon in the brand amber (owner, 2026-10-26) */
  #results.layout-sidebar .result-tabs button.active,
  #results.layout-sidebar .result-tabs button.active:hover {
    color: #F0A02C; background: var(--accent-soft); font-weight: 600;
  }
  #results.layout-sidebar .result-tabs button.active .tab-icon { color: #F0A02C; }
  #results.layout-sidebar .result-tabs button.active:focus { box-shadow: none; }
  /* a section row under the mouse or the keyboard is outlined in the brand
     amber, the same for both (owner, 2026-10-26), inset so the rows do not move */
  #results.layout-sidebar .result-tabs button:hover,
  #results.layout-sidebar .result-tabs button.active:hover,
  #results.layout-sidebar .result-tabs button:focus-visible,
  #results.layout-sidebar .result-tabs button.active:focus-visible { box-shadow: inset 0 0 0 1px #F0A02C; }
  /* and the hover's lighter fill (.result-tabs button:hover) */
  #results.layout-sidebar .result-tabs button:focus-visible:not(.active) { color: var(--text); background: var(--surface-2); }
  /* the amber line is the focus mark, so not the browser's own outline too */
  #results.layout-sidebar .result-tabs button:focus-visible { outline: none; }
  /* while the keys are in use, the row under a resting mouse is not outlined
     too (app.js sets .keyboard-nav until the mouse moves) */
  #results.layout-sidebar .result-tabs.keyboard-nav button:hover:not(:focus-visible) { box-shadow: none; }
  #results.layout-sidebar .result-tabs.keyboard-nav button:hover:not(:focus-visible):not(.active) { background: none; }
}

/* Light mode: the sidebar is navy, #171e2e (owner, 2026-10-26), so it takes
   the dark theme's colours for its text, icons, lines and selected row, as
   in dark mode, where that is already its background. Two rules, as the
   theme blocks above: light chosen with the toggle, or by the system. */
@media (min-width: 641px) {
  :root[data-theme="light"] #results.layout-sidebar #result-tabs-carousel {
    --surface: #171e2e; --surface-2: #1f2739; --border: #2c3650;
    --text: #e8ecf3; --muted: #9aa5b8; --accent: #818cf8; --accent-soft: #1e1b4b;
  }
}
@media (min-width: 641px) and (prefers-color-scheme: light) {
  :root:not([data-theme]) #results.layout-sidebar #result-tabs-carousel {
    --surface: #171e2e; --surface-2: #1f2739; --border: #2c3650;
    --text: #e8ecf3; --muted: #9aa5b8; --accent: #818cf8; --accent-soft: #1e1b4b;
  }
}

/* Dashboard is the overview; a line sets it apart from the detail sections */
#results.layout-sidebar .result-tabs button[data-tab="dashboard"] { position: relative; margin-bottom: 9px; }
#results.layout-sidebar .result-tabs button[data-tab="dashboard"]::after {
  content: ""; position: absolute; left: 8px; right: 8px; bottom: -7px;
  border-bottom: 1px solid var(--border);
}

/* "Download Report" (config.ini [features] SIDEBAR_ZIP_DOWNLOAD), the last
   row of the sidebar, below a line like Dashboard's. Filled amber by
   button.calc-btn; laid out here like the section rows above it. Keyed on
   body.results-sidebar, not #results, because at phone width the list is
   moved out of #results (app.js placeSectionPicker). Never shown in the tab
   layout, which keeps its own "Download all (.zip)".
   Edit Data and Start Again sit above it, filled blue by button.primary, and
   the line goes above the first of them (.sidebar-first). The PDF report's own
   "Download Report" (#sidebar-pdf) follows it, amber the same way. */
.sidebar-action { display: none; }
body.results-sidebar .sidebar-action:not(.hidden) {
  position: relative; display: flex; align-items: center; gap: 14px;
  width: 100%; margin-top: 6px; padding: 11px 14px;
  text-align: left; font-size: 15px; font-weight: 600; line-height: 1.3; white-space: normal;
}
body.results-sidebar .sidebar-action.sidebar-first:not(.hidden) { margin-top: 13px; }
body.results-sidebar .sidebar-first::before {
  content: ""; position: absolute; left: 8px; right: 8px; top: -7px;
  border-top: 1px solid var(--border);
}
body.results-sidebar .sidebar-action .tab-icon { display: block; flex: none; }

/* the intro bars index.html gives the sections that have none of their own
   (Annual summary, Disposals and so on) belong to this layout only */
#results:not(.layout-sidebar) .sidebar-intro { display: none; }

/* the summary cards are the Dashboard; the year tabs stay on every section,
   since the year they pick applies to all of them */
#results.layout-sidebar:not([data-section="dashboard"]) .dash-cards,
#results.layout-sidebar:not([data-section="dashboard"]) #year-strip,
/* its footnote explains the trading costs figures under those cards */
#results.layout-sidebar:not([data-section="dashboard"]) #trading-costs-note,
/* the PDF report's button is in the sidebar here (#sidebar-pdf, under
   Download Report), so the one beside the title is for the tab layout */
#results.layout-sidebar #download-pdf { display: none; }
/* the selected year's report beside the title shows on every section, not
   just the Dashboard (owner, 2026-10-28) */
/* on All Years it is the all-years "Download Report" (app.js
   applyYearPdfButtons), which the tab layout already has as #download-pdf */
#results:not(.layout-sidebar) #download-year-pdf.all-years { display: none; }
/* so is the data summary bar (sources, rows, Start again, Edit data), which
   sits above #results in the calculator section: app.js mirrors the shown
   section onto <body> for it */
body.results-sidebar:not([data-results-section="dashboard"]) #data-summary { display: none; }

@media (max-width: 900px) and (min-width: 641px) {
  body.results-sidebar { --sidebar-w: 64px; }
  #results.layout-sidebar #result-tabs-carousel { padding: 14px 8px; }
  #results.layout-sidebar .result-tabs button,
  #results.layout-sidebar .sidebar-action:not(.hidden) { justify-content: center; padding: 11px 0; }
  /* hidden from sight only: the name stays the button's accessible name */
  #results.layout-sidebar .result-tabs .tab-label,
  #results.layout-sidebar .sidebar-action .tab-label {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); white-space: nowrap;
  }
}

@media (max-width: 640px) {
  /* At phone width the picker is moved out of #results to the top of <main>,
     above the data summary (app.js placeSectionPicker), so these rules key
     on body.results-sidebar rather than on #results */
  body.results-sidebar #result-tabs-carousel {
    /* under the fixed 59px header.site, and above the tables it covers */
    position: sticky; top: 59px; z-index: 5;
    margin: 0 0 16px; padding: 8px 0;
    background: var(--bg); border: none; border-radius: 0;
  }
  /* the tab row's scroll arrows: nothing to scroll in a drop-down list */
  body.results-sidebar #result-tabs-carousel .carousel-arrow { display: none; }
  body.results-sidebar .section-picker {
    display: flex; align-items: center; gap: 10px; width: 100%;
    padding: 10px 14px; font: inherit; font-size: 15px; font-weight: 600;
    /* filled like the selected tax year (.year-tabs button.year.active): a
       pale tint was too easy to miss */
    color: var(--btn-text); background: var(--btn-primary);
    border: 1px solid var(--btn-primary); border-radius: var(--radius-sm);
    cursor: pointer; transition: background 0.2s ease;
  }
  body.results-sidebar .section-picker:hover { background: var(--btn-primary-hover); }
  body.results-sidebar .section-picker:focus-visible {
    outline: none; box-shadow: 0 0 0 2px var(--btn-focus-glow);
  }
  .section-picker-icon svg { display: block; }
  .section-picker-label { flex: 1; text-align: left; }
  .section-picker-chevron { transition: transform .2s; }
  #result-tabs-carousel.nav-open .section-picker-chevron { transform: rotate(180deg); }
  body.results-sidebar .result-tabs-scroll {
    display: none;
    position: absolute; left: 0; right: 0; top: calc(100% - 4px);
    max-height: 70vh; overflow-y: auto; padding: 6px;
    background: var(--surface);
    border: 1px solid var(--border); border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
  }
  body.results-sidebar #result-tabs-carousel.nav-open .result-tabs-scroll { display: block; }
  /* the list: one section per row, icon and name, as in the sidebar */
  body.results-sidebar .result-tabs {
    display: flex; flex-direction: column; gap: 2px;
    background: none; border: none; border-radius: 0; overflow: visible;
  }
  body.results-sidebar .result-tabs button {
    display: flex; align-items: center; gap: 12px;
    width: 100%; padding: 11px 12px; text-align: left;
    font-size: 15px; color: var(--text);
    border: none; border-radius: var(--radius-sm); white-space: normal;
  }
  body.results-sidebar .result-tabs .tab-icon { display: block; flex: none; color: var(--muted); }
  body.results-sidebar .result-tabs button.active,
  body.results-sidebar .result-tabs button.active:hover {
    color: var(--accent); background: var(--accent-soft); font-weight: 600;
  }
  body.results-sidebar .result-tabs button.active .tab-icon { color: var(--accent); }
}

/* upload and paste input panels: both start at the same size so toggling
   between them doesn't shift the page layout */
#panel-upload { min-height: 220px; display: flex; }
.dropzone {
  width: 100%;
  border: 2px dashed var(--faint); border-radius: 12px;
  background: var(--surface-2);
  padding: 48px 20px; text-align: center; cursor: pointer;
  transition: border-color .15s, background .15s;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
}
.dropzone:hover, .dropzone.dragover { border-color: var(--accent); background: var(--accent-soft); }
.dropzone-icon { display: block; color: #112057; opacity: .18; margin-bottom: 8px; }
/* #112057 (navy) reads as a faint watermark on the light dropzone background,
   but is nearly invisible against the dark theme's own navy-ish surface —
   swap to a light slate at higher opacity so it stays a visible watermark */
@media (prefers-color-scheme: dark) {
  html:not([data-theme]) .dropzone-icon { color: #cbd5e1; opacity: .35; }
}
html[data-theme="dark"] .dropzone-icon { color: #cbd5e1; opacity: .35; }
.dropzone p { margin: 6px 0; }
.dropzone .big { font-weight: 600; font-size: 16px; }
.dropzone .hint { color: var(--muted); font-size: 13.5px; }

/* ---------- the data card: Text Data / Preview ---------- */
/* One panel holding two views of the same data, so the raw text and the table
   the calculation will run on read as one object rather than two stacked
   blocks. The card owns the border, radius and shadow; everything inside it
   (including the preview's own .table-frame) is flat. */
.data-card {
  /* How tall the collapsed preview is, in rows. The row and header heights are
     pinned (see .pane-preview below) so this is exact rather than approximate,
     and the pane height falls out of it — which is also the text pane's height,
     so switching tabs never moves what is below the card. app.js reads
     --preview-rows back for the "Showing N of M rows" label, so the number
     lives in one place. */
  --preview-rows: 8;
  --preview-row-h: 40px;
  --preview-head-h: 45px;
  --card-pane-h: calc(var(--preview-head-h) + var(--preview-rows) * var(--preview-row-h));
  background: var(--surface);
  border: 1px solid var(--border); border-radius: 12px;
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}
.data-card-head {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 10px;
  padding: 12px 16px; border-bottom: 1px solid var(--border);
}
/* the inner tab strip is deliberately the same segmented control as the
   Upload/Paste row above it, at a smaller size — one shape, two scopes */
.data-card-head .pill { padding: 7px 14px; font-size: 13px; }
.data-card-hint { margin: 0; font-size: 13px; color: var(--muted); text-align: right; }
.data-card-hint b { color: var(--text); font-weight: 600; }

/* One height for both panes, derived from the same --preview-rows: switching
   tabs never moves what is below the card, and a drag on either footer's
   handle moves both at once. Eight rows leaves the text view a window of about
   sixteen lines. */
.pane-text { height: var(--card-pane-h); }
/* min-, not a fixed height: "Show all" grows the pane past it */
.pane-preview { min-height: var(--card-pane-h); }

/* the colouring layer and the textarea are stacked, so every metric that moves
   a glyph is set once here and inherited by both — a font-size or padding set
   on only one of them shows up immediately as drifting text. See
   renderHighlight() in app.js. */
.pane-text {
  position: relative;
  --code-lh: 21px;     /* one line of text, shared by the gutter, the <pre> and the textarea */
  --code-pad-y: 14px;
  --code-pad-x: 18px;
  --gutter-w: 54px;    /* room for an error mark and a four-digit row number */
}
.pane-text > pre, .pane-text > textarea {
  margin: 0; border: 0; width: calc(100% - var(--gutter-w)); height: 100%; box-sizing: border-box;
  padding: var(--code-pad-y) var(--code-pad-x);
  font-family: var(--mono); font-size: 13.5px; line-height: var(--code-lh);
  letter-spacing: 0; tab-size: 2;
  white-space: pre; word-spacing: normal; word-break: normal;
}

/* Row numbers, left of the text and outside the textarea, so they scroll down
   with it but never sideways (renderGutter in app.js). The bottom padding
   allows for the textarea's horizontal scrollbar, so the two can scroll the
   same distance. */
.code-gutter {
  position: absolute; top: 0; left: 0; bottom: 0; width: var(--gutter-w);
  box-sizing: border-box; overflow: hidden;
  padding: var(--code-pad-y) 0 calc(var(--code-pad-y) + 10px);
  background: var(--surface-2); border-right: 1px solid var(--border);
  font-family: var(--mono); font-size: 11.5px; line-height: var(--code-lh);
  color: var(--faint); text-align: right; user-select: none; pointer-events: none;
}
.gut-line {
  height: var(--code-lh); padding-right: 8px;
  display: flex; align-items: center; justify-content: flex-end; gap: 3px;
}
.gut-error { background: var(--loss-soft); color: var(--loss); font-weight: 600; }
.gut-error svg { flex-shrink: 0; }
/* behind: scrolled programmatically to follow the textarea, never by the user.
   The textarea keeps a permanent scrollbar gutter and the <pre> subtracts the
   same width, so the two have identical content boxes whether or not the
   scrollbar is currently drawn — otherwise a long line scrolls 10px further in
   one layer than the other and the colouring slides off the text. */
.pane-text > pre {
  position: absolute; top: 0; left: var(--gutter-w); width: calc(100% - var(--gutter-w) - 10px); overflow: hidden;
  color: var(--text); pointer-events: none;
  /* the textarea's horizontal scrollbar takes 10px off its height, letting it
     scroll 10px further down; this lets the layer follow it all the way */
  padding-bottom: calc(var(--code-pad-y) + 10px);
}
/* renderHighlight() writes one block per line: exactly one line high, so the
   layer stays in step with the textarea, inside a wrapper at least as wide as
   the longest line, so a tint reaches the right edge when scrolled sideways */
.hl-wrap { display: inline-block; min-width: 100%; vertical-align: top; }
.hl-line { display: block; min-height: var(--code-lh); }
/* the tint runs into the side padding too, so the whole line reads as marked */
.hl-error {
  background: var(--loss-soft);
  margin: 0 calc(-1 * var(--code-pad-x)); padding: 0 var(--code-pad-x);
}
/* in front: its own text is invisible, so what the user reads is the layer
   behind — but the caret and the selection band still have to be visible */
.pane-text > textarea {
  position: relative; display: block; resize: none; margin-left: var(--gutter-w);
  background: transparent; color: transparent; caret-color: var(--text);
  overflow: auto; scrollbar-gutter: stable; outline: none;
  scrollbar-width: thin; scrollbar-color: var(--faint) transparent;
}
.pane-text > textarea::placeholder { color: var(--muted); opacity: .6; }
.pane-text > textarea::selection { background: var(--select-bg); }
.pane-text > textarea::-webkit-scrollbar { width: 10px; height: 10px; }
.pane-text > textarea::-webkit-scrollbar-track { background: transparent; }
.pane-text > textarea::-webkit-scrollbar-thumb { background: var(--faint); border-radius: 6px; }
.pane-text > textarea::-webkit-scrollbar-thumb:hover { background: var(--muted); }

/* token colours reuse the palette the tables already use, so the dark theme
   follows without a second set of rules */
/* the header line: halfway between --muted and --faint, so the column names
   sit a step back from the data in both themes */
.tok-head  { color: color-mix(in srgb, var(--muted) 50%, var(--faint)); }
.tok-num   { color: var(--gain); }
.tok-text  { color: var(--text); }
.tok-delim { color: var(--faint); }

/* "there is more below" — hidden once the last line is on screen */
.pane-fade {
  position: absolute; left: 0; right: 10px; bottom: 0; height: 34px;
  background: linear-gradient(to bottom, transparent, var(--surface));
  pointer-events: none;
}

/* the preview's frame is already inside the card's, so it drops its own */
.pane-preview .table-frame { border: 0; border-radius: 0; box-shadow: none; }
/* Every row is in the table; the box is simply capped at --preview-rows of
   them and scrolls through the rest. "Show all" lifts the cap so the table
   grows to its full length instead. The explicit line-heights are what make a
   row exactly --preview-row-h tall (padding + line + border), so the cap lands
   on a row boundary rather than halfway through one. */
.pane-preview th { line-height: 20px; }
.pane-preview td { line-height: 19px; }
/* A height rather than a max-height, so the drag handle below has something to
   drag. max-content stops that height leaving dead space under a table with
   fewer rows than the cap. */
.pane-preview .scroll-x {
  height: calc(var(--preview-rows) * var(--preview-row-h));
  max-height: max-content;
}
/* nothing left to drag towards once every row is on screen */
.pane-preview .scroll-x.expanded { height: auto; max-height: none; }

/* The starting size: until the user drags a handle, the text area (and the
   Tabular Data view in its place) is the drop box's 220px (#panel-upload).
   The first drag adds .sized (app.js resizePanesTo), and from then on the
   --preview-rows sizing above applies instead. "Show all" still grows the
   Tabular Data view past 220px. */
.data-card:not(.sized) .pane-text { height: 220px; }
/* absolutely placed, so it fills the pane whatever the pane's height */
.data-card:not(.sized) .pane-text > textarea { position: absolute; top: 0; bottom: 0; left: 0; height: auto; }
.data-card:not(.sized) .pane-preview { height: 220px; min-height: 0; display: flex; flex-direction: column; }
.data-card:not(.sized) .pane-preview:has(.scroll-x.expanded) { height: auto; }
.data-card:not(.sized) .pane-preview .table-frame { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.data-card:not(.sized) .pane-preview .scroll-x:not(.expanded) { flex: 1 1 0; height: auto; max-height: none; min-height: 0; }

/* it's showing raw input rows, not a formatted results table */
.pane-preview td { font-family: var(--mono); font-size: 13px; }
.pane-preview td.rownum { font-size: 12px; }


/* the deeper bottom padding is the drag handle's room, below the row it
   shares with the count and "Show all rows". Both footers carry it, not just
   the preview's, so the card stays the same height whichever tab is up. */
.data-card-foot {
  position: relative;
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 12px 16px 22px;
  border-top: 1px solid var(--border); background: var(--surface-2);
  font-size: 13px; color: var(--muted);
}
/* what was read (trades, ERI and so on), formerly a green bar under the card;
   it takes the free space, pushing the link or button to the right */
.foot-summary { flex: 1; min-width: 0; }
/* too narrow for three items side by side: the summary takes a line of its
   own under the count and the link, rather than squeezing into a column */
@media (max-width: 560px) {
  .data-card-foot { flex-wrap: wrap; row-gap: 6px; }
  .foot-summary { order: 3; flex-basis: 100%; }
  .foot-summary:empty { display: none; }
}
.foot-summary-mark { color: var(--gain); font-weight: 700; }
.data-card-foot a { color: var(--accent); font-weight: 500; text-decoration: none; }
.data-card-foot a:hover { text-decoration: underline; }

/* Drag handle for the preview box, in the footer bar directly under the "Show
   all rows" link. A handle of our own rather than CSS `resize: vertical`: the
   browser's own grip cannot be styled in every browser, comes out at whatever
   size that browser picks, sits on top of the scrollbar corner, and drags to
   any height — this one snaps to whole rows, so the box still ends on a row
   boundary afterwards. */
.resize-grip {
  position: absolute; right: 8px; bottom: 2px;
  display: flex; align-items: center; justify-content: center;
  width: 18px; height: 16px; padding: 0;
  border: 0; background: none; color: var(--faint);
  cursor: ns-resize; touch-action: none;
}
.resize-grip:hover { color: var(--muted); }
.resize-grip:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 3px; }

/* a button that reads as a link — for actions that change what is on screen
   rather than navigating (Show all rows, the hint's "Text Data") */
.linkish {
  font: inherit; font-size: inherit; font-weight: 500;
  background: none; border: none; padding: 0;
  color: var(--accent); cursor: pointer;
}
.linkish:hover { text-decoration: underline; }

/* a column name that wasn't recognised is a nudge, not a warning — nothing is
   wrong with the data and the fix is a rename, so it reads as a quiet line
   under the summary rather than as a banner */
.input-note {
  display: flex; align-items: flex-start; gap: 8px;
  margin: 10px 2px 0; font-size: 13px; color: var(--muted);
}
.input-note svg { flex-shrink: 0; margin-top: 1px; color: var(--faint); }
.input-note code { font-family: var(--mono); font-weight: 600; color: var(--text); }

/* the only home of the Calculate button: on screen from the first visit, so
   it never moves when data arrives */
.calc-bar {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 12px;
  margin-top: 22px; padding-top: 22px;
}
.calc-bar-note { font-size: 13.5px; color: var(--muted); }

@media (max-width: 700px) {
  /* the head's two halves stack rather than sit opposite each other, so the
     hint reads from the same left edge as everything else on the card */
  .data-card-hint { text-align: left; }
  .data-card { --preview-rows: 6; }
}

/* upload/paste toggle + delimiter + calculate button, one row below the
   dropzone/textarea — the round-cornered-rectangle shape (--radius-sm)
   matches mockup-fintech-v3.html, distinct from the fully-rounded
   .pill-tabs used for the results view's own tab row */
/* right-aligned, and carrying the gap down to whichever panel follows it —
   the dropzone or the data card — so the two states space identically */
.upload-controls {
  display: flex; align-items: center; justify-content: flex-end;
  gap: 10px; flex-wrap: wrap;
  margin-top: 16px; margin-bottom: 16px;
}
/* Upload file / Paste data, and Text Data / Tabular Data — literally
   .year-tabs' and .result-tabs' recipe, with the two fills swapped.

   It used to be a rail: a --surface-2 tray with 4px of padding and a 4px gap,
   holding two free-floating buttons, the selected one lifting onto a white
   pill with a shadow. Two joined buttons instead — one container drawing the
   border and rounding only its OUTER corners, clipping the segments square
   where they meet, with a hairline between them. Same geometry as every other
   segmented control on the site, so a reader meets one control shape rather
   than two that nearly match.

   The swap: .year-tabs and .result-tabs sit white with the selected segment
   filled solid blue, because there the selection is the page's main subject —
   which tax year, which results view. These two only choose an input method,
   so a solid-blue segment would shout louder than the Calculate button below
   them. Grey tray, selected segment lifted to white instead: the same
   figure-and-ground the rail already used, and the same weight it had. */
.input-toggle {
  display: inline-flex; align-items: stretch;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.pill {
  font: inherit; font-size: 13.5px; font-weight: 500;
  padding: 10px 16px;
  color: var(--muted);
  background: none; border: none; border-right: 1px solid var(--border);
  border-radius: 0;
  cursor: pointer; white-space: nowrap;
  transition: color .15s, background .15s;
}
.pill:last-child { border-right: none; }
.pill:hover:not(.active):not(:disabled) { color: var(--text); }
.pill.active {
  color: var(--text); font-weight: 600;
  background: var(--surface);
}
/* DARK MODE INVERTS THE RELATIONSHIP, so it has to be stated separately.
   In light the selected segment is LIGHTER than the tray — white lifting off
   grey. Reusing --surface / --surface-2 in dark flips that: --surface
   (#171e2e) is DARKER than --surface-2 (#1f2739), so the selected segment
   sank into the tray instead of rising out of it, and at 1.12:1 between the
   two fills there was almost nothing left to see. Same tokens, opposite
   reading.

   So dark keeps the direction rather than the tokens: the tray drops to
   --surface and the selected segment lifts to a slate well above it, 1.71:1
   between the two, with the label brightening to #f8fafc (9.29:1 on it)
   against the tray's --muted. Written twice, as .hero and .dropzone-icon
   are — once for the OS preference, guarded so an explicit "light" choice
   still wins, once for the toggle. */
@media (prefers-color-scheme: dark) {
  html:not([data-theme]) .input-toggle { background: var(--surface); }
  html:not([data-theme]) .pill.active { background: #38445f; color: #f8fafc; }
}
html[data-theme="dark"] .input-toggle { background: var(--surface); }
html[data-theme="dark"] .pill.active { background: #38445f; color: #f8fafc; }

/* Tabular Data is disabled until there is data to tabulate. Without this it
   is indistinguishable from the merely-unselected segment beside it. */
.pill:disabled { color: var(--faint); cursor: default; }
/* inset, like .result-tabs': the container clips (overflow:hidden, for the
   outer corners), so an outer ring on the first or last segment would be
   sliced off by the very edge it needs to sit outside of */
.pill:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--btn-focus-glow);
}

/* the select's own selected-option text is hidden and replaced by the static
   ".delimiter-label" overlay, so the control always reads "Delimiter" instead
   of flipping to the picked value (comma/pipe/tab). A custom chevron is added
   via background-image so it remains visible while the text is transparent.
   The label sits on top, clicks pass through since pointer-events: none. */
.delimiter-row { position: relative; display: inline-flex; align-items: center; }
.delimiter-row select {
  /* appearance: none removes the browser's default arrow so we can add our own */
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  background: var(--surface) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23808080' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E") no-repeat;
  background-position: right 8px center; background-size: 12px;
  color: transparent; border: 1px solid var(--border);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-sm);
  width: 112px; padding: 8px 10px; padding-right: 28px;
  font: inherit; font-size: 13px; cursor: pointer;
}
.delimiter-row select:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
/* only the closed select's own text is hidden — <option> colour is reset so
   the native dropdown list itself still shows Comma/Pipe/Tab normally */
.delimiter-row select option { color: var(--text); }
.delimiter-row .delimiter-label {
  position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
  font-size: 13px; color: var(--muted); pointer-events: none;
}

button.primary {
  background: var(--btn-primary);
  color: var(--btn-text);
  border: none;
  font: inherit;
  font-weight: 600;
  font-size: 14.5px;
  padding: 10px 22px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background 0.2s ease, box-shadow 0.2s ease;
}
button.primary:hover {
  background: var(--btn-primary-hover);
}
button.primary:active {
  background: var(--btn-primary-active);
}
/* :focus-visible, not :focus. The ring is a box-shadow spread, so it is the
   button's own shape grown by 3px — corner radius included — in a translucent
   wash of the same blue as the fill. On a mouse press that does not read as a
   focus indicator at all: it reads as a second, larger button sitting behind
   the one being pressed, which is exactly how it was reported. A click focuses
   the button on mousedown, so the ring was up for as long as the press was
   held. :focus-visible keeps the ring where it earns its place (keyboard
   navigation, where it is the only thing saying where you are) and drops it
   for the pointer, which already knows. This is what the rest of the file
   does: .pill, .resize-grip, section.prose a, .footer-tagline a and a.cite are
   all :focus-visible, and so is button.calc-btn since BUG-127. */
button.primary:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--btn-focus-glow);
}
button.primary:focus-visible:active {
  background: var(--btn-primary-active);
  box-shadow: 0 0 0 3px var(--btn-focus-glow);
}
button.primary:disabled {
  opacity: 0.5;
  cursor: default;
  background: var(--btn-primary);
}

/* Calculate's two labels — "Calculate gains" and the spinner plus
   "Calculating…" — are both in the button (see index.html), stacked in one
   grid cell so the button is laid out to whichever of them is wider and
   keeps that width through the whole calculation. .calc-bar right-aligns the
   button, so a width that changed on click would slide its left edge out
   from under the cursor; measuring the resting width in JS and pinning it
   instead is not enough, because the busy label is the wider of the two and
   would wrap inside the pin. The idle label is hidden with visibility rather
   than display so it keeps holding its space open, and so screen readers are
   handed only the state that is actually showing. */
#calculate-btn { display: grid; justify-items: center; }
#calculate-btn > .btn-state {
  grid-area: 1 / 1;
  display: flex; align-items: center; white-space: nowrap;
}
#calculate-btn > .btn-state-busy { visibility: hidden; }
#calculate-btn[aria-busy="true"] > .btn-state { visibility: hidden; }
#calculate-btn[aria-busy="true"] > .btn-state-busy { visibility: visible; }

/* stands in for the collapsed input area once results exist (app.js
   hideInputArea) — a one-line receipt of what was loaded, plus the way back */
.data-summary {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; flex-wrap: wrap;
  margin-top: 16px; padding: 14px 20px;
  background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}
.data-summary-text {
  display: flex; align-items: center; gap: 10px;
  margin: 0; font-size: 14px; font-weight: 500;
}
.status-dot {
  width: 8px; height: 8px; flex: none;
  background: var(--gain); border-radius: 50%;
}
.data-summary-actions {
  display: flex; align-items: center; gap: 10px;
}
/* What the server said about the data: parse warnings, then what currency
   conversion did. Sits under the summary line and its buttons.

   `flex-basis: 100%` puts it on its own row of the flex container rather than
   competing with the buttons for width, so a long line wraps instead of
   squeezing "Edit data" off the end on a narrow screen. */
.data-summary-notes {
  flex-basis: 100%;
  margin: 0; padding-top: 10px;
  border-top: 1px solid var(--border);
  font-size: 13px; color: var(--muted);
}
.data-summary-notes p { margin: 0 0 4px; }
.data-summary-notes p:last-child { margin-bottom: 0; }
/* A warning is about a figure that was changed or dropped, so it is set in
   the app's red rather than as the muted aside the conversion summary is.
   `--loss` because it is the existing red and it is theme-aware: a darkened
   crimson on the light ground, a lighter one on the dark, where #b91c1c
   would be close to unreadable.

   Colour is not the only cue (TODO-020): every one of these carries a ⚠ from
   the markup, so the line still reads as a warning in monochrome, to a
   screen reader, and to anyone who cannot separate red from the body text. */
.data-summary-notes p.warn { color: var(--loss); font-weight: 500; }
/* a reminder about the data (TODO-065, a short sale), not a warning that a
   figure was changed: the accent of the pre-calculation .msg-notice */
.data-summary-notes p.notice { color: var(--accent); font-weight: 500; }
/* the chevron beside "Edit data" that folds the notes away: points up while
   they are open (click to close), down once closed */
/* a bare chevron, not a button: no border, fill or shadow, in the muted text
   colour so it sits quietly beside the two blue buttons */
button.ghost.notes-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 4px; border: none; background: none; box-shadow: none; color: var(--muted);
}
button.ghost.notes-toggle:hover { background: none; box-shadow: none; color: var(--text); }
button.ghost.notes-toggle:focus-visible { outline: 2px solid var(--btn-focus-glow); outline-offset: 2px; }
.notes-chevron { transition: transform .2s; }
.notes-toggle[aria-expanded="false"] .notes-chevron { transform: rotate(180deg); }
.data-summary-notes.collapsed { display: none; }

button.ghost, a.ghost {
  background: transparent; border: 1px solid var(--btn-secondary-border); color: var(--btn-secondary-text);
  font: inherit; font-size: 13px; font-weight: 500; padding: 7px 14px; border-radius: var(--radius); cursor: pointer;
  text-decoration: none; display: inline-block;
  transition: border-color .15s, background .15s, color .15s, box-shadow .15s;
}
button.ghost:hover, a.ghost:hover {
  border-color: var(--btn-secondary-border-hover); color: var(--btn-secondary-text-hover);
  background: var(--btn-secondary-bg-hover); box-shadow: var(--btn-secondary-shadow-hover);
}

/* Every take-it-away action — Download all (.zip), each table's Export CSV /
   Export TXT, Copy text, the spouse BUY download — sits on grey: the results
   head on --bg, the table header bars on --surface-2. A transparent .ghost
   against either is a rectangle of outline the eye skims past, which is a
   poor showing for the buttons that hand the user their own figures. So they
   are filled with the page's own card colour (white in light, the raised
   surface in dark) and carry the same small shadow the cards do, reading as
   a control sitting *on* the bar rather than drawn into it.

   The resting state is the fill; hover still darkens to
   --btn-secondary-bg-hover with the deeper shadow, so the change on hover is
   as visible as it ever was.

   Scoped to the three action containers, plus the expand button that floats
   over a header row when its table has no label bar (Fund income per tax
   year), rather than every .ghost: the
   "Show all N rows" button inside .table-more is deliberately styled as a
   link (see .table-frame > .table-more), and Start again / Edit data have
   their own accent treatment below. */
.results-head .ghost,
.table-head .ghost,
.table-actions .ghost,
.table-frame > .expand-btn.floating {
  background: var(--surface);
  box-shadow: var(--shadow-sm);
}

/* Start again / Edit data on the calculator: filled blue like button.primary
   (owner, 2026-10-21), matching the same two buttons in the results sidebar.
   They keep the .ghost size. */
#edit-data, #start-again {
  background: var(--btn-primary); border: 1px solid var(--btn-primary); color: var(--btn-text);
  font-weight: 600;
}
#edit-data:hover, #start-again:hover {
  background: var(--btn-primary-hover); border-color: var(--btn-primary-hover); color: var(--btn-text);
}
#edit-data:active, #start-again:active {
  background: var(--btn-primary-active); border-color: var(--btn-primary-active);
}
#edit-data:focus-visible, #start-again:focus-visible {
  outline: none; box-shadow: 0 0 0 3px var(--btn-focus-glow);
}

/* "Calculate this example" on every worked example — same shape as
   button.primary (border-radius, font, padding) but filled with the site's
   literal brand amber rather than the calculator blue, since these buttons
   sit on the marketing/docs pages rather than the calculator itself. The
   amber is deliberately the same literal #F0A02C used elsewhere on the page
   (see .brand-engine, .hero), not a token, for the same reason those are
   literal: it is a brand colour, not a themed one. */
button.calc-btn {
  background: #F0A02C; color: #FFFFFF; border: none;
  font: inherit; font-weight: 600; font-size: 14.5px;
  padding: 10px 24px; border-radius: var(--radius-sm);
  cursor: pointer; transition: background 0.2s ease, box-shadow 0.2s ease;
}
button.calc-btn:hover { background: #D98E22; }
button.calc-btn:active { background: #C27F1E; }
/* :focus-visible, not :focus, for the reason given at button.primary above.
   Here the stale ring was the reported fault: "Add another file" opens the
   file dialog, and cancelling it hands focus back to the button, so a plain
   :focus left the amber ring round it after a mouse click (BUG-127).
   :focus-visible keeps the ring for keyboard users only. */
button.calc-btn:focus { outline: none; }
button.calc-btn:focus-visible { box-shadow: 0 0 0 3px rgba(240, 160, 44, 0.35); }
button.calc-btn:focus-visible:active { background: #C27F1E; box-shadow: 0 0 0 3px rgba(240, 160, 44, 0.35); }
button.calc-btn:disabled { opacity: 0.5; cursor: default; background: #F0A02C; }
/* Dark: the light-mode amber is bright enough to glow on a near-black page,
   so dark mode gets a darker step of the same hue instead — one shade per
   state, same relationships (hover darker still, active darker again). */
@media (prefers-color-scheme: dark) {
  html:not([data-theme]) button.calc-btn,
  html:not([data-theme]) button.calc-btn:disabled { background: #A6690F; }
  html:not([data-theme]) button.calc-btn:hover { background: #8F5A0C; }
  html:not([data-theme]) button.calc-btn:active,
  html:not([data-theme]) button.calc-btn:focus-visible:active { background: #7A4C0A; }
}
html[data-theme="dark"] button.calc-btn,
html[data-theme="dark"] button.calc-btn:disabled { background: #A6690F; }
html[data-theme="dark"] button.calc-btn:hover { background: #8F5A0C; }
html[data-theme="dark"] button.calc-btn:active,
html[data-theme="dark"] button.calc-btn:focus-visible:active { background: #7A4C0A; }
/* the button sits right-aligned above its own result block, which stays a
   full-width block below it regardless of the text-align on its parent */
.example-calc { text-align: right; margin-top: 12px; }
/* the right alignment is for the Calculate button only: the results below it
   are the calculator's own tables, so text sits left and numbers right (td.num)
   as on index.html */
.example-calc .calc-result { text-align: left; }

.banner { border-radius: var(--radius); padding: 12px 16px; margin-top: 16px; font-size: 13.5px; }
.banner.error { background: var(--loss-soft); color: var(--loss); }
.banner.warn { background: var(--accent-soft); color: var(--accent); }
.banner.ok { background: var(--gain-soft); color: var(--gain); }
.banner ul { margin: 6px 0 0; padding-left: 20px; }
/* preview banner + calc-status banner sit side by side, each keeping its
   own colour/message, grouped inside one outer cell — same recipe as
   .input-toggle (a --surface-2 container, bordered and radius-sm, holding
   the Upload/Paste segments) so the two grouped rows in this UI read as one
   system */
.status-row {
  display: flex; column-gap: 0; row-gap: 6px; flex-wrap: wrap;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.status-row > * { flex: 1 1 240px; }
.status-row .banner { margin-top: 0; }
.status-row > #calc-status:empty { display: none; }
/* the data summary moved to the card's footers, so a row holding only an
   empty #calc-status has nothing to show */
.status-row:not(:has(> :not(#calc-status:empty))) { display: none; }
/* gap between the data card and the status group below it — there's
   otherwise nothing providing space here, since .status-row's own margin-top
   is zeroed out above */
#input-status:not(:empty) { margin-top: 16px; }

/* blocking data errors, between the status row and Calculate (TODO-086):
   where the user is already looking, unlike the old top-of-page ribbon. Every
   message is listed, so a long list scrolls inside the panel. */
.msg-panel {
  margin-top: 10px; padding: 12px 16px;
  border: 1px solid var(--loss); border-radius: var(--radius);
  background: var(--loss-soft); color: var(--loss); font-size: 13.5px;
}
.msg-panel-head { display: flex; align-items: flex-start; gap: 8px; font-weight: 600; }
.msg-panel-head svg { flex-shrink: 0; margin-top: 2px; }
.msg-panel ul { margin: 6px 0 0; padding: 0 0 0 42px; max-height: 220px; overflow-y: auto; }
.msg-panel li { margin-bottom: 3px; }
.msg-panel li b { font-weight: 600; }
/* a reminder about the data, not an error (TODO-065, a sale bigger than the
   holding): it never blocks Calculate, so it takes the calm accent of
   .banner.warn rather than the red of a blocking error. The warning sign in
   the markup carries the meaning without the colour (TODO-020). */
.msg-panel.msg-notice {
  border-color: var(--accent); background: var(--accent-soft); color: var(--accent);
}

/* fixed top-of-page ribbon. The calculator no longer uses it (its data errors
   moved to .msg-panel above); the feedback admin page still does, for its
   Turnstile warnings (app/feedback/admin.py, board.css). Lives on its own
   fixed layer and unrolls into view via a transform transition rather than a
   display toggle, since display:none can't be animated. */
.error-ribbon {
  position: fixed; top: 0; left: 0; right: 0; z-index: 50;
  display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
  background: var(--loss-soft); border-bottom: 1px solid var(--loss);
  padding: 14px 24px;
  transform: translateY(-100%);
  transition: transform .35s cubic-bezier(.22, 1, .36, 1);
}
.error-ribbon.open { transform: translateY(0); box-shadow: 0 8px 20px -8px rgba(0,0,0,.25); }
.error-ribbon-inner { display: flex; align-items: flex-start; gap: 12px; }
.error-ribbon-icon { color: var(--loss); flex-shrink: 0; margin-top: 1px; }
.error-ribbon-title { color: var(--loss); font-weight: 700; font-size: 16px; }
.error-ribbon-detail { color: var(--muted); font-size: 13.5px; margin-top: 3px; }
.error-ribbon-detail ul { margin: 4px 0 0; padding-left: 18px; }
.error-ribbon-detail li { margin-bottom: 2px; }
.error-ribbon-close {
  background: transparent; border: none; color: var(--muted); cursor: pointer;
  padding: 4px; border-radius: 6px; flex-shrink: 0;
}
.error-ribbon-close:hover { background: var(--surface-2); color: var(--text); }

.table-head {
  display: flex; align-items: center; justify-content: space-between; padding: 12px 16px;
  background: var(--surface-2); font-size: 12.5px; color: var(--muted);
}
/* header bars carrying an action (Export CSV) sit opposite the table's own
   "Disposals (43 rows)" label, with tighter padding above and below it.

   The button inside is a plain .ghost, exactly as Copy text and Download
   spouse BUY rows are in .table-actions — it used to be shrunk here (5px/12px
   at 12.5px) to hold the bar to a label-only bar's height, but that bought a
   couple of pixels of bar at the price of two sizes of the same button
   appearing one tab apart. The bar is now ~5px taller than a label-only one
   and every take-it-away action in the results is the same size. */
.table-head.has-action { padding-top: 8px; padding-bottom: 8px; }

/* ONE HEIGHT FOR EVERY TAB'S EXPORT.

   Switching results tabs used to move the export button up and down the page:
   16px below the panel top on HMRC boxes (.panel-head-bar's own padding),
   8px on the five table tabs (.has-action's tighter padding, with the bar
   flush against the top of the panel), 0 on Calculation text, and — worst —
   207px on Fund income, whose export hung off the second of two tables.
   Fund income's now sits in a .panel-head-bar of its own (see index.html), and
   these two rules bring the rest down onto the same 16px line, so the button
   stays put as the reader moves between tabs.

   Written as an offset on the panel's first block rather than as extra padding
   inside .table-head, so the header bars keep the compact height they share
   with every label-only bar — only where the block starts changes.

   THE NUMBERS LOOK TOO BIG BECAUSE THEY COLLAPSE. A results panel has no top
   padding or border, so its first child's top margin collapses straight
   through it and meets .carousel's own 16px margin-bottom; the two do not add,
   the larger simply wins. So each value below is the whole distance from the
   tab bar, not an addition to it — 24 for a .table-frame (24 + .has-action's
   8px padding = 32) and 32 for a bare .table-actions (no padding of its own).
   Anything under 16 would be swallowed by the carousel's margin and do
   nothing, which is exactly what an 8 here did. A panel led by a
   .panel-head-bar needs no rule: it keeps the carousel's 16 and its bar's own
   16px padding-top makes the same 32. */
#results-panel > div > .table-frame:first-child { margin-top: 24px; }
#results-panel > div > .table-actions:first-child { margin-top: 32px; }
/* .table-frame wraps a fixed (non-scrolling) header table plus a separately
   scrolling body table. A sticky <th> inside the same overflow box as the
   rows looks like it should stay clear of the vertical scrollbar, but the
   scrollbar's track/thumb still spans the *whole* scrolling box (including
   the header's own space in normal flow) — every browser does this, since
   the native scrollbar isn't aware of position:sticky. Physically splitting
   the header out is the only way to actually keep the scrollbar off it.
   Column widths can't sync automatically across two separate tables, so
   syncFrameColumnWidths() in app.js measures the body table's rendered
   column widths and locks both tables to matching pixel widths. */
.table-frame {
  display: block; border: 1px solid var(--border); border-radius: 12px; overflow: hidden;
  background: var(--surface); box-shadow: var(--shadow-sm);
}
table.data.data-head { margin: 0; }
/* the scrollbar (10px, set below) eats into the body table's available
   width but not the header's — reserve the same width here so both tables
   measure/render against the same content width and columns line up */
.data-head-wrap {
  padding-right: 10px;
  /* match the header row's own background into the reserved gutter —
     otherwise it's a blank patch sitting directly above the scrollbar,
     which visually reads as the scrollbar bleeding up into the header */
  background: var(--surface-2);
  /* no visible scrollbar of its own (the body's is the only one the user
     drags) — but still a scroll container, so app.js can mirror the body's
     scrollLeft onto it. Without this a table wider than the frame would
     just have its trailing header cells clipped by .table-frame's
     overflow:hidden as the body scrolls underneath them. */
  overflow-x: hidden;
}

table.data {
  width: 100%; border-collapse: collapse; font-size: 12.5px;
  font-variant-numeric: tabular-nums;
}
/* --muted, not --faint: at 12px, uppercase and letter-spaced, --faint on
   --surface-2 came to 2.75:1 — under the 4.5:1 WCAG AA floor (this is not
   "large text", which starts at 18.66px bold). --muted clears it at 5.23:1
   light / 6.00:1 dark, and being a token it moves the right way in both
   themes — darker on the light surface, lighter on the dark one. Still well
   short of --text, so headers stay subordinate to the figures they label. */
table.data th {
  text-align: left; font-weight: 600; font-size: 12px; text-transform: uppercase;
  letter-spacing: .05em; color: var(--muted);
  padding: 12px 10px; background: var(--surface-2); border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
table.data td { padding: 10px 10px; border-top: 1px solid var(--border); white-space: nowrap; }
/* 10px either side, down from 16, so wide tables scroll sideways less
   (TODO-094 O1); the first column keeps 16px on its left so the table's text
   still lines up with the label bar above it (.table-head's 16px) */
table.data th:first-child, table.data td:first-child { padding-left: 16px; }
/* the hovered row in light blue, the multi-file list's selected-file colour:
   the grey it used to be (--surface-2) is also the header's and the balance
   rows' colour, so a hovered row read as one of those */
table.data tbody tr:hover td { background: var(--accent-soft); }
/* .num is for figures; .right is the same alignment claimed for something
   that isn't one — the input preview's date column, where a fixed-width
   dd/mm/yyyy is scanned down the column the way a figure is */
table.data td.num, table.data th.num,
table.data td.right, table.data th.right { text-align: right; }
/* The trailing spare column every scrolling table gets (see renderScrollTable).
   table-layout:auto hands the free width to a cell asking for 100% of it
   rather than sharing it out among all of them, which leaves each real column
   at the width of what it holds instead of stretched to fill the frame. It
   keeps the row and header bands running the full width, which simply ending
   the table short of it would not. */
table.data th.filler, table.data td.filler { width: 100%; padding: 0; }
/* leading row-number column — the input preview's and the Remaining positions
   table's: a grey gutter, --surface-2 on the cells, the same fill the header
   band already uses, so the column reads as one continuous strip from the "ROW"
   label down rather than as a data column. Right-aligned, muted text, and
   width:1% so it shrinks to its digits instead of pushing the real columns
   around. What the numbers mean is per table — the preview carries the row
   numbers from validation messages (header row 0, first transaction row 1),
   Remaining positions simply counts the positions listed.

   Two rules deliberately outrank this one, both by specificity: row hover
   (identical --surface-2, so the gutter simply stays put) and .row-error, which
   carries its tint across the gutter too — a flagged row reads as one band. */
table.data td.rownum, table.data th.rownum {
  text-align: right; color: var(--muted); width: 1%; padding-right: 14px;
  background: var(--surface-2);
}
table.data td.wrap, table.data th.wrap { white-space: normal; min-width: 220px; }
/* Remaining positions has only seven columns, so they are spaced wider apart
   than the busier tables' 10px, which leaves them easier to read across */
.remaining-frame table.data th, .remaining-frame table.data td { padding-left: 22px; padding-right: 22px; }
.remaining-frame table.data th:first-child, .remaining-frame table.data td:first-child { padding-left: 16px; }
.remaining-frame table.data th.filler, .remaining-frame table.data td.filler { padding: 0; }
/* Disposals: the frame's spare width shared out equally, half each side of
   every cell, so the gaps between columns are even. --even-pad is set by
   app.js syncFrameColumnWidths; the first column keeps its 16px left edge. */
.even-cols table.data th:not(.filler), .even-cols table.data td:not(.filler) {
  padding-left: calc(10px + var(--even-pad, 0px)); padding-right: calc(10px + var(--even-pad, 0px));
}
.even-cols table.data th:first-child, .even-cols table.data td:first-child { padding-left: 16px; }
/* Remaining positions' "Tax Year" heading rows: one full-width cell naming the
   year end the rows below it belong to, in the balance rows' band. Hover keeps
   the band, as it is a heading, not a position. */
table.data tr.year-sep td,
table.data tbody tr.year-sep:hover td { font-weight: 600; background: var(--surface-2); color: var(--text); }
@media (max-width: 700px) {
  /* three wrap columns at 220px each (660px) would force horizontal page
     scroll on a phone-width viewport — let cells shrink to fit instead.
     overflow-wrap handles long unbroken <code> identifiers (e.g.
     event) that white-space:normal alone can't wrap since
     they have no spaces to break at. */
  table.data td.wrap, table.data th.wrap { min-width: 0; overflow-wrap: break-word; }
}
/* preview rows with a blocking validation error (e.g. an invalid event
   value) — same colour as the error ribbon, until the row is fixed */
table.data tr.row-error td { background: var(--loss-soft); }
.scroll-x {
  overflow-x: auto; overflow-y: auto; max-height: 360px;
  scrollbar-width: thin; scrollbar-color: var(--faint) transparent; /* Firefox */
}
/* "Show all" state: grow the box to fit every row instead of scrolling
   within the collapsed-height box */
.scroll-x.expanded { max-height: none; }
.scroll-x::-webkit-scrollbar { width: 10px; height: 10px; }
.scroll-x::-webkit-scrollbar-track { background: transparent; }
.scroll-x::-webkit-scrollbar-thumb { background: var(--faint); border-radius: 6px; }
.scroll-x::-webkit-scrollbar-thumb:hover { background: var(--muted); }

.dir-buy { color: var(--gain); font-weight: 600; }
.dir-sell { color: var(--loss); font-weight: 600; }
.dir-eri { color: var(--accent); font-weight: 600; }
.dir-reorg { color: var(--purple); font-weight: 600; }
.dir-transfer { color: var(--teal); font-weight: 600; }
.dir-rights { color: var(--orange); font-weight: 600; }
/* notional distributions are fund income (grouped with ERI); equalisation is a
   capital distribution (grouped with rights sales) */
.dir-notional { color: var(--accent); font-weight: 600; }
.dir-equalisation { color: var(--orange); font-weight: 600; }
/* a user-reported loss, not a computed one — shares the loss colour used for
   negative gains throughout the results */
.dir-loss { color: var(--loss); font-weight: 600; }
.gain-pos { color: var(--gain); }
.gain-neg { color: var(--loss); }
.text-eri { color: var(--accent); }
.text-accent { color: var(--accent-light); }

.year-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 12px; margin: 16px 0 20px; }
.year-card {
  background: var(--surface-2); border: 1px solid transparent; border-radius: var(--radius);
  padding: 14px 16px; cursor: pointer; text-align: left; font: inherit; color: var(--text);
}
.year-card.cgt { background: var(--gain-soft); }
.year-card:hover { border-color: var(--faint); }
.year-card.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.year-card .label { font-size: 12.5px; color: var(--muted); }
.year-card .value { font-size: 21px; font-weight: 650; font-variant-numeric: tabular-nums; }
.year-card .meta { font-size: 12px; color: var(--muted); }

.metric-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(128px, 1fr)); gap: 9px; margin-bottom: 16px; font-family: "DM Sans", sans-serif; }
/* white card + coloured left border, replacing the old solid pastel fill —
   the accent still identifies each box's kind, just without painting the
   whole box; the border also brightens to the same accent on hover to
   signal "informational", not a filled block */
.metric {
  background: var(--surface); border: 2px solid var(--border);
  border-left: 6px solid var(--border); border-radius: var(--radius); padding: 10px 12px;
  display: flex; flex-direction: column;
  transition: border-color .15s;
}
.metric.cgt        { border-left-color: var(--gain); }
.metric.eri        { border-left-color: var(--accent); }
.metric.pc         { border-left-color: var(--purple); }
.metric.gr         { border-left-color: var(--loss); }
.metric.disposals  { border-left-color: var(--teal); }
.metric.chg        { border-left-color: var(--orange); }
.metric:hover           { border-color: var(--faint); }
.metric.cgt:hover       { border-color: var(--gain); }
.metric.eri:hover       { border-color: var(--accent); }
.metric.pc:hover        { border-color: var(--purple); }
.metric.gr:hover        { border-color: var(--loss); }
.metric.disposals:hover { border-color: var(--teal); }
.metric.chg:hover       { border-color: var(--orange); }
.metric-grid + .metric-grid { margin-top: -8px; }
.table-more { margin-top: 12px; }
/* "Show all N rows" toggle, anchored to the bottom of its table rather than
   floating below the frame — attached as the .table-frame's last child so
   the border/background read as part of the table, not a separate block.
   The input preview keeps its own row toggle in the card footer instead, so
   this covers the results tables only. */
.table-frame > .table-more {
  margin-top: 0;
  border-top: 1px solid var(--border);
  background: var(--surface-2);
  padding: 12px 16px;
  text-align: center;
}
.table-frame > .table-more button.ghost {
  background: none; border: none; box-shadow: none; padding: 0;
  font-size: 13px; font-weight: 500; color: var(--accent);
}
.table-frame > .table-more button.ghost:hover {
  background: none; border-color: transparent; text-decoration: underline;
  filter: brightness(.92);
}
/* right-aligned row of actions under a table (kept for mockup-dashboard-results
   .html; the results tables' own Export CSV now lives in .table-head) */
.table-below { display: flex; align-items: center; justify-content: flex-end; gap: 12px; margin-top: 12px; }
/* fixed two-line label zone (top-aligned) so every box's first value
   starts at the same horizontal level regardless of label length */
.metric .label {
  font-size: 13px; color: var(--muted); line-height: 1.3;
  min-height: 2.3em; display: flex; align-items: flex-start;
}
.metric .value { font-size: 18px; font-weight: 700; font-variant-numeric: tabular-nums; }
/* secondary caption (e.g. "after £22,624 exemption") sits directly under the
   headline figure rather than being pushed to the bottom of the card */
.metric .meta { font-size: 13px; color: var(--muted); margin-top: 4px; }

/* ---------- results dashboard cards ------------------------------------
   The calculator's own two card grids — the top KPI row (#summary-general,
   config.ini [tax_year_breakdown] HEADLINERS) and the per-year grid
   (#year-metrics, CARDS) — share one roomier treatment: a plain white card
   with an uppercase label and a large figure, no coloured kind accent (the
   value's own green/red already carries gain vs loss). Deliberately scoped
   to those two ids so the compact .metric default still serves the examples
   worked-example boxes, which pack many more cards into one grid.
   Every rule below is presentation only: which cards appear, and in which
   grid, stays entirely config.ini's call. ---------------------------- */
#summary-general, #year-metrics {
  /* 220px, not the reference design's 250px: at the container's ~1020px
     content width 250px only fits three cards and strands the fourth */
  display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 16px; margin: 0 0 16px;
}
/* Below the width that fits four 220px cards (4 x 220 + 3 x 16 gaps =
   928px) the two grids merge into one: .dash-cards-grid takes over the
   grid and the two grids drop out of the layout (display: contents), so
   their cards flow on in order (3 + 3 + 2) instead of each grid leaving
   a single card stranded on its own row (3 + 1 + 3 + 1). */
.dash-cards { container-type: inline-size; }
@container (max-width: 927.98px) {
  .dash-cards-grid {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 16px; margin: 0 0 16px;
  }
  .dash-cards-grid > #summary-general,
  .dash-cards-grid > #year-metrics { display: contents; }
}
#summary-general .metric, #year-metrics .metric {
  background: var(--surface);
  border: 1px solid var(--border); border-left: 1px solid var(--border);
  border-radius: 12px; padding: 20px;
  box-shadow: var(--shadow-sm);
  transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
  /* content stays top-aligned under the fixed two-row label rather than
     spreading to fill the card, so every headline figure in a row starts
     at the same height whether or not its card carries a meta caption */
  justify-content: flex-start;
}
#summary-general .metric:hover, #year-metrics .metric:hover {
  border-color: var(--faint);
  transform: translateY(-3px);
  box-shadow: var(--shadow-lg);
}
/* the metric name on one line, its tax-year context (.lbl-year, added by
   app.js) on a second — two rows are reserved either way, so the figures
   line up across cards whose labels carry no year (open positions, total
   proceeds) */
#summary-general .metric .label, #year-metrics .metric .label {
  display: block; min-height: 2.7em; margin-bottom: 12px;
  font-size: 13.5px; font-weight: 600; line-height: 1.35;
  text-transform: uppercase; letter-spacing: .05em;
}
#summary-general .lbl-year, #year-metrics .lbl-year { display: block; }
#summary-general .metric .value, #year-metrics .metric .value {
  font-size: 28px; line-height: 1.2; color: #1E293B;
}
@media (prefers-color-scheme: dark) {
  html:not([data-theme]) #summary-general .metric:not(.highlight) .value:not(.gain-pos):not(.gain-neg),
  html:not([data-theme]) #year-metrics .metric:not(.highlight) .value:not(.gain-pos):not(.gain-neg) { color: #F1F5F9; }
}
html[data-theme="dark"] #summary-general .metric:not(.highlight) .value:not(.gain-pos):not(.gain-neg),
html[data-theme="dark"] #year-metrics .metric:not(.highlight) .value:not(.gain-pos):not(.gain-neg) { color: #F1F5F9; }
#summary-general .metric .value.gain-pos, #year-metrics .metric .value.gain-pos { color: var(--gain); }
#summary-general .metric .value.gain-neg, #year-metrics .metric .value.gain-neg { color: var(--loss); }


#summary-general .metric .meta, #year-metrics .metric .meta { font-size: 13.5px; margin-top: 8px; }

/* two-figure cards (proceeds/costs, gross gains/losses): one labelled row
   per figure rather than two stacked bare numbers */
.dual-metric { display: flex; flex-direction: column; gap: 8px; }
.dual-row { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
#summary-general .dual-row .value, #year-metrics .dual-row .value { font-size: 20px; }
.dual-row .lbl-sm {
  font-size: 12px; font-weight: 600; color: var(--muted);
  text-transform: uppercase; letter-spacing: .04em;
}

/* the one emphasised card — the chargeable gain, the figure the whole
   calculation exists to produce. Gated behind config.ini
   [tax_year_breakdown] HIGHLIGHT (app.js sets the class), and follows the
   chargeable card into whichever grid it is configured for. */
#summary-general .metric.highlight, #year-metrics .metric.highlight {
  background: var(--hero-bg); border-color: #c7d2fe;
}
#summary-general .metric.highlight:hover, #year-metrics .metric.highlight:hover {
  background: var(--hero-bg); border-color: #F0A02C;
}
#summary-general .metric.highlight .label, #year-metrics .metric.highlight .label,
#summary-general .metric.highlight .value, #year-metrics .metric.highlight .value { color: #fff; }
#summary-general .metric.highlight .meta, #year-metrics .metric.highlight .meta { color: rgba(255, 255, 255, .66); }
@media (prefers-color-scheme: dark) {
  html:not([data-theme]) #summary-general .metric.highlight,
  html:not([data-theme]) #year-metrics .metric.highlight { border-color: var(--accent); }
  html:not([data-theme]) #summary-general .metric.highlight:hover,
  html:not([data-theme]) #year-metrics .metric.highlight:hover { border-color: #F0A02C; }
}
html[data-theme="dark"] #summary-general .metric.highlight,
html[data-theme="dark"] #year-metrics .metric.highlight { border-color: var(--accent); }
html[data-theme="dark"] #summary-general .metric.highlight:hover,
html[data-theme="dark"] #year-metrics .metric.highlight:hover { border-color: #F0A02C; }

.summary-note { font-size: 11px; color: var(--muted); line-height: 1.4; margin: 20px 0 0; }

/* small hover-hint affordance: dotted underline on the labelled text plus a
   round "?" badge, both carrying the same native title tooltip */
.hintmark { border-bottom: 1px dotted var(--faint); cursor: help; }
.hint-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 13px; height: 13px; margin-left: 3px;
  border: 1px solid var(--faint); border-radius: 50%;
  color: var(--faint); font-size: 9px; font-weight: 700; line-height: 1;
  vertical-align: middle; cursor: help;
}
.summary-note code { font-size: 0.95em; }

/* ---------- tax year tabs (single-select: one year, or "All Years") ----------
   a segmented control sitting on the results header row, opposite the
   title: one bordered strip of buttons divided by hairlines, the selected
   year filled in the brand accent */
.year-tabs {
  display: inline-flex; align-items: stretch;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.year-tabs button.year {
  font: inherit; font-size: 14px; font-weight: 500;
  padding: 8px 16px;
  color: var(--muted);
  background: none; border: none; border-right: 1px solid var(--border);
  border-radius: 0;
  cursor: pointer; white-space: nowrap; flex: none;
  font-variant-numeric: tabular-nums;
  transition: color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
}
.year-tabs button.year:last-child { border-right: none; }
/* held on All Years by a tab whose table covers every year (app.js
   ALL_YEARS_TABS): still listed, but plainly not clickable */
.year-tabs button.year:disabled,
.year-tabs button.year:disabled:hover { opacity: 0.45; cursor: not-allowed; background: none; color: var(--muted); }
.year-tabs button.year:hover:not(.active) { color: var(--text); background: var(--surface-2); }
.year-tabs button.year.active {
  color: var(--btn-text);
  background: var(--btn-primary);
}
.year-tabs button.year.active:hover {
  background: var(--btn-primary-hover);
}
.year-tabs button.year.active:focus {
  outline: none;
  box-shadow: 0 0 0 2px var(--btn-focus-glow);
}
/* --accent is a light indigo in dark mode, where white-on-accent has too
   little contrast — the selected segment flips to dark text instead */
@media (prefers-color-scheme: dark) {
  html:not([data-theme]) .year-tabs button.year.active { color: var(--btn-text); }
}
html[data-theme="dark"] .year-tabs button.year.active { color: var(--btn-text); }

/* remaining metrics (config.ini [tax_year_breakdown] STRIP) — inline text
   instead of a full card */
/* label colour is nudged one step past --muted: at 13px on --bg these read as
   captions and were sitting lighter than they need to. Deliberately still well
   short of --text (14.32:1), which the <b> values below use — the labels have
   to stay subordinate to the figures they introduce. A raw hex with both dark
   hooks rather than a new token, matching how the other one-off theme colours
   in this file are handled; the greyscale ramp has nothing between --muted and
   --text to reuse. */
.year-strip {
  display: flex; flex-wrap: wrap; gap: 8px 26px;
  margin: 0 0 32px; font-size: 13px; color: #515a6a;
}
@media (prefers-color-scheme: dark) {
  html:not([data-theme]) .year-strip { color: #a6b0c1; }
}
html[data-theme="dark"] .year-strip { color: #a6b0c1; }
.year-strip b { font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }
/* semantic value colour (gain/loss/fund-income) must win over the default
   .year-strip b text colour above — same specificity order otherwise loses */
.year-strip b.gain-pos { color: var(--gain); }
.year-strip b.gain-neg { color: var(--loss); }
.year-strip b.text-eri { color: var(--accent); }
.year-strip b.text-accent { color: var(--accent-light); }

/* ---------- horizontal carousel (year tabs row, if it overflows) ---------
   plain flex + overflow-x scroll, with prev/next arrows that only appear
   once the track's content is wider than the viewport ------------------- */
.carousel { display: flex; align-items: center; gap: 2px; margin-bottom: 16px; }
.carousel-track {
  flex: 1; min-width: 0;
  overflow-x: auto; scroll-behavior: smooth; scrollbar-width: none;
}
.carousel-track::-webkit-scrollbar { display: none; }
/* the year-tab track sizes to its buttons (max-width, so it can still
   shrink and scroll) rather than filling its row */
.carousel-track.year-tabs { flex: 0 1 auto; max-width: max-content; margin-bottom: 0; }
/* the selector's own row, between the results header and the cards it scopes */
#year-tabs-carousel { margin-bottom: 24px; max-width: 100%; min-width: 0; }
.carousel-arrow {
  display: none; flex: none; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 99px; margin-top: -8px;
  border: 1px solid var(--border); background: var(--surface); color: var(--muted);
  cursor: pointer; transition: border-color .15s, background .15s, color .15s;
}
.carousel-arrow:hover:not(:disabled) { border-color: var(--accent); background: var(--accent-soft); color: var(--text); }
.carousel-arrow:disabled { opacity: .3; cursor: default; }
.carousel-arrow svg { display: block; }
.carousel.has-overflow .carousel-arrow { display: inline-flex; }
#year-tabs-carousel .carousel-arrow { margin-top: 0; }

/* results title on the left, the "download everything" action right-aligned
   opposite it — the tax-year selector sits on its own row below (see
   #year-tabs-carousel), since it re-scopes the whole dashboard rather than
   belonging to the title */
.results-head {
  display: flex; justify-content: space-between; align-items: center;
  gap: 16px; flex-wrap: wrap; margin-bottom: 16px;
}
.results-head h2 { margin: 0; }
.results-head .actions { display: flex; gap: 8px; }
/* the selected year's "2024-2025 Report", amber (button.calc-btn) at the
   size of the ghost buttons beside it */
.results-head button.btn-year-pdf { font-size: 13px; padding: 8px 16px; border-radius: var(--radius); }

/* follows the page theme like every other panel — white in light mode,
   not the fixed dark console box this used to be */
pre.report {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  color: var(--text);
  padding: 16px; font-family: var(--mono); font-size: 13px; line-height: 1.6;
  overflow-x: auto; white-space: pre; max-height: 520px; overflow-y: auto;
  /* examples.html nests this inside .example-calc, which right-aligns its own
     "Calculate this example" button — text-align:right would otherwise
     inherit down here too. Harmless with white-space:normal, but pre's hard
     line breaks turn it into a real bug: each unwrapped line becomes its own
     right-aligned line box, so every line shorter than the widest one shifts
     right by the difference instead of lining up at the left edge. */
  text-align: left;
}
pre.report .c-head { color: var(--text); font-weight: 700; }
pre.report .c-rule { color: var(--border); }
/* a box-drawn table (the rate periods): pre.report's 1.6 line height leaves
   gaps in its vertical lines, so the table alone sits tighter. inline-block,
   because line-height on an inline span does not shrink its line boxes. */
pre.report .c-box  { display: inline-block; line-height: 1.2; }
pre.report .c-key  { color: var(--muted); }
pre.report .c-pos  { color: var(--gain); }
pre.report .c-neg  { color: var(--loss); }
pre.report .c-tag  { color: var(--accent-light); }

.table-actions { display: flex; justify-content: flex-end; margin: 0 0 8px; }

.chart-title { font-size: 16px; margin: 4px 0 8px; }
.chart-wrap svg { width: 100%; height: auto; display: block; }
/* side-by-side pairing for the tax-year and by-code charts — each column's
   own .chart-title sits directly above its .chart-wrap, same as a full-width
   chart, just at half the horizontal space; stacks back to one column below
   the same 700px breakpoint used elsewhere for narrow viewports.
   align-items:flex-start (rather than the flex default, stretch) keeps the
   two cards independently top-aligned instead of stretched to match
   heights when they're side by side — but that same rule, read as a
   cross-axis alignment once flex-direction flips to column, would leave
   each stacked card sized to its own content width instead of filling the
   row, which is why the stacked override below switches it back to
   stretch so both cards end up the same full width as every other card
   on the page.
   Since TODO-099 the two cards in a row are the same height after all:
   Cumulative realised P&L and the Matching rules donut have intros of
   different lengths, and the owner wants a row's cards to match. Each card
   fills its column, and its chart is pushed to the bottom, so the two charts
   (same canvas size) also line up with each other. */
.chart-row { display: flex; gap: 28px; align-items: stretch; }
.chart-row .chart-col { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; }
.chart-row .chart-col > .chart-card { flex: 1 1 auto; display: flex; flex-direction: column; }
.chart-row .chart-col > .chart-card > .chart-wrap { margin-top: auto; }
/* Cumulative realised P&L and the matching rules donut fill whatever height
   their row gives them, rather than sitting at the bottom of a taller card.
   Each keeps its own 400:280 shape as the minimum (aspect-ratio as the flex
   basis), then grows. The P&L SVG is laid over its wrap, so it never adds to
   the height it is measured against; app.js fitCumulativeChart redraws it at
   the wrap's shape. */
#chart-cumulative, #chart-rules { margin-top: 0; flex: 1 1 auto; }
#chart-cumulative { position: relative; aspect-ratio: 400 / 280; }
#chart-cumulative > svg { position: absolute; inset: 0; width: 100%; height: 100%; }
#chart-rules { display: flex; flex-direction: column; }
#chart-rules .rules-box { flex: 1 1 auto; display: flex; flex-direction: column; justify-content: center; }
#chart-rules .rules-chart { flex: 1 1 auto; }
@media (max-width: 700px) {
  .chart-row { flex-direction: column; gap: 0; align-items: stretch; }
}
.chart-empty { color: var(--muted); font-size: 13.5px; }
.chart-legend {
  display: flex; gap: 18px; flex-wrap: wrap;
  font-size: 12.5px; color: var(--muted); margin: 0 0 10px;
}
.chart-legend .sw {
  display: inline-block; width: 12px; height: 12px; border-radius: 3px;
  margin-right: 6px; vertical-align: -2px;
}
.chart-legend .sw.net { height: 3px; background: var(--text); vertical-align: 2px; }
.chart-legend .sw.ex {
  height: 0; width: 14px; border-top: 2px dashed #e8a33d;
  background: transparent; border-radius: 0; vertical-align: 2px;
}

/* chart gridlines: kept very faint so they read as structure rather than
   competing with the data; the zero/break-even axis is a touch more
   prominent (var(--border)) but still subordinate to the bars and points */
.grid-line { stroke: var(--grid-line); stroke-width: 0.75px; }
.grid-line-zero { stroke: var(--border); stroke-width: 1.5px; }

/* scatter-plot bubbles: translucent fill so overlapping points in dense
   clusters stay distinguishable, with a stroke in the same semantic colour
   at matching transparency, same treatment as the .bar-* classes below */
.bubble { stroke-width: 1.6px; }
.bubble-gain { fill: var(--gain); fill-opacity: 0.32; stroke: var(--gain); stroke-opacity: 0.32; }
.bubble-loss { fill: var(--loss); fill-opacity: 0.32; stroke: var(--loss); stroke-opacity: 0.32; }
.bubble-pinned-up { fill: var(--muted); fill-opacity: 0.28; stroke: var(--muted); stroke-opacity: 0.28; }
.bubble-pinned-down { fill: var(--faint); fill-opacity: 0.24; stroke: var(--faint); stroke-opacity: 0.24; }

/* tax-year and by-code chart bars: same translucent-fill treatment as the
   scatter bubbles above, but the outline matches the fill's transparency
   too (rather than staying solid), since a bar's edge is its own straight
   line rather than needing to stay legible against overlapping neighbours */
.bar { stroke-width: 1.6px; }
.bar-gain { fill: var(--gain); fill-opacity: 0.32; stroke: var(--gain); stroke-opacity: 0.32; }
.bar-loss { fill: var(--loss); fill-opacity: 0.32; stroke: var(--loss); stroke-opacity: 0.32; }

/* matching-rules donut (TODO-099): the charts' own colours at the bars'
   transparency. Segments are parted by a --surface stroke rather than an
   outline of their own colour, since they sit edge to edge. The key's
   swatches (HTML, .rules-key) take the same colours, so they match. */
.rule-seg { stroke: var(--surface); stroke-width: 1.5px; }
.rule-seg-same-day   { fill: var(--loss);   fill-opacity: 0.32; }
.rule-seg-30-day     { fill: var(--accent); fill-opacity: 0.32; }
.rule-seg-s104       { fill: var(--gain);   fill-opacity: 0.32; }
.rule-seg-short-sale { fill: #e8a33d;       fill-opacity: 0.32; }

/* The donut and its key, side by side in the other charts' 400:280 shape,
   so the chart is the same height as Cumulative realised P&L beside it. The
   key is HTML at a fixed size (results.js renderRulesChart), so as the card
   narrows it takes a growing share of the width; once that passes 15% the
   key moves below the donut. The key measures about 77px, so 15% is a box
   of about 513px; change the figure with the key's labels or font size. A
   container
   query, because it is the card's width that matters, not the window's: the
   card is half the window on desktop and the full width once the chart row
   stacks. */
.rules-box { container-type: inline-size; }
.rules-chart {
  display: flex; align-items: center; justify-content: center; gap: 24px;
  aspect-ratio: 400 / 280;
}
.chart-wrap .rules-chart svg.rules-donut { height: 100%; width: auto; min-width: 0; }
.rules-key {
  list-style: none; margin: 0; padding: 0; flex: none;
  display: flex; flex-direction: column; gap: 10px;
  font-size: 12.5px; color: var(--muted); white-space: nowrap;
}
.rules-key li { display: flex; align-items: center; }
.rules-key .sw {
  display: inline-block; width: 12px; height: 12px; border-radius: 3px; margin-right: 6px;
}
.rules-key .sw.rule-seg-same-day   { background: var(--loss);   opacity: 0.32; }
.rules-key .sw.rule-seg-30-day     { background: var(--accent); opacity: 0.32; }
.rules-key .sw.rule-seg-s104       { background: var(--gain);   opacity: 0.32; }
.rules-key .sw.rule-seg-short-sale { background: #e8a33d;       opacity: 0.32; }
@container (max-width: 512px) {
  .rules-chart { flex-direction: column; gap: 12px; aspect-ratio: auto; }
  /* full width, so taller than the chart beside it: shrinking it to keep the
     400:280 height made the ring names and percentages unreadable. The
     cards in the row still match (.chart-row stretches them). */
  .chart-wrap .rules-chart svg.rules-donut { width: 100%; height: auto; }
  .rules-key { flex-direction: row; flex-wrap: wrap; justify-content: center; gap: 8px 16px; }
}

section.prose h2 { margin-top: 0; }
section.prose table.data { font-size: 13px; }
/* formatting.html's column-name cells: the first name is the one the
   calculator documents (and the one every other page uses), the rest are
   accepted spellings nobody needs to remember. Muting them keeps the primary
   name the thing your eye lands on when scanning the column. */
section.prose table.data .alias { color: var(--muted); }

/* ---------- body-text links ----------
   Body copy keeps the theme's own link colours — --accent, --purple once
   visited, both set globally above — and drops only the browser's default
   underline. Keyed to section.prose so it reaches exactly the body copy: the
   nav, the footer, the data card's "Formatting help" and the results' own
   links all keep the treatment they have.

   The underline returns on hover and focus, and that is load-bearing rather
   than decorative. Without it at rest, colour is the only thing marking a
   link, and --accent sits 2.44:1 against the body text around it in the light
   theme (2.52:1 dark) — WCAG 1.4.1 asks for 3:1 before colour may carry that
   on its own. Hover and focus are what keep a link findable, so if the
   underline ever comes off those too, the colour has to do more work than it
   currently can. */
section.prose a { text-decoration: none; }
section.prose a:hover,
section.prose a:focus-visible {
  text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px;
}

/* ---------- scope.html's limitation lists ----------
   Twenty-seven bullets across six groups, most of them several sentences
   long. Stacked at the default one line apart they ran together into a wall,
   and where one item ended and the next began was carried by the marker
   alone.

   Half a line added to the line box the list already has (15px × 1.55 =
   23.25px), so consecutive items sit one and a half lines apart. On `li + li`
   rather than every `li`, so the first bullet does not gain a gap the <h2>
   above it has already provided.

   The markers take the brand amber — the logo's own colour, and the hero
   eyebrow's dot — instead of inheriting --text, which is a navy near enough
   to the sentence beside it that the dots read as punctuation rather than as
   structure. A literal in both themes, like the eyebrow dot: it holds its
   contrast on the light canvas and the dark one alike. ::marker accepts
   colour and font properties only, which is all this needs.

   Not applied to every prose list: most of the other pages' lists are short
   and do not want the extra air. `.list-airy` opts one in — cookies.html's
   three bullets are the same shape as these, a sentence or three each — while
   #scope keeps its id selector so all six of its lists are covered without
   six class attributes. */
#scope li + li,
.list-airy li + li { margin-top: .775em; }
#scope li::marker,
.list-airy li::marker { color: #F0A02C; }

/* ---------- reference-page prose on the page canvas ----------
   formatting.html's sections used to be .card.prose — a white panel per
   section. The band above now does the separating the card did, and the
   calculator's own layout puts content straight on the canvas, so the card
   goes. Body text keeps main.container's own width: the measure it had
   inside the card, near enough, and the width every other block on the page
   already runs to. The rules below only restore the spacing .card's padding
   and margin used to provide. */
/* 34px until the section headings below took the keyline and the step up to
   21px/700. A heading that announces itself that firmly wants more clear air
   above it than one that was near enough body copy did. */
main.container > section.prose + section.prose { margin-top: 44px; }
/* ...but not when everything before it is filtered away. examples.html's strip
   hides the unchosen sections with .hidden (display:none), and a display:none
   element is still an adjacent sibling, so the one visible section kept the
   44px meant to separate it from a section above it that is not there. That
   margin then collapsed with .section-nav's own 26px to 44px — except under
   "Matching rules", which is the FIRST section and so never had a margin at
   all, and sat 18px higher than every other choice.
   Zeroing it here gives every filtered section the strip's own 26px, which is
   what .section-nav's margin-bottom is for. "Show all" hides nothing, so the
   rule does not apply there and the 44px rhythm between sections stands. */
main.container > section.prose.hidden + section.prose { margin-top: 0; }
section.prose > p:last-child { margin-bottom: 0; }
section.prose > .table-frame { margin-top: 20px; }
/* ...except when the frame IS the section — a lone .table-frame right after
   the section opens (no intervening heading or paragraph) would only stack
   its 20px onto the section's own 34px. */
section.prose > .table-frame:first-child { margin-top: 0; }
/* formatting.html's #format runs two tables back to back (mandatory, then
   optional): h2's own top margin is zeroed (see "section headings" below),
   so the second heading would otherwise sit flush against the first table's
   bottom edge with no clear air at all. */
section.prose > .table-frame + h2 { margin-top: 34px; }

/* ---------- section headings on the prose pages ----------
   The base h2 (18px/600, styles.css's own rule near the top) is the same
   family and the same colour as the copy under it, and 18px over 15px body is
   a 1.2x step — too small to register. Its 12px bottom margin then collapses
   with the following paragraph's own 15px top margin, so the gap under a
   heading rendered at exactly the gap between two paragraphs: nothing marked
   where a section started, and the reference pages read as one long run of
   text.

   21px/700 with a 34x3px keyline under it in the brand amber. #F0A02C is a
   literal here for the same reason #scope's list markers are (see §1 of
   style.md): the brand amber is chrome, so it does not restate itself per
   theme, and this needs no dark override. As a 3px bar it is decoration
   rather than text, so the 2.01:1 it scores against --bg — the reason body
   links are not amber — does not govern it.

   DIRECT CHILD, deliberately, and this is the part that will bite if it is
   ever loosened to a descendant selector: examples.html mounts the
   calculator's own results markup INSIDE its .prose sections (.calc-result,
   bound at the foot of that page), so `section.prose h2` would draw a
   keyline under the chart titles, "Which figures go in which boxes" and the
   filing-year headings there, while the identical markup on index.html —
   which has no .prose section — kept none. Every real section heading is a
   direct child of its section; every generated one is nested inside
   .calc-result, .filing-year-head or .chart-card. */
section.prose > h2 {
  font-size: 21px; font-weight: 700; letter-spacing: -.01em; margin: 0 0 18px;
}
section.prose > h2::after {
  content: ""; display: block;
  width: 34px; height: 3px; border-radius: 2px;
  background: #F0A02C;
  margin-top: 10px;
}

/* examples.html's section strip: .result-tabs-scroll's own 16px is the gap
   between the results panel's tabs and the table right under them; here the
   strip is heading a whole page section, so it wants the rhythm a section
   break has. */
.section-nav { margin-bottom: 26px; }
/* The strip had a scroll-margin-top here while the filter scrolled TO it. It
   lands at the top of the page now instead, band and all (see land() in
   examples.html), and nothing scrolls the strip into view — so there is no
   anchor offset left to set. */

/* .table-frame clips (overflow:hidden, so the head bar meets the rounded
   corner), which on a phone would cut the widest cells off with nothing to
   scroll — the reference table is ~390px wide at its narrowest, against a
   ~333px frame, because the <code> chips in it are single unbreakable words
   (EQUALISATION, stamp_duty_currency) that td.wrap's overflow-wrap cannot
   split. So the table gets a scroller of its own inside the frame.

   NOT .scroll-x: that one caps height at 360px to stop a results table
   running away with the page, and a column reference is read straight down.
   Horizontal only here, and the frame keeps its corners. */
.table-frame > .scroll-h { overflow-x: auto; }
/* The column-reference table on formatting.html wraps inline <code> chips. Each
   chip's box is ~23px (12.5px text plus padding and border), taller than the 1.55
   line box the cells inherit from body, so wrapped chips touch. Open the lines up
   to clear them. Scoped to #format so the other prose tables keep the normal rhythm */
#format table.data th, #format table.data td { line-height: 2; }
section.prose code {
  font-family: var(--mono); font-size: 12.5px; background: var(--surface-2);
  border: 1px solid var(--border); border-radius: 5px; padding: 1px 6px;
}
.example-wrap { position: relative; }
.table-frame.example-preview { margin-top: 12px; }
pre.example {
  /* --surface-2. Now that examples.html's sections sit on the page canvas
     rather than inside a white .card, this is only a shade off --bg in light
     mode (#f1f3f6 on #f6f7f9) and the 1px --border is what actually draws the
     block's edge — quiet on purpose, so a wall of sample rows stays
     subordinate to the prose explaining it rather than reading as a stack of
     white panels. In dark mode the same token lands lighter than the page
     (#1f2739 on #0f1420), so there it reads as a raised panel. Both are the
     intended look; if this ever needs more separation in light mode, the fix
     is a stronger border, not a white fill. */
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 14px; font-family: var(--mono); font-size: 12px; overflow-x: auto; margin: 0;
}
/* Spouse transfers: the spouse's BUY rows are white once there are any
   (app.js renderTransfers); the "none" note keeps the grey above */
#spouse-buy-text.has-data { background: var(--surface); }
.copy-btn {
  position: absolute; top: 10px; right: 10px;
  display: inline-flex; align-items: center;
  background: transparent; border: none; color: var(--faint);
  padding: 4px; cursor: pointer; border-radius: 6px;
}
.copy-btn:hover { color: var(--text); }
.copy-btn.copied { color: var(--gain); }
.copy-btn svg { display: block; }
/* unlike the plain .example blocks, the text pane behind this one is live
   code colouring the button would otherwise float over unreadably - so this
   one gets a solid backdrop of its own */
.pane-text .copy-btn { background: var(--surface); border-radius: 6px; }

/* ---------- HMRC boxes tab (#view-filing) ----------
   One .filing-year card per tax year, each holding one .filing-form section
   per Self Assessment form (SA108/SA106/SA100). Inside a form, .boxlist is
   the box-number / label / copyable-figure row, and .filing-sub is the
   visually-demoted panel for figures that are NOT box entries — the
   rate-period breakdown. That demotion is load-bearing, not decorative: it
   is what stops a supporting figure reading as something to type into a box.
   See renderFiling in app.js. */
.filing-intro { margin-bottom: 22px; }
.filing-caveat {
  display: flex; gap: 10px; align-items: flex-start;
  background: var(--accent-soft); border: 1px solid var(--border);
  border-left: 3px solid var(--accent); border-radius: var(--radius);
  padding: 12px 14px; margin: 0; font-size: 13px; line-height: 1.55; color: var(--muted);
}
.filing-caveat svg { color: var(--accent); flex: none; margin-top: 2px; }

/* A tab's own head bar, used by HMRC boxes and Fund income: the same
   .table-head look Disposals/Annual/etc. use for their "Export CSV" button,
   but standalone rather than the top strip of a .table-frame (neither panel
   has a single scrolling table to head — .filing-year is its own bordered
   card, and Fund income has two tables), so all four corners round rather
   than just the top two. It carries the panel's own heading and intro line
   rather than a plain label, so it needs its own padding/alignment for two
   lines of text next to the buttons — the .table-head.has-action selector
   this overrides is the same specificity, so the override is written at that
   same specificity rather than as a bare .panel-head-bar rule, which would
   lose the tie.

   Its 16px padding-top is what every other tab's export button is aligned to;
   see "one height for every tab's export" below. */
.table-head.panel-head-bar {
  align-items: flex-start; flex-wrap: wrap; gap: 16px; padding: 16px 18px;
}
.panel-head-bar { border-radius: var(--radius); margin-bottom: 22px; }
/* without a flex-basis, an unconstrained flex item's *hypothetical* size (the
   width it would take on one line) is what decides whether it fits beside the
   buttons — so the two-line heading, whose one-line width is huge, forced a
   wrap onto its own row even though it visually wraps just fine once given a
   width. flex-basis 0 plus grow lets it take "whatever's left of the row"
   from the start, so the actions never lose the tug-of-war for room. */
.panel-head-text { flex: 1 1 0; min-width: 220px; }
.panel-head-text h2 { margin: 0 0 4px; font-size: 18px; color: var(--text); }
.panel-head-text .sub { margin: 0; font-size: 14.5px; color: var(--muted); }
.panel-head-bar .table-head-actions { flex: 0 0 auto; }

/* Input Data: the Original / Converted switch above the table is the Upload
   file / Paste data toggle (.input-toggle, .pill), with room below it; each
   converted figure in the converted view is marked (app.js renderInput) */
.view-switch { margin: 0 0 12px; }
#view-input .summary-note { margin: 0 0 12px; }
table.data td.fx-converted { color: var(--accent); font-weight: 600; }

/* Investment lifecycle / Annual Movements (TODO-072, TODO-093): the holding
   filter sits beside Export CSV, and the balance rows
   (brought forward, carried forward, closing) read as the block's frame */
.holding-filter select {
  background: var(--surface); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-sm);
  padding: 0 10px; font: inherit; font-size: 13px; max-width: 180px; cursor: pointer;
  /* the height of the 42px expand button beside it */
  height: 42px; box-sizing: border-box;
}
.holding-filter select:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
table.data tr.balance-row td { font-weight: 600; background: var(--surface-2); }
/* Annual Movements, Latest with nothing since the last year end: the table is
   only Brought forward rows, so they are neither shaded nor bold (app.js
   renderMovements) */
table.data tr.balance-row.balance-plain td { font-weight: 400; background: var(--surface); }
/* thirteen columns (fourteen with Tax year): tighter cells, and the long
   headers on two lines (app.js POOL_HEAD_LABEL) sitting on the same baseline
   as the one-line ones, so the table fits the normal page width */
.pool-frame table.data th, .pool-frame table.data td { padding-left: 7px; padding-right: 7px; }
.pool-frame table.data th:first-child, .pool-frame table.data td:first-child { padding-left: 16px; }
.pool-frame table.data th { vertical-align: bottom; }
/* Investment lifecycle / Annual Movements: an income event's (ERI, notional
   distribution) Event label, Income and Income tax year (app.js INCOME_EVENTS) */
table.data tr.income-row td.income-col { color: var(--blue); }
.table-head-actions { display: flex; gap: 8px; flex-shrink: 0; }

/* the card wrapping each tax year — darker (--surface-2) than the page's
   --bg, holding .filing-form panels (--bg, lighter than this wrapper) inside
   it, so a year reads as one grouped unit distinct from the years above and
   below it */
.filing-year {
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 20px 22px; margin-bottom: 26px;
}
.filing-year-head { margin-bottom: 14px; }
.filing-eyebrow {
  display: block; font-size: 11px; font-weight: 650; letter-spacing: .09em;
  text-transform: uppercase; color: var(--muted); margin-bottom: 4px;
}
.filing-year-head h2 { margin: 0; font-size: 21px; }
.filing-year-head .hint { margin: 2px 0 0; font-size: 13px; color: var(--muted); }

.filing-form {
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 16px 18px; margin-bottom: 14px;
}
.filing-form-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 12px; margin-bottom: 10px;
}
.filing-form-head h3 { margin: 0; font-size: 16px; }
.filing-form-head .hint { margin: 2px 0 0; font-size: 12.5px; color: var(--muted); }
.form-chip-group {
  flex: none; display: flex; align-items: center; gap: 8px;
}
.form-chip-title {
  font-size: 11px; font-weight: 650; letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted); white-space: nowrap;
}
.form-chip {
  flex: none; font-family: var(--mono); font-size: 15px; font-weight: 700;
  color: var(--accent); background: var(--accent-soft);
  border: 1px solid var(--border); border-radius: 6px; padding: 4px 12px;
}
/* The chip names the HMRC form the figures go on, so it links to that form.
   The chip itself keeps the box; the anchor inside it only has to stop the
   global a/a:visited pair repainting it (--purple once followed would make a
   read chip louder than an unread one) and hold the underline back until
   hover, since the chip's border and fill already mark it as a thing to hit. */
.form-chip-link,
.form-chip-link:visited { color: inherit; text-decoration: none; }
.form-chip-link:hover,
.form-chip-link:focus-visible { text-decoration: underline; text-underline-offset: 3px; }

.filing-note { font-size: 12.5px; line-height: 1.55; color: var(--muted); margin: 0 0 12px; }
.filing-note.accent { color: var(--text); background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 10px 12px; }
.filing-note.strong { color: var(--text); }
/* a figure the user reported that this tool could not verify — flagged rather
   than blended into the surrounding guidance, since it changes which boxes
   they should actually be filling in */
.filing-note.asserted { color: var(--text); background: var(--surface);
  border: 1px solid var(--orange); border-left-width: 3px;
  border-radius: var(--radius); padding: 10px 12px; }
.filing-note-deminimis { margin-top: 18px; }
.filing-note em { font-family: var(--mono); font-style: normal; }
/* relief routes the calculator does not compute, named so their absence from
   the boxes above cannot be read as "these do not apply to me". Accent-edged
   rather than orange: this is guidance to act on, not a warning about a
   figure shown, which is what .asserted means */
.filing-note.advisory { color: var(--text); background: var(--surface);
  border: 1px solid var(--border); border-left: 3px solid var(--accent);
  border-radius: var(--radius); padding: 10px 12px; }
.filing-note.advisory ul { margin: 8px 0 8px; padding-left: 18px; }
.filing-note.advisory li { margin-bottom: 5px; }
.filing-note.advisory li:last-child { margin-bottom: 0; }

/* a further group of real box entries on the same form. Deliberately NOT
   .filing-sub: that panel's dashed frame and "supporting figures" framing
   mean "do not copy these into boxes", which is the opposite of true here */
.filing-group { margin-top: 20px; }
.filing-group > h4 { margin: 0 0 8px; font-size: 14px; }

.boxlist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.boxlist.totals { margin-top: 12px; }
.boxlist li {
  display: grid; grid-template-columns: 108px 1fr minmax(150px, 210px);
  align-items: center; gap: 12px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 8px 12px;
}
.boxlist.totals li { border-color: var(--accent); }
.box-num {
  font-family: var(--mono); font-size: 12px; font-weight: 600; color: var(--accent);
  white-space: nowrap;
}
/* the unlisted-box alternative, deliberately quieter than the primary box
   number and on its own line so "Box 23" and "unlisted 31" can never be
   misread as a single "23/31" reference */
.box-num em { display: block; font-style: normal; font-weight: 400; font-size: 11px; color: var(--faint); }
.box-num.none { color: var(--faint); }
.box-label { font-size: 13.5px; color: var(--text); }
.box-field { display: flex; align-items: center; gap: 4px; }
.box-field input {
  flex: 1 1 auto; min-width: 0; text-align: right;
  font-family: var(--mono); font-size: 13.5px; font-weight: 600;
  color: var(--text); background: var(--surface-2);
  border: 1px solid var(--border); border-radius: 7px; padding: 7px 10px;
}
.box-field input:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
/* the shared .copy-btn is absolutely positioned for the <pre> blocks it was
   written for; here it sits inline next to the field */
.box-field .copy-btn { position: static; flex: none; }

.filing-sub {
  margin-top: 16px; padding: 14px; border-radius: var(--radius);
  background: var(--surface); border: 1px dashed var(--border);
}
.filing-sub-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 6px; }
.filing-sub-head h4 { margin: 0; font-size: 14px; }
.tag-warn {
  font-size: 10.5px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  color: var(--orange); background: var(--surface-2);
  border: 1px solid var(--orange); border-radius: 5px; padding: 2px 7px;
}
.filing-sub-note { font-size: 12.5px; line-height: 1.55; color: var(--muted); margin: 0 0 12px; }
.filing-sub-note a { color: var(--accent); }

/* these tables sit directly in a .filing-form/.filing-sub panel rather than
   in the split-header .table-frame the result tabs use — they are short
   enough not to need a frozen header, so they keep a plain thead */
.filing-form > .scroll-x, .filing-sub > .scroll-x {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); max-height: none;
}
table.data.filing-periods, table.data.filing-funds { width: 100%; }
table.data.filing-funds select.eri-class {
  font-family: inherit; font-size: 12.5px; color: var(--text);
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 6px; padding: 4px 6px;
}
table.data.filing-funds select.eri-class:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
table.data .muted-cell { color: var(--muted); font-family: var(--mono); font-size: 12px; }
/* matches .box-num's default accent colour, so a row's "Goes to" box number
   reads as the same box as the dividend total below it */
table.data .dividend-box { color: var(--accent); font-weight: 600; }
table.data .interest-box { color: var(--fuchsia); font-weight: 600; }
.filing-note.filing-note-interest { color: var(--fuchsia); }

/* Disposals "rule" column (config.ini [features] HIGHLIGHT_MATCHING_RULE;
   app.js matchRuleCell emits the markup, and emits plain text instead when
   the toggle is off, so nothing here applies). No pill/badge chrome — the
   rule reads at a glance from its font colour alone: blue for the s104
   pool, purple for same day, amber for the 30 day rule, teal for a short
   sale (HMRC's fourth rule, later purchases; TODO-065). */
table.data .match-badge { font-weight: 600; }
table.data .match-badge.rule-s104     { color: var(--blue); }
table.data .match-badge.rule-30-day   { color: var(--orange); }
table.data .match-badge.rule-same-day { color: var(--fuchsia); }
table.data .match-badge.rule-short-sale { color: var(--teal); }

@media (max-width: 640px) {
  .boxlist li { grid-template-columns: 1fr; gap: 6px; }
  .box-field { justify-content: flex-end; }
}

/* FAQ accordion — native <details>/<summary>, no JS needed */
.faq-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 6px; flex-wrap: wrap; }
.faq-toolbar h1 { margin: 0; }
/* ---------- the band's category dropdown ----------
   Six category names are more than a row can hold — that is what ruled out a
   segmented strip, which cannot wrap and can only scroll — and as a plain
   wrapped link row they took two lines of the page before any content. Folded
   into a dropdown in the band instead, next to the calculator link.

   Written for faq.html, now shared with examples.html, whose band carries the
   same control over its eight example groups. Nothing below is FAQ-specific,
   so keep it that way — the two pages are meant to feel like one band with one
   dropdown in it, not two that nearly match.

   The trigger repeats .hero-link exactly (see the note there on :visited), so
   the band carries one pill shape rather than two that nearly match.

   The panel is absolutely positioned, unlike .rules-pop's on index.html,
   which is deliberately in flow because it sits directly above the drop zone
   and pushing the page down is better there than covering it. Here there is
   nothing below but questions, and covering them briefly is what a dropdown
   is for — so it overlays, which also keeps the pill row from changing height
   when it opens.

   Its fill is opaque and lighter than the band in both themes (the band is
   #1E293B in light and --bg in dark), because a translucent panel over page
   content would show the questions through it. */
.hero-cats { position: relative; }
.hero-cats > summary {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 14px;
  font-size: 13px; font-weight: 500; color: #e2e8f0;
  background: rgba(255, 255, 255, .06);
  border: 1px solid rgba(255, 255, 255, .16); border-radius: 999px;
  cursor: pointer; list-style: none;
  transition: color .15s, border-color .15s, background .15s;
}
.hero-cats > summary::-webkit-details-marker { display: none; }
.hero-cats > summary:hover,
.hero-cats[open] > summary {
  color: #fff; border-color: rgba(255, 255, 255, .28); background: rgba(255, 255, 255, .11);
}
.hero-cats > summary .chev { transition: transform .15s; }
.hero-cats[open] > summary .chev { transform: rotate(180deg); }
.hero-cats-panel {
  position: absolute; top: calc(100% + 8px); left: 0; z-index: 5;
  min-width: 250px;
  display: flex; flex-direction: column;
  padding: 8px;
  background: #24314a;
  border: 1px solid rgba(255, 255, 255, .14); border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
}
.hero-cats-panel a,
.hero-cats-panel a:visited {
  color: #F0A02C; font-size: 13.5px; font-weight: 500;
  padding: 7px 10px; border-radius: 6px;
  text-decoration: none; white-space: nowrap;
}
.hero-cats-panel a:hover,
.hero-cats-panel a:focus-visible { background: rgba(255, 255, 255, .09); text-decoration: underline; }
/* nothing shares the toolbar row now, so the selector simply sits right */
.faq-toolbar .input-toggle { margin-left: auto; }
/* the dropdown jumps to these, so they need to clear the fixed header */
.faq-section { scroll-margin-top: 76px; }
.faq-section { margin-top: 30px; }
.faq-section:first-child { margin-top: 20px; }
.faq-section h2 { margin-bottom: 14px; }
.faq-list { display: flex; flex-direction: column; gap: 10px; }
.faq-item {
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius);
  overflow: hidden;
}
.faq-item summary {
  list-style: none; cursor: pointer; padding: 14px 16px;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  font-weight: 600; font-size: 14.5px;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after {
  content: "+"; color: var(--muted); font-size: 18px; font-weight: 400; flex-shrink: 0;
}
.faq-item[open] summary::after { content: "\2212"; }
.faq-item[open] summary { border-bottom: 1px solid var(--border); }
.faq-item .faq-answer { padding: 14px 16px; color: var(--muted); font-size: 13.5px; line-height: 1.6; }
/* The answer takes the card surface while the question keeps the tray's grey,
   so an opened answer separates from the question heading it belongs to
   rather than the two reading as one block of fill.
   NOTE the theme relationship reverses, as it does everywhere --surface and
   --surface-2 are paired: in light the answer is lighter than the question
   (#ffffff on #f1f3f6), in dark it is darker (#171e2e on #1f2739). Both read
   as a distinct panel, which is the job here, so this is left as one rule
   rather than split — but see §4 of style.md before assuming the direction. */
.faq-item[open] .faq-answer { background: var(--surface); }
.faq-item .faq-answer p { margin: 0 0 10px; }
.faq-item .faq-answer p:last-child { margin-bottom: 0; }

/* ---------- feedback form ----------
   Used in two places from one set of rules: inside the <dialog> that
   static/feedback.js appends on demand, and inline on /feedback.html. The
   markup is identical in both, so everything here is scoped to .fb-form
   rather than to either host.

   This is the site's first real form, so the input/label/fieldset rules below
   are new rather than reused. They follow the two focus idioms already in the
   file: a 3px soft ring for anything button-like (see button.primary) and
   border-colour + ring for text entry (see textarea#paste-input). */

/* Off-screen but still focusable and still read by screen readers. Needed for
   the honeypot: display:none and type="hidden" are both skipped by the bots
   worth catching, whereas this looks like an ordinary field to them and is
   invisible to everyone else. tabindex="-1" on the input keeps it out of the
   keyboard order. */
.visually-hidden {
  position: absolute !important; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}

.fb-form { display: flex; flex-direction: column; gap: 18px; }
.fb-form .fb-field { display: flex; flex-direction: column; gap: 6px; }
.fb-form label { font-size: 13px; font-weight: 500; color: var(--text); }
.fb-form .fb-optional { font-weight: 400; color: var(--muted); }
.fb-form .fb-hint { margin: 0; font-size: 12px; color: var(--muted); }

/* min-width:0 is load-bearing, not tidying. Browsers give <fieldset> a UA
   default of `min-inline-size: min-content`, which no other element has: the
   fieldset then refuses to shrink below the intrinsic width of the radio grid
   (measured at 460px), so on a 375px phone it overflowed its container instead
   of letting the grid collapse to one column. Removing it lets the fieldset
   shrink and the grid reflow, which is the whole basis of the layout below. */
.fb-types { border: 0; margin: 0; padding: 0; min-width: 0; }
.fb-types legend {
  padding: 0; margin-bottom: 10px;
  font-size: 13px; font-weight: 500; color: var(--text);
}
/* Two columns where there is room, one where there is not — and the switch is
   the grid's own doing, not a breakpoint. `auto-fit` + `minmax` collapses to a
   single column as soon as two tracks cannot both hold 190px, which is exactly
   the condition that matters; a hand-picked max-width would have to be
   re-guessed every time the dialog width or the label wording changes.
   At the dialog's 560px (about 512px of content) that yields two columns; on a
   375px phone, one.

   The 220px floor is measured, not guessed: at 190px the longest label ("My
   file would not upload or parse") wraps to a second line, which produced two
   cramped ragged columns in a narrow band around 480-500px viewports. 220px is
   the width at which that label fits on one line, so the layout now goes to two
   columns only when two columns are genuinely comfortable, and stays single
   until then. */
.fb-radios {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 2px 20px;
  /* Two columns is a maximum, not just what the dialog happens to fit. The
     same markup renders on /feedback.html, whose container is ~880px wide —
     there `auto-fit` cheerfully produced three columns. Capping the grid's own
     width keeps a third from ever fitting (3 x 220 + gaps > 520) without
     reintroducing a breakpoint, and it keeps the option list a readable
     measure on the wide page rather than stretching it across the card. */
  max-width: 520px;
}
/* align-items rather than center: the longest label ("My file would not upload
   or parse") wraps to two lines in a narrow column, and a control centred
   against a two-line label sits visibly below its own text. The 1px nudge
   optically centres the radio on the first line's cap height. */
.fb-radio { display: flex; align-items: flex-start; gap: 9px; padding: 4px 0; }
.fb-radio input[type="radio"] {
  accent-color: var(--accent); width: 16px; height: 16px; margin: 1px 0 0;
}
.fb-radio input[type="radio"]:focus { outline: 2px solid var(--accent); outline-offset: 2px; }
.fb-radio label { font-weight: 400; color: var(--muted); cursor: pointer; }
.fb-radio input[type="radio"]:checked + label { color: var(--text); }

.fb-form textarea,
.fb-form input[type="email"],
.fb-form input[type="text"] {
  width: 100%; background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  font: inherit; font-size: 13.5px; padding: 10px 12px;
  transition: border-color .15s, box-shadow .15s;
}
.fb-form textarea { min-height: 130px; resize: vertical; line-height: 1.55; }
.fb-form textarea::placeholder { color: var(--muted); opacity: .6; }
.fb-form textarea:focus,
.fb-form input[type="email"]:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.fb-count { margin: 0; align-self: flex-end; font-size: 12px; color: var(--faint); }
.fb-count.near { color: var(--orange); }

.fb-check { display: flex; align-items: center; gap: 9px; font-weight: 400; color: var(--muted); }
.fb-check input[type="checkbox"] { accent-color: var(--accent); width: 15px; height: 15px; margin: 0; }

/* Empty until the widget decides it needs to show something (the form asks
   Turnstile for appearance:"interaction-only"), so it must not reserve space. */
.fb-turnstile:empty { display: none; }

/* Shown only in the gap between the form being asked for and /api/config
   answering. Muted and unemphatic on purpose: nothing has gone wrong yet, so
   it must not read like a warning. */
.fb-loading { margin: 0; padding: 6px 0; font-size: 13.5px; color: var(--muted); }

.fb-status:empty { display: none; }
.fb-status .banner { margin: 0; }

.fb-actions { display: flex; align-items: center; justify-content: flex-end; gap: 10px; }
.fb-privacy { margin: 0; font-size: 12px; line-height: 1.6; color: var(--faint); }
.fb-privacy a { color: var(--muted); }
.fb-done { display: flex; flex-direction: column; gap: 14px; align-items: flex-start; }
.fb-done p { margin: 0; font-size: 13.5px; color: var(--muted); }

/* the dialog itself — static/feedback.js creates this on first open */
.fb-dialog {
  width: min(560px, calc(100vw - 32px)); max-height: min(88vh, 900px);
  padding: 0; border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); color: var(--text); box-shadow: var(--shadow-lg);
  overflow: auto;
}
/* tabindex="-1" (feedback.js ensureDialog) makes showModal() focus the
   dialog itself rather than falling through to the × close button; that
   focus is programmatic, not a visitor tabbing in, so it gets no ring. */
.fb-dialog:focus { outline: none; }
.fb-dialog::backdrop { background: rgba(0, 0, 0, .45); }
.fb-dialog-inner { padding: 22px 24px 24px; }
.fb-dialog-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 16px; margin-bottom: 18px;
}
.fb-dialog-head h2 { margin: 0; font-size: 17px; }
.fb-dialog-x {
  background: none; border: 0; cursor: pointer; padding: 0 4px;
  font-size: 22px; line-height: 1; color: var(--muted);
}
.fb-dialog-x:hover { color: var(--text); }
.fb-dialog-x:focus { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

/* The paywall funnel (static/funnel.js), opened when Calculate is refused and
   passes are on (config.ini PASSES_ENABLED). The .fb-dialog frame, a little
   wider for two pass cards side by side. Every colour is an existing token, so
   both themes follow without a new one. */
.pf-dialog { width: min(620px, calc(100vw - 32px)); }
.pf-dialog .banner { margin-top: 0; }
.pf-msg { font-size: 14px; line-height: 1.55; margin: 14px 0 0; }
.pf-summary {
  background: var(--surface-2); border-radius: var(--radius);
  padding: 10px 14px; margin-top: 14px; font-size: 13.5px; line-height: 1.6;
}
.pf-label {
  margin: 0 0 2px; font-size: 11px; font-weight: 650; letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted);
}
.pf-summary ul { margin: 0; padding-left: 18px; }
.pf-total { margin: 6px 0 0; padding-top: 6px; border-top: 1px solid var(--border); }
.pf-free { color: var(--muted); }
.pf-cards {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 12px; margin-top: 16px;
}
.pf-card {
  display: flex; flex-direction: column; gap: 4px;
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: 14px; background: var(--surface);
}
.pf-card.pf-rec { border: 2px solid var(--btn-primary); padding: 13px; }
.pf-card.pf-off { background: var(--surface-2); color: var(--muted); }
.pf-card-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
.pf-name { font-weight: 600; font-size: 15px; }
.pf-tag {
  font-size: 11.5px; font-weight: 600; padding: 1px 8px; border-radius: var(--radius-sm);
  background: var(--accent-soft); color: var(--accent); white-space: nowrap;
}
.pf-tag.pf-tag-off { background: none; color: var(--muted); padding: 0; }
.pf-price { font-size: 22px; font-weight: 600; }
.pf-price span { font-size: 12.5px; font-weight: 400; color: var(--muted); }
.pf-caps { margin: 2px 0 0; padding: 0; list-style: none; font-size: 13px; color: var(--muted); }
.pf-why { margin: 4px 0 0; font-size: 12.5px; }
.pf-card button { margin-top: auto; width: 100%; }
.pf-card .pf-buy { margin-top: 10px; }
.pf-custom {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  flex-wrap: wrap; margin-top: 12px; font-size: 13.5px;
}
.pf-custom p { margin: 0; }
.pf-error { margin: 12px 0 0; font-size: 13.5px; color: var(--loss); }
.pf-foot { margin: 14px 0 0; font-size: 12.5px; line-height: 1.55; color: var(--muted); }
.pf-test {
  margin: 12px 0 0; font-size: 12.5px; padding: 8px 12px; border-radius: var(--radius);
  background: var(--orange-soft); color: var(--orange);
}
.pf-actions { display: flex; justify-content: flex-end; margin-top: 18px; }

/* /pricing.html (app/plans_view.py). Plan cards are the one place cards hold
   content (style.md §9 exception, owner 2026-10-08): the plans are bounded
   objects compared side by side. Numbers sit in each card's list, never in
   prose. Existing tokens only. */
.pricing { margin-top: 28px; }
/* three across or one: two-and-one would strand the annual pass on a row of
   its own and read as lesser */
.plan-grid {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 16px; align-items: stretch;
}
@media (max-width: 820px) {
  .plan-grid { grid-template-columns: 1fr; gap: 22px; }
}
.plan-card {
  position: relative; display: flex; flex-direction: column;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 20px; box-shadow: var(--shadow-sm);
}
.plan-card.plan-featured { border: 2px solid var(--btn-primary); padding: 19px; }
.plan-badge {
  position: absolute; top: -12px; left: 16px; margin: 0;
  font-size: 12px; font-weight: 600; padding: 2px 10px; border-radius: var(--radius-sm);
  background: var(--accent-soft); color: var(--accent);
}
.plan-card h3 { margin: 0; font-size: 17px; }
.plan-for { margin: 4px 0 0; font-size: 13.5px; color: var(--muted); min-height: 2.9em; }
.plan-price { margin: 10px 0 0; font-size: 30px; font-weight: 600; line-height: 1.1; }
.plan-price span { font-size: 13px; font-weight: 400; color: var(--muted); }
.plan-list { list-style: none; margin: 14px 0 0; padding: 0; font-size: 13.5px; line-height: 1.5; }
.plan-list li { display: flex; gap: 8px; align-items: flex-start; margin: 5px 0; }
.plan-list svg { flex: none; margin-top: 3px; color: var(--gain); }
.plan-list .plan-cap { font-weight: 600; }
.plan-inc { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border); }
.plan-list .plan-not { color: var(--muted); }
.plan-list .plan-not svg { color: var(--muted); }
.plan-cta { display: block; margin-top: auto; text-align: center; width: 100%; }
.plan-card .plan-cta { margin-top: 18px; }
.plan-card .plan-inc { margin-bottom: auto; }
.plan-custom {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  flex-wrap: wrap; margin-top: 16px; padding: 16px 20px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
}
.plan-custom p { margin: 0; font-size: 14px; }
.plan-custom p span { color: var(--muted); margin-left: 6px; }
.plan-custom .plan-cta { width: auto; margin: 0; }
.pricing-note { margin: 12px 0 0; font-size: 13px; color: var(--muted); }
.pricing-error { margin: 12px 0 0; font-size: 13.5px; color: var(--loss); }
/* the questions wear faq.html's .faq-item; only the spacing is the page's own */
.pricing-faq { margin-top: 40px; }
.pricing-faq .faq-toolbar h2 { margin: 0; }

/* the active pass, beside Calculate */
.pass-status { align-self: center; font-size: 13px; color: var(--muted); }
.pass-status-sub { white-space: nowrap; }
.pass-forget {
  background: none; border: 0; padding: 0 0 0 4px; cursor: pointer;
  font: inherit; color: var(--btn-outline); text-decoration: underline;
}

/* Expanded results table (app.js openTableModal): the table's own frame is
   moved into this dialog, which fills the window less a 16px margin, and its
   scroll box drops the 360px cap to fill the dialog instead. */
/* the same height as the Export CSV button beside it */
.expand-btn { display: inline-flex; align-items: center; justify-content: center; height: 42px; padding: 0 10px; }
/* square, the icon filling most of it and centred both ways; the class pair
   outranks the padding and display:block the label bars give every .ghost
   button, which otherwise sit the icon on the text baseline, above centre */
button.ghost.expand-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 42px; padding: 0; border-radius: 8px;
}
button.ghost.expand-btn svg { display: block; }
/* the border and icon colour the Export / Copy / Print buttons have */
button.ghost.expand-btn, button.ghost.expand-btn:hover,
button.ghost.pinch-btn, button.ghost.pinch-btn:hover {
  border-color: var(--btn-tool-border); color: var(--btn-tool-text);
}
/* the expanded view's hide/show header button (app.js setTableModalPinched):
   the same square as the expand button it stands in for */
button.ghost.pinch-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 42px; height: 42px; padding: 0; border-radius: 8px;
}
button.ghost.pinch-btn.hidden { display: none; }
button.ghost.pinch-btn svg { display: block; }
/* full window (arrows-out button): the header bar goes, and so does the dialog around the table
   (its 16px margin to the window, border, rounded corners and inner
   padding), so the table's frame fills the whole window edge to edge */
.table-modal.head-hidden {
  width: 100vw; height: 100vh; margin: 0; inset: 0;
  border: 0; border-radius: 0; box-shadow: none;
}
.table-modal.head-hidden .table-modal-head { display: none; }
.table-modal.head-hidden .table-modal-body { padding: 0; }
.table-modal.head-hidden .table-frame.in-modal { border: 0; border-radius: 0; box-shadow: none; }
.table-frame { position: relative; }
/* a frame with no label bar (Fund income per tax year): over the header
   row's right end, where the spare filler column leaves it clear */
.table-frame > .expand-btn.floating { position: absolute; top: 6px; right: 16px; z-index: 1; }
/* its header row is made tall enough for the 42px button, 6px above and below */
.table-frame:not(.in-modal):has(> .expand-btn.floating) thead th { height: 54px; vertical-align: middle; }
.table-frame.in-modal .expand-btn { display: none; }
/* every row is already in the table and the expanded view scrolls through
   all of them, so Show all / Show fewer would change nothing there */
.table-frame.in-modal > .table-more { display: none; }
/* The results' take-away buttons (owner, 2026-10-21): white, navy label,
   8px corners, 42px tall, a 1px border and medium weight. Export (CSV, TXT,
   the zip) was first given a heavier 2px border as the main action; the
   owner then asked for it to match Print, so both classes now share one
   style. Hover tints them pale blue. app.js adds the classes to the buttons
   it builds. Three classes, so this outranks the label bars' white fill and
   shadow (.table-head .ghost) and button.ghost:hover. */
button.ghost.btn-export, button.ghost.btn-print {
  background: var(--btn-tool-bg); color: var(--btn-tool-text);
  border: 1px solid var(--btn-tool-border); font-weight: 500;
  border-radius: 8px; min-height: 42px; padding: 0 16px; box-shadow: none;
  font-size: 13.5px;
}
button.ghost.btn-export:hover, button.ghost.btn-print:hover {
  background: var(--btn-tool-bg-hover); border-color: var(--btn-tool-border-hover);
  color: var(--btn-tool-text); box-shadow: none;
}
button.ghost.btn-export:focus-visible, button.ghost.btn-print:focus-visible {
  outline: none; box-shadow: 0 0 0 3px var(--btn-focus-glow);
}

/* Print buttons (expanded tables, Calculation text): icon and word on one
   line. The class pair outranks the display:block every .ghost button gets. */
button.ghost.print-btn { display: inline-flex; align-items: center; gap: 6px; height: 42px; white-space: nowrap; }
.print-btn svg { display: block; }
/* Print, Export and Fit to width at phone width: the icon alone, in a square
   button the size of the expand button; the word stays for screen readers
   and as the tooltip (app.js wraps it in .btn-label) */
@media (max-width: 640px) {
  button.ghost.print-btn { width: 42px; padding: 0; gap: 0; justify-content: center; }
  .print-btn .btn-label {
    position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden;
    clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
  }
}
/* Fit to width (app.js fitModalTable) when on: the accent tint the selected
   multi-file row uses, so it reads as a switch that is set */
button.ghost.fit-btn[aria-pressed="true"] {
  border-color: var(--accent); color: var(--accent); background: var(--accent-soft);
}

/* ---------- printing an expanded table (app.js printModalTable) ----------
   A Print button puts a copy of one table, or of the Calculation text, in
   #print-host and sets html.printing-table; for that print only, everything
   else on the page is hidden. The copy keeps its own styling (alignment,
   gain and loss colours) on light-theme colours whatever the screen theme.
   Tables print landscape (.landscape), since most are wider than they are
   long; the calculation text prints upright. */
#print-host { display: none; }
@page wide-table { size: landscape; margin: 12mm; }
@media print {
  html.printing-table body > :not(#print-host) { display: none !important; }
  html.printing-table body { padding: 0 !important; background: #fff !important; }
  html.printing-table #print-host {
    display: block;
    --text: #1a2233; --muted: #5c6675; --faint: #94a3b8; --border: #cfd5dd;
    --surface: #fff; --surface-2: #f1f3f6;
    --gain: #15803d; --loss: #b91c1c; --accent: #4f46e5;
    color: var(--text); background: #fff;
    print-color-adjust: exact; -webkit-print-color-adjust: exact;
  }
  html.printing-table #print-host.landscape { page: wide-table; }
  #print-host h1 { margin: 0 0 1.5mm; font-size: 15pt; }
  /* Calculation text: wrapped rather than cut off at the page edge */
  #print-host pre {
    margin: 0; padding: 0; border: 0; background: none; max-height: none; overflow: visible;
    white-space: pre-wrap; font-size: 8.5pt; line-height: 1.4;
  }
  #print-host p { margin: 0 0 4mm; font-size: 9pt; color: var(--muted); }
  #print-host table { width: 100%; border-collapse: collapse; font-size: 8.5pt; margin-bottom: 6mm; }
  /* a section printing two tables (Fund income) heads each with its own */
  #print-host h2 { margin: 2mm 0 2mm; font-size: 11pt; break-after: avoid; }
  #print-host thead { display: table-header-group; }
  #print-host tr { break-inside: avoid; }
  #print-host th, #print-host td {
    padding: 1.3mm 2mm; border-bottom: 0.5pt solid var(--border); white-space: nowrap;
  }
  #print-host th { background: var(--surface-2); color: var(--muted); font-size: 7.5pt; }
}

.table-modal {
  width: calc(100vw - 32px); max-width: none;
  height: calc(100vh - 32px); max-height: none;
  overflow: hidden;
}
.table-modal[open] { display: flex; flex-direction: column; }
.table-modal-head {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 14px 18px; border-bottom: 1px solid var(--border);
}
.table-modal-head h2 { margin: 0; font-size: 16px; }
.table-modal-actions { display: flex; align-items: center; gap: 14px; }
.table-modal-body { flex: 1 1 auto; min-height: 0; padding: 16px; display: flex; }
.table-frame.in-modal { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
/* only the rows' scroll box gives up height; the label bar, the header row
   (overflow hidden, so it would otherwise shrink to nothing) and Show all keep theirs */
.table-frame.in-modal > * { flex-shrink: 0; }
.table-frame.in-modal > .scroll-x,
.table-frame.in-modal > .scroll-x.expanded { flex: 1 1 auto; min-height: 0; max-height: none; }

footer.site {
  border-top: 1px solid var(--border); padding: 20px 0 40px;
  color: var(--faint); font-size: 12.5px;
}
.footer-bottom { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 12px 24px; }
.footer-text { flex: 1 1 320px; min-width: 0; }
.footer-tagline { margin: 0 0 16px; line-height: 1.7; }
/* Every keyword in the tagline is a link into the guide (app/layout.py's
   FOOTER_TAGLINE_ITEMS). At rest they inherit footer.site's own --faint, so
   the line reads exactly as the plain sentence it replaced — and :visited is
   pinned to the same inherit, or the browser's default purple would mark the
   sections a reader had already opened and the line would end up two colours.
   Hover and keyboard focus take the brand amber, the literal #F0A02C used for
   .brand-engine and .hero p a rather than a token, since it is a brand colour
   and not a themed one.
   The underline is the non-colour half of that cue, and it is doing real work
   here: amber is 2.01:1 against the light page (#f6f7f9), well under WCAG
   1.4.1's 3:1 for colour used alone, so hue on its own would not say a link
   had been reached. Same reasoning as .hero p a, one background lighter. */
.footer-tagline a,
.footer-tagline a:visited { color: inherit; text-decoration: none; }
.footer-tagline a:hover,
.footer-tagline a:focus-visible {
  color: #F0A02C;
  text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px;
}
/* Dark: #F0A02C is legible on the near-black page (8.65:1), but it is the
   light-mode value on a background it was not picked against, and the site
   already has an amber for exactly this — the lighter step .hero p a:hover
   uses on the navy band. Same hue, one shade up, so hover reads as the same
   gesture in both themes. */
@media (prefers-color-scheme: dark) {
  html:not([data-theme]) .footer-tagline a:hover,
  html:not([data-theme]) .footer-tagline a:focus-visible { color: #ffc46b; }
}
html[data-theme="dark"] .footer-tagline a:hover,
html[data-theme="dark"] .footer-tagline a:focus-visible { color: #ffc46b; }
.footer-text .disclaimer { margin: 0; max-width: 62ch; color: var(--muted); }
.footer-links { margin-left: auto; flex-shrink: 0; display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
.footer-links a { color: var(--muted); text-decoration: none; }
.footer-links a:hover { color: var(--text); }

.hidden { display: none !important; }
.spinner {
  display: inline-block; width: 14px; height: 14px; border-radius: 50%;
  border: 2px solid rgba(255,255,255,.4); border-top-color: #fff;
  animation: spin .7s linear infinite; vertical-align: -2px; margin-right: 8px;
}
@keyframes spin { to { transform: rotate(360deg); } }

table.data .val-zero {
  color: var(--muted);
  font-weight: 400;
}


/* ---------- HMRC citation links (.cite) ----------
   The bracketed rule references - (TCGA92/S105), (CG14200), (helpsheet HS227) -
   link to the HMRC page that states the rule, so a reader can check any claim
   the calculator makes rather than going looking for the code themselves.

   A citation does NOT follow the no-underline-at-rest rule that
   `section.prose a` sets. That rule leans on hover and focus to keep a link
   findable, which is fine for a link that is the point of its sentence ("see
   the worked examples"). A citation is not: it sits inside a bracket
   mid-sentence, it is read past rather than aimed at, and nobody hovers a
   bracket to find out whether it is live. Colour alone would have to carry it,
   and --accent is 2.44:1 against the body text in light (2.52:1 dark), short of
   the 3:1 WCAG 1.4.1 asks before colour may do that on its own.

   So a dotted underline at rest, which is the convention a reference marker
   already carries in print, going solid on hover and focus. The citation keeps
   the body text's colour rather than taking --accent: a paragraph carrying four
   references turns into a colour field otherwise, and the dotted rule marks it
   regardless.

   :visited is restated because the global a:visited would otherwise repaint a
   followed citation in --purple, making a read citation louder than an unread
   one beside it.

   **One unscoped selector, last in the file, and `:link`/`:visited` carried on
   every rule.** Both halves are load-bearing.

   Unscoped, because citations appear in three places that each style links
   their own way - `section.prose` (the prose pages), `.faq-answer` (the FAQ,
   rendered from markdown) and the `.filing-note` / `.filing-sub-note` family
   (the HMRC boxes panel, built in results.js). Naming those ancestors instead
   was tried and silently missed `.filing-note.advisory`, which has no link rule
   of its own, so the helpsheet citations there fell through to the browser's
   default underline. One plain selector covers every container, including any
   added later.

   `:link`/`:visited` because a bare `a.cite` is (0,1,1) and loses to
   `section.prose a` at (0,1,2) - the `section` counts. `a.cite:link` is (0,2,1)
   and wins; the hover rule needs both pseudo-classes to clear
   `section.prose a:hover` at (0,2,2). Position in the file is not what settles
   it, so moving this block cannot quietly break it. */
a.cite:link,
a.cite:visited {
  color: inherit;
  text-decoration: underline dotted;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--muted);
}
a.cite:link:hover,
a.cite:visited:hover,
a.cite:link:focus-visible,
a.cite:visited:focus-visible {
  color: var(--accent);
  text-decoration: underline solid;
  text-decoration-color: currentColor;
}

/* --- Cookie consent (docs/todo.md TODO-079) ---------------------------------
   The banner (app/layout.py _consent_banner) and the same two buttons on the
   Cookie Settings page (content/pages/cookies.html), both from layout.
   consent_controls(). Same look as the parked four-switch banner
   (docs/parked/cookie-banner-4-toggle/) without the switches: text above,
   and a row of wide, equal buttons across the full width. The buttons are
   deliberately identical: the ICO expects "Reject" to be as easy and as
   prominent as "Accept", so neither is styled as the primary action.
   Existing tokens only, so both themes follow without new entries in the
   four token blocks. */
.cookie-banner {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
  max-height: 85vh; overflow-y: auto;
  background: var(--surface-2); color: var(--text);
  border-top: 1px solid var(--border); box-shadow: var(--shadow-lg);
}
.cookie-banner[hidden] { display: none; }
.cc-inner { max-width: 1120px; margin: 0 auto; padding: 16px 16px 18px; }
.cc-title { margin: 0 0 4px; font-size: 15px; font-weight: 700; }
.cc-text { margin: 0 0 14px; font-size: 13.5px; line-height: 1.55; color: var(--muted); }
.cc-text a { color: var(--accent); }

.cc-actions { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
.cc-btn {
  font: inherit; font-weight: 600; font-size: 14px;
  padding: 10px 14px; border-radius: var(--radius-sm); cursor: pointer;
  background: var(--btn-primary); color: var(--btn-text); border: none;
  transition: background 0.2s ease;
}
.cc-btn:hover { background: var(--btn-primary-hover); }
.cc-btn:active { background: var(--btn-primary-active); }
.cc-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--btn-focus-glow); }

/* "Change cookie settings" on /cookies.html: sized to its label, not the
   banner's full-width grid, since it only reopens the banner. */
.cookie-settings { margin: 14px 0 28px; }
.cookie-settings .cc-btn { display: inline-block; width: auto; }
.cc-status { font-weight: 650; }

@media (max-width: 640px) {
  .cc-inner { padding: 12px 16px 14px; }
  .cc-text { margin-bottom: 10px; font-size: 13px; }
  .cc-actions { gap: 6px; }
  .cc-btn { padding: 9px 6px; font-size: 13px; }
}

/* ---------- multi-file upload (TODO-060) ----------
   Only drawn when config.ini [features] MULTI_FILE_UPLOAD = true; app.js then
   puts .mf-on on #calculator. Agreed from the reviewed mockup, see TODO-060
   "Agreed design". In this design amber means "Invalid data" and BLOCKS
   Calculate, like red; it is not a warning. */

.sr-only {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}

/* "Add another file" sits to the left of Calculate */
.calc-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-left: auto; }

/* the data card takes dropped files too */
.mf-on .data-card.dragover { outline: 2px dashed var(--accent); outline-offset: 3px; }

/* Show combined dataset: a left/right switch beside Text Data / Tabular Data.
   Off: the source selected in the list. On: every source combined. */
.data-card-head .mf-switch { margin-right: auto; }
.mf-switch {
  display: inline-flex; align-items: center; gap: 8px;
  font: inherit; font-size: 13px; font-weight: 500; color: var(--muted);
  background: none; border: 0; padding: 4px 2px; cursor: pointer;
}
.mf-switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.mf-track {
  position: relative; width: 36px; height: 20px; flex-shrink: 0;
  border-radius: 999px; background: var(--faint); transition: background .15s;
}
.mf-knob {
  position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
  border-radius: 50%; background: #fff; box-shadow: var(--shadow-sm); transition: left .15s;
}
.mf-switch[aria-checked="true"] { color: var(--text); font-weight: 600; }
.mf-switch[aria-checked="true"] .mf-track { background: var(--accent); }
.mf-switch[aria-checked="true"] .mf-knob { left: 18px; }
@media (prefers-reduced-motion: reduce) { .mf-track, .mf-knob { transition: none; } }

/* invalid rows are amber here: the same marks the single-file page draws in
   red, recoloured, with a "!" in the gutter instead of the cross */
.mf-on .hl-error { background: var(--orange-soft); }
.mf-on .gut-error { background: var(--orange-soft); color: var(--orange); }
.mf-on table.data tr.row-error td,
.mf-on table.data tbody tr.row-error:hover td { background: var(--orange-soft); }
.mf-on table.data tr.row-error td.rownum { color: var(--orange); font-weight: 600; }
/* the combined view is read only, but keeps the white text area */
.mf-on .pane-preview td.src { color: var(--muted); }

/* the source list: under the data card, above Calculate */
.mf-list {
  margin: 16px 0 0; background: var(--surface);
  border: 1px solid var(--border); border-radius: 12px; box-shadow: var(--shadow-sm);
  overflow: hidden;
}
.mf-head, .mf-row {
  display: grid; grid-template-columns: minmax(0, 1fr) 84px 150px 56px;
  column-gap: 12px; align-items: center; padding: 10px 16px;
}
.mf-head {
  background: var(--surface-2); border-bottom: 1px solid var(--border);
  color: var(--muted); text-transform: uppercase;
}
/* one font for every heading: .c-rows sets the figures in mono, and that must
   not reach the "Rows" heading */
.mf-head > span { font-family: inherit; font-size: 12px; font-weight: 600; letter-spacing: .03em; }
.mf-head .c-rows, .mf-row .c-rows { text-align: right; }
.mf-list .c-remove { display: flex; justify-content: center; }
.mf-item { border-bottom: 1px solid var(--border); }
.mf-item:last-child { border-bottom: 0; }
.mf-item.selected { background: var(--accent-soft); box-shadow: inset 3px 0 0 var(--accent); }
.mf-name {
  font: inherit; text-align: left; background: none; border: 0; padding: 0;
  color: var(--text); cursor: pointer; min-width: 0;
}
.mf-name:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
.mf-name .fn { display: block; font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* the typed-in entry is not a file, so its name is blue to set it apart */
.mf-name .fn.fn-typed { color: var(--blue); }
.mf-name .meta { display: block; font-size: 12px; color: var(--muted); margin-top: 1px; }
.mf-tag {
  display: inline-block; margin-left: 6px; padding: 0 6px; border-radius: 4px;
  font-size: 11px; font-weight: 600; vertical-align: 1px;
  background: var(--blue-soft); color: var(--blue);
}
.mf-row .c-rows { font-family: var(--mono); font-size: 13.5px; }

/* status lights: shape as well as colour (TODO-020), label beside the icon */
.light { display: flex; align-items: center; gap: 7px; font-size: 13px; font-weight: 600; white-space: nowrap; }
.light-icon {
  width: 22px; height: 22px; flex-shrink: 0; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
}
.light.red { color: var(--loss); }
.light.red .light-icon { background: var(--loss); color: #fff; }
.light.amber { color: var(--orange); }
.light.amber .light-icon { background: none; color: var(--orange); } /* a triangle */
.light.green { color: var(--gain); }
.light.green .light-icon { background: var(--gain); color: #fff; }
.light.empty { color: var(--faint); font-weight: 500; }
.light.empty .light-icon { border: 2px dashed var(--faint); }
.light.busy { color: var(--muted); font-weight: 500; }

.mf-remove {
  width: 32px; height: 32px; border: 0; border-radius: 6px; background: none;
  color: var(--muted); cursor: pointer; display: flex; align-items: center; justify-content: center;
}
.mf-remove:hover { background: var(--surface-2); color: var(--loss); }
.mf-remove:focus-visible { outline: 2px solid var(--accent); }

/* each entry's own error box, under its name */
.mf-errors {
  margin: 0 16px 12px; padding: 8px 12px; border-radius: var(--radius-sm);
  font-size: 13px; border: 1px solid;
}
.mf-errors.red { border-color: var(--loss); background: var(--loss-soft); color: var(--loss); }
.mf-errors.amber { border-color: var(--orange); background: var(--orange-soft); color: var(--orange); }
.mf-errors .eh { font-weight: 600; }
.mf-errors ul { margin: 4px 0 0; padding-left: 20px; }
.mf-errors li { margin-bottom: 2px; }
.mf-errors li b { font-weight: 600; }
.mf-errors .linkish { color: inherit; text-decoration: underline; font-size: 13px; margin-top: 4px; }

/* a file that could not be read has nothing to preview */
.mf-unread { padding: 30px 20px; color: var(--muted); font-size: 14px; text-align: center; }

/* hover pop-up for an invalid row */
.row-tip {
  position: fixed; z-index: 60; max-width: 360px; pointer-events: none;
  padding: 8px 12px; border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text); font-size: 13px; line-height: 1.4;
  border: 1px solid var(--orange); border-left-width: 4px; box-shadow: var(--shadow-lg);
}
.row-tip[hidden] { display: none; }
.row-tip b { color: var(--orange); font-weight: 600; }
.row-tip ul { margin: 4px 0 0; padding-left: 18px; }
.row-tip li { margin-bottom: 2px; }

/* the confirmation dialog (delete a file, over the size limit, storage
   nearly full), in the feedback dialog's frame */
.mf-confirm { width: min(440px, calc(100vw - 32px)); }
.mf-confirm p { margin: 0 0 20px; font-size: 14.5px; line-height: 1.5; }
.mf-confirm-actions { display: flex; justify-content: flex-end; gap: 10px; }
.mf-confirm-actions button { font-size: 14px; padding: 9px 18px; }

@media (max-width: 640px) {
  .mf-head { display: none; }
  .mf-row { grid-template-columns: minmax(0, 1fr) auto 36px; row-gap: 4px; padding: 10px 12px; }
  .mf-row .c-rows { grid-column: 1; grid-row: 2; text-align: left; font-size: 12.5px; color: var(--muted); }
  .mf-row .c-rows::after { content: " rows"; }
  .mf-row .c-status { grid-column: 2; grid-row: 1 / span 2; }
  .mf-row .c-remove { grid-column: 3; grid-row: 1 / span 2; }
  .light .light-text { display: none; }
  .mf-errors { margin: 0 12px 10px; }
}
