﻿/* ==========================================================================
   ArrayTrace design system - v1
   --------------------------------------------------------------------------
   Values are lifted from Documentation/design/tokens/design-tokens.css, which
   is the authority for WHAT the values are. STYLES.md is the authority for HOW
   they are applied. This file and Services/ThemeService.cs must stay in sync:
   change the token file first, then propagate to both.

   Light is the default surface - the marketing site, the app and the printed
   report all start on warm paper. Dark is still a first-class mode and the one
   the design canvas is tuned for; it is reached by setting data-theme="dark" on
   any element, and whole sections of a light page deliberately do exactly that.

   There is deliberately NO --color-* alias layer - components consume --sv-*
   directly.
   ========================================================================== */

/* ── Fonts ────────────────────────────────────────────────────────────────
   Self-hosted. No CDN link: the app must render correctly offline and on a
   site with no signal, and an external font request is a privacy and
   reliability liability. Latin and Latin-Extended subsets only.
   ------------------------------------------------------------------------ */

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                 U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
                 U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
                 U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-500-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                 U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-500-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
                 U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
                 U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-600-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                 U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-600-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
                 U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
                 U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                 U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
                 U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
                 U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-500-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                 U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-500-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
                 U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
                 U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-600-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                 U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-600-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
                 U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
                 U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ── Tokens - light (the default surface) ─────────────────────────────────── */

:root,
[data-theme='light'] {
  --sv-bg: #F7F8FA;
  --sv-bg-raised: #FFFFFF;
  --sv-bg-overlay: #F2EFEA;
  --sv-bg-sunken: #F2EFEA;

  --sv-border-subtle: #EAECF0;
  --sv-border: #E4E7EC;
  --sv-border-strong: #D0D5DD;

  /* A BOUNDARY INSIDE A SURFACE IS NOT THE EDGE OF ONE. The three above are the edges a box draws;
     these four are the rules a box draws inside itself, and a card that separates its band, its
     rows and its inputs with one hairline has nothing left to tell a region from a row. */
  --sv-border-band: #E3DED5;
  --sv-border-row: #F3F0EA;
  --sv-border-input: #E0DBD2;

  /* Three surfaces that used to share one tint with the badges and the popovers, and could not be
     moved without moving those too. A heading band, a column row and a control fill are three
     different jobs, so each is named from what it is for. */
  --sv-card-band: #F0EBE2;
  --sv-table-head: #FCFBF9;
  --sv-input-fill: #FFFFFF;
  --sv-divider-band: #DAD4C9;

  --sv-text: #17202A;
  --sv-text-secondary: #667085;
  --sv-text-tertiary: #8B939E;
  --sv-text-faint: #B4BAC2;

  /* The tier under tertiary, warm rather than the cool greys above it: an eyebrow, a field label
     and a unit label are read on paper beside a measured figure. Then what nobody may use, and the
     dash a cell renders where nothing was measured - which is a different statement from nought
     and must not be set in the type of a value. */
  --sv-text-quiet: #9A958B;
  --sv-text-disabled: #B5AFA4;
  --sv-text-dash: #D8D3C9;

  /* One unstarred row in a column of stars. A star is a shape, so it sits at the weight of the
     border tokens rather than of the type beside it. */
  --sv-star-inactive: #CFC9BF;

  /*
    The one gotcha, and it is why --sv-accent-fill exists. #F5A524 is a FILL
    colour, not a text colour - it is about 2.1:1 on paper and fails WCAG AA for
    body text. So on paper the roles split: --sv-accent darkens for text, links
    and numerals, while --sv-accent-fill keeps the true amber for anything the
    eye reads as a shape rather than a word - buttons, chart bars, the logo disc.

    This is a semantic split, not an alias layer: the two carry different values
    in light mode and the same value in dark. Never hardcode #F5A524 as a text
    colour; use var(--sv-accent) and let the mode resolve it.
  */
  --sv-accent: #B8791A;
  --sv-accent-hover: #96610F;
  --sv-accent-deep: #F5A524;
  --sv-accent-fill: #F59E0B;
  --sv-accent-fill-hover: #D97706;
  --sv-accent-wash: rgba(245, 165, 36, 0.14);
  --sv-on-accent: #12181F;

  /* THREE OPAQUE AMBER TINTS, and opaque is the whole point. --sv-accent-wash is translucent,
     which is right over satellite imagery and wrong where two of these meet: a badge inside a
     selected row takes the row's wash a second time and comes out a colour nobody chose. */
  --sv-accent-badge: #F8EED8;
  --sv-accent-avatar: #F0E4CB;
  --sv-accent-underline: #E3D2AE;
  --sv-mark-shaded: #C9C3BA;
  --sv-mark-ground: #C9C3BA;

  --sv-module: #2C4A6B;
  --sv-module-edge: #1E3A57;
  --sv-module-shaded: #5A6470;

  /* Wiring-zone identity - data, like the module blues, never chrome. Legible on
     satellite imagery in both themes, which is why they do not change below. */
  --sv-zone-1: #F5A524;
  --sv-zone-2: #38BDF0;
  --sv-zone-3: #F0559B;
  --sv-zone-4: #54C48A;
  --sv-zone-5: #9B7BE8;
  --sv-zone-6: #7FA8D9;

  /* Loss-donut ramp - data, one family rather than six hues, and read on a page
     rather than on imagery so it can use the full range. See design-tokens.css. */
  --sv-loss-1: #1D4E89;
  --sv-loss-2: #2C6FB5;
  --sv-loss-3: #4A90D9;
  --sv-loss-4: #7FB2E5;
  --sv-loss-5: #A9C9EC;
  --sv-loss-6: #CBDDF3;

  /* The printed report's series ramp, and the one family declared here that no
     stylesheet reads: QuestPDF cannot resolve a custom property, so
     Infrastructure/Documents/DocumentPalette carries the values and this is what
     it is held to. Six steps of the module blue, because a proposal's charts are
     read beside the array drawing on the page before them. Declared with the
     light block alone, as the loss ramp is - paper has no dark mode. */
  --sv-doc-series-1: #1E3A57;
  --sv-doc-series-2: #2C4A6B;
  --sv-doc-series-3: #3A5A7F;
  --sv-doc-series-4: #496B93;
  --sv-doc-series-5: #587CA7;
  --sv-doc-series-6: #688DBB;

  /* A SURFACE THAT CARRIES ITS OWN CONTRAST, and so does not follow the theme -
     fixed in both modes for the same reason the wiring zones are. A photograph
     is not a design surface: a scrim that inverted would be a light haze over a
     bright roof in one mode and legible in the other. Paper is not one either:
     the template artboard shows the sheet that will print, and a sheet that goes
     dark with the reader's chrome is a sheet nobody can judge a layout on. */
  --sv-unthemed-ink: #12181F;
  --sv-unthemed-paper: #FCFBF9;

  --sv-obstruction: #B8791A;
  --sv-obstruction-ink: #8A5B13;
  --sv-chart-bar: #F5A524;
  --sv-chart-loss: #C9C3BA;
  --sv-chart-track: #E6E1DA;
  --sv-map-canvas: #EDE9E3;

  /* darkened to hold 4.5:1 on paper, exactly as the accent is */
  --sv-danger: #C62828;
  --sv-danger-ink: #C62828;
  --sv-on-danger: #FFFFFF;

  /* THE TINT A ROW OR A CELL TAKES WHERE SOMETHING IS STOPPED, and the counterpart of
     --sv-accent-wash. Severe is not a louder attention, so a blocking row and a warning row cannot
     both be amber. Mixed from the danger colour rather than fixed, so it follows the mode - and
     named once, because three rules were writing the same mix by hand. */
  --sv-danger-wash: color-mix(in srgb, var(--sv-danger) 8%, transparent);

  /* A verdict, which is DATA - the same category as the module blues and the six
     wiring zones, and not a second brand colour. Amber already means "read this
     before continuing", so it cannot also mean "this one cleared". */
  --sv-pass: #2F6B4A;
  --sv-pass-fill: #EDF5F0;
  --sv-pass-border: #D8E7DE;

  /* LIFECYCLE TONES, which are DATA - a status is the one thing a reader scans a whole
     column of, so it has to be told apart at a glance rather than read word by word.
     Three hues here plus three the product already has: waiting is the accent, approved
     is the pass green, lost is the danger red. Spaced around the wheel rather than
     shaded along a ramp, because these are named states and not a quantity. */
  --sv-status-progress: #1F5C8B;
  --sv-status-progress-fill: #E8F1F8;
  --sv-status-progress-border: #CFE1EE;

  --sv-status-filed: #6B4BA8;
  --sv-status-filed-fill: #F0EBFA;
  --sv-status-filed-border: #DDD2F0;

  --sv-status-pending: #0E7490;
  --sv-status-pending-fill: #E4F3F3;
  --sv-status-pending-border: #C9E6E7;

  /* The two terminal outcomes are SOLID, and that is how they differ from the six
     states above. Won and Approved are both good news in one green, and weight
     separates them where a seventh hue nobody can name would not. */
  --sv-status-won-fill: #2F6B4A;
  --sv-on-status-won: #FFFFFF;


  /* THE READOUT STRIP: a card of headline figures, drawn dark, and the one surface on a page of
     white cards a reader lands on first. It CARRIES ITS OWN CONTRAST and so does not follow the
     theme - the dark block repeats these rather than changing them, exactly as --sv-unthemed-* and
     the wiring zones do. Warm rather than the dark theme's blue near-black, because it sits on a
     warm page. */
  --sv-readout: #102238;
  --sv-readout-border: #24313F;
  --sv-readout-ink: #FBFAF8;
  --sv-readout-ink-quiet: #9FAAB6;
  --sv-readout-ink-faint: #95A0AC;
  --sv-readout-link: #E9B457;

  /* A MARKED CELL ON THIS GROUND KEEPS THE GROUND. The marking is the cell's ink and an edge down
     its side, never a tint across it: amber carries almost no blue, so a wash quiet enough to read
     through neutralises this navy and the cell arrives grey-olive - a marking that no longer names
     its own hue, which is the one thing a marking is for. Two dark surfaces in one strip read as
     one of them being broken, and the strip is drawn dark precisely so it is one surface.

     The paper values do not survive the move either: --sv-accent measures under 3:1 here and
     --sv-danger-ink is a paper red on a near-black. Amber is --sv-accent-fill, which is the colour
     every icon on this strip already takes and holds 7.5:1 on the navy; the red is stated below
     because nothing else on this surface is red. */
  --sv-readout-pass: #6BCF9A;
  --sv-readout-severe: #E5484D;
  --sv-readout-severe-ink: #F0989B;


  /* THE FIELD-HELP PANEL, which carries its own contrast for the reason the readout strip does: it
     floats over a form of white boxes in one mode and dark ones in the other, and a panel that
     followed the theme would be a white card on a white card in the first. Not the readout's own
     three - that surface is a block of figures a reader lands on, this is a sentence over the top
     of one, so it sits a step lighter and takes an edge with enough lift to separate from either
     ground. The dark block repeats these rather than changing them. */
  --sv-help-panel: #162A44;
  --sv-help-panel-border: #3D5470;
  --sv-help-panel-ink: #F7F8FA;

  /* Depth is by border here; paper takes a far lighter shadow than near-black
     does, and the dark values below would read as soot on this surface. */
  --sv-shadow-panel: 0 24px 48px -28px rgba(18, 24, 31, 0.28);
  --sv-shadow-sheet: 0 18px 44px -22px rgba(18, 24, 31, 0.22);

  /* Tighter and nearer than --sv-shadow-panel: a tooltip sits a few pixels off the label rather
     than floating over the page, so the long soft cast a dialog throws reads as the panel having
     come unstuck. Mixed from --sv-nav-bg, which is the product's own near-black. */
  --sv-shadow-help: 0 8px 24px rgba(11, 23, 38, 0.40);

  color-scheme: light;
}

/* ── Tokens - dark (the instrument) ───────────────────────────────────────
   Set data-theme="dark" on <html> for the whole app, or on any single element
   to invert one band of a light page. The tokens are custom properties, so
   everything inside that element flips with no other change.
   ------------------------------------------------------------------------ */

[data-theme='dark'] {
  /* surfaces */
  --sv-bg: #0A121B;
  --sv-bg-raised: #0E1721;
  --sv-bg-overlay: #111A24;
  --sv-bg-sunken: #070D14;

  /* The same three roles on near-black: band and column row keep the overlay tint they always had
     here, and a control sits in the deepest well, which is what says it is a hole to type into. */
  --sv-card-band: #111A24;
  --sv-table-head: #111A24;
  --sv-input-fill: #070D14;

  /* borders */
  --sv-border-subtle: #1B2735;
  --sv-border: #24313F;
  --sv-border-strong: #33414F;

  /* The four boundaries inside a surface. On paper they fan out either side of --sv-border-subtle;
     on near-black the input outline and the default outline land on one value, and that agreement
     belongs to the mode - the two roles part again the moment either is retuned. */
  --sv-border-band: #1F2C3A;
  --sv-border-row: #16202C;
  --sv-border-input: #24313F;
  --sv-divider-band: #293745;

  /* text */
  --sv-text: #E8ECF1;
  --sv-text-secondary: #93A1B1;
  --sv-text-tertiary: #5D6E80;
  --sv-text-faint: #3E4C5C;

  /* The same three tiers the light block adds, stepping DOWN from tertiary, because fainter on
     near-black is darker rather than lighter. */
  --sv-text-quiet: #4E5D6E;
  --sv-text-disabled: #44515F;
  --sv-text-dash: #33404D;

  --sv-star-inactive: #3A4854;

  /* accent - sun. On near-black amber is ~8.4:1 and serves as both text and
     fill, so the two accent roles collapse back onto one value. */
  --sv-accent: #F59E0B;
  --sv-accent-hover: #FFB84D;
  --sv-accent-deep: #C97C0C;
  --sv-accent-fill: #F59E0B;
  --sv-accent-fill-hover: #FFB84D;
  --sv-accent-wash: rgba(245, 165, 36, 0.09);
  --sv-on-accent: #0A121B;

  /* The three opaque tints: the accent fill over --sv-bg-raised at 12 %, 18 % and 35 %, frozen so
     that a tint laid on a wash stays the colour it names. */
  --sv-accent-badge: #2A2821;
  --sv-accent-avatar: #383122;
  --sv-accent-underline: #5F4922;
  --sv-mark-shaded: #7A5413;
  --sv-mark-ground: #5D6E80;

  /* data - array, shading, charts */
  --sv-module: #1E3A57;
  --sv-module-edge: #4E7099;
  --sv-module-shaded: #2B3B4B;
  --sv-zone-1: #F5A524;
  --sv-zone-2: #38BDF0;
  --sv-zone-3: #F0559B;
  --sv-zone-4: #54C48A;
  --sv-zone-5: #9B7BE8;
  --sv-zone-6: #7FA8D9;
  /* Drawn on photography or on paper, so unchanged - see the light block. */
  --sv-unthemed-ink: #12181F;
  --sv-unthemed-paper: #FCFBF9;

  --sv-obstruction: #7A6335;
  --sv-obstruction-ink: #D0A85E;
  --sv-chart-bar: #F5A524;
  --sv-chart-loss: #2B3B4B;
  --sv-chart-track: #1B2735;
  --sv-map-canvas: #0B1420;

  /* Danger, and NOT a second accent:
     amber is the primary action colour, so a validation failure cannot also be amber.
     Validation errors and destructive actions only, never decorative. */
  --sv-danger: #E5484D;
  --sv-danger-ink: #F0989B;
  --sv-on-danger: #0A121B;
  --sv-danger-wash: color-mix(in srgb, var(--sv-danger) 8%, transparent);

  /* The verdict tone on near-black, lifted the way the danger red is. */
  --sv-pass: #6FCF97;
  --sv-pass-fill: #10201A;
  --sv-pass-border: #1F3A2C;

  /* The lifecycle tones on near-black: hue held, value raised, so the six states stay
     as far apart here as they are on paper. */
  --sv-status-progress: #7FB3DA;
  --sv-status-progress-fill: #0E1A24;
  --sv-status-progress-border: #1D3348;

  --sv-status-filed: #B49BE8;
  --sv-status-filed-fill: #17122A;
  --sv-status-filed-border: #2C2447;

  --sv-status-pending: #5FC7D9;
  --sv-status-pending-fill: #0A1E23;
  --sv-status-pending-border: #17383F;

  /* Ink ON the solid green inverts with the mode as --sv-on-danger does: white on the
     lifted green measures ~2.4:1 and fails. */
  --sv-status-won-fill: #6FCF97;
  --sv-on-status-won: #0A121B;


  /* Not the reader's theme's to change - see the light block. */
  --sv-readout: #102238;
  --sv-readout-border: #24313F;
  --sv-readout-ink: #FBFAF8;
  --sv-readout-ink-quiet: #9FAAB6;
  --sv-readout-ink-faint: #95A0AC;
  --sv-readout-link: #E9B457;

  /* A MARKED CELL ON THIS GROUND KEEPS THE GROUND. The marking is the cell's ink and an edge down
     its side, never a tint across it: amber carries almost no blue, so a wash quiet enough to read
     through neutralises this navy and the cell arrives grey-olive - a marking that no longer names
     its own hue, which is the one thing a marking is for. Two dark surfaces in one strip read as
     one of them being broken, and the strip is drawn dark precisely so it is one surface.

     The paper values do not survive the move either: --sv-accent measures under 3:1 here and
     --sv-danger-ink is a paper red on a near-black. Amber is --sv-accent-fill, which is the colour
     every icon on this strip already takes and holds 7.5:1 on the navy; the red is stated below
     because nothing else on this surface is red. */
  --sv-readout-pass: #6BCF9A;
  --sv-readout-severe: #E5484D;
  --sv-readout-severe-ink: #F0989B;


  /* Not the reader's theme's to change - see the light block. */
  --sv-help-panel: #162A44;
  --sv-help-panel-border: #3D5470;
  --sv-help-panel-ink: #F7F8FA;

  --sv-shadow-panel: 0 30px 70px -30px rgba(0, 0, 0, 0.85);
  --sv-shadow-sheet: 0 24px 60px -20px rgba(0, 0, 0, 0.70);
  --sv-shadow-help: 0 8px 24px rgba(11, 23, 38, 0.40);

  color-scheme: dark;
}

/* ── Type, shape and space ────────────────────────────────────────────────── */

:root {
  /* The navigation column is navy in BOTH themes - it is chrome, not content - so these sit
     outside the two theme blocks rather than being declared twice with one value. Every value is
     one the dark theme already declares. The active item is carried by the gold edge rather than
     by its fill. Mirrors design-tokens.css. */
  --sv-nav-bg: #0B1726;
  --sv-nav-border: #1B2735;
  --sv-nav-border-strong: #24313F;
  --sv-nav-hover: #111A24;
  --sv-nav-active: #1B2735;
  --sv-nav-accent: #F59E0B;
  --sv-nav-text: #E8ECF1;
  --sv-nav-text-secondary: #93A1B1;
  --sv-nav-text-tertiary: #5D6E80;
  --sv-nav-scroll: #33414F;

  --sv-font-sans: 'IBM Plex Sans', system-ui, -apple-system, sans-serif;
  --sv-font-mono: 'IBM Plex Mono', ui-monospace, monospace;

  /* SIX STEPS, NAMED BY ROLE RATHER THAN BY SIZE. A scale read as xs/sm/md invites a seventh the
     moment something falls between two of them, and DesignTokenReferenceTests exists because
     --sv-radius-sm was written against a scale that has no such name. */
  --sv-radius-chip: 3px;
  --sv-radius-item: 6px;
  --sv-radius-control: 7px;
  --sv-radius-card: 10px;
  /* The designer's floating panes, the marketing surfaces and the result panels, none of which the
     app's card grammar covers. Nothing INSIDE a card takes it: a box sunk in a card with corners
     looser than the card's reads as having escaped it. */
  --sv-radius-panel: 14px;
  --sv-radius-pill: 99px;

  /* ── The four responsive tracks, and the month grid beside them ─────────────
     A TRACK IS A STATED JUDGEMENT ABOUT HOW NARROW A COLUMN MAY GET, so it is named once and read,
     never re-derived. Held as custom properties rather than only as classes because a screen whose
     gap or margin has to differ still has to wear the same judgement, and a second literal is two
     decisions that agree today.

     THE `min(100%, N)` FLOOR ON THE TWO FIELD TRACKS IS NOT DECORATION. A plain `minmax(N, 1fr)`
     demands N per track whatever the container has, so a field grid inside a card asks for more
     width than the card, and a card stating `overflow: hidden` clips its own controls with nothing
     reporting it. The strip and card tracks are drawn on the well rather than inside a bounded box
     and keep the plain floor. */
  --sv-track-kpi: repeat(auto-fit, minmax(190px, 1fr));
  --sv-track-cards: repeat(auto-fit, minmax(280px, 1fr));
  --sv-track-fields: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  --sv-track-fields-tight: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
  --sv-track-months: repeat(auto-fit, minmax(min(100%, 116px), 1fr));

  --sv-space-1: 4px;
  --sv-space-2: 8px;
  --sv-space-3: 12px;
  --sv-space-4: 16px;
  --sv-space-5: 22px;
  --sv-space-6: 32px;
  --sv-space-7: 48px;

  /* --sv-shadow-* are NOT here. They differ by mode and are declared in the two
     token blocks above; a copy in this block would sit later in the file at
     equal specificity to :root and silently win back the dark values on paper. */

  /* The two the application shell measures itself with. Declared here rather
     than in app-shell.css because a token is a value the whole document may
     read, and a shell that hid its own measurements would be a shell nothing
     else could line up against. The print surface needs no such token: it is
     composed in millimetres and measures itself. */
  --sv-sidebar-width: 236px;
  --sv-topbar-height: 44px;

  /* THE AIR EITHER SIDE OF THE APPLICATION WELL, and the one number that puts the
     utility bar, the back link, the title row, the section strip and the page on a
     single left edge. Every one of them reads this and nothing states 40 itself.

     NO MAXIMUM WIDTH GOES WITH IT. The app is an instrument reading eight columns
     of figures and it takes the monitor it is given; a centred column would strand
     a third of a wide screen either side of a wire schedule. .sv-container's
     1120px is the MARKETING measure, set for a column of prose, and stays there. */
  --sv-well-gutter: 40px;
}

/* ── Base ─────────────────────────────────────────────────────────────────── */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--sv-bg);
  color: var(--sv-text);
  font: 400 13px/1.45 var(--sv-font-sans);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3, h4, h5, h6 {
  margin: 0;
  font-family: var(--sv-font-sans);
  font-weight: 600;
  color: var(--sv-text);
}

p {
  margin: 0 0 var(--sv-space-3);
}

/* THE GAP UNDER A PARAGRAPH IS THE GAP BEFORE WHAT FOLLOWS IT, so where nothing follows there is
   no gap to keep. A card, a panel and an alert all state their own padding, and a trailing margin
   inside one is a second bottom edge nobody chose - which is why fifty-odd paragraphs across the
   product were written `style="margin: 0"` to take it back off again, and why the ones nobody
   thought to write it on sit a few pixels lower than their neighbours. */
p:last-child {
  margin-bottom: 0;
}

a {
  color: var(--sv-accent);
  text-decoration: none;
}

a:hover {
  color: var(--sv-accent-hover);
  text-decoration: underline;
}

:focus-visible {
  outline: 2px solid var(--sv-accent);
  outline-offset: 2px;
}

/* THE ONE THING FOCUSED WITHOUT ANYBODY ASKING FOR IT, AND SO THE ONE THING NOT RINGED.
   `Routes.razor` carries `<FocusOnNavigate Selector="h1" />`, which is there so a screen reader
   announces the new page's title rather than leaving the reader at the top of a document that has
   silently changed underneath them. To focus a heading at all it has to make one focusable, so
   Blazor stamps `tabindex="-1"` on it - and a browser treats programmatic focus on a non-input as
   worth drawing a ring for. The result is a gold box around the page title after every navigation,
   which reads as the title having been selected, and was reported as exactly that.

   THE FOCUS STAYS AND ONLY THE RING GOES, because the two serve different people. The focus is the
   whole point for somebody listening to the page. The ring is for somebody who moved focus
   themselves and needs to see where it landed - and nobody can move focus here, because
   `tabindex="-1"` is not reachable by tabbing. So the ring can only ever appear after a navigation
   the reader just made, marking a heading they cannot act on.

   NARROW ON PURPOSE: it names the heading and the negative tabindex together, so a real focus ring
   on anything a person can actually reach is untouched. */
h1[tabindex="-1"]:focus,
h1[tabindex="-1"]:focus-visible {
  outline: none;
}

hr {
  border: 0;
  border-top: 1px solid var(--sv-border-subtle);
  margin: var(--sv-space-4) 0;
}

code, pre, kbd, samp {
  font-family: var(--sv-font-mono);
}

/* ── Typography scale ─────────────────────────────────────────────────────── */

.sv-h1      { font: 600 30px/1.15 var(--sv-font-sans); letter-spacing: -0.025em; }
.sv-h2      { font: 600 23px/1.2  var(--sv-font-sans); letter-spacing: -0.02em; }
.sv-h3      { font: 600 15px/1.3  var(--sv-font-sans); }
.sv-body    { font: 400 14px/1.55 var(--sv-font-sans); }
.sv-ui      { font: 400 13px/1.45 var(--sv-font-sans); }
.sv-small   { font: 400 12px/1.5  var(--sv-font-sans); }

/*
  THE most load-bearing rule in the system. Every measured value - kWh, kWp, %,
  coordinates, tariffs, IDs, timestamps, dates - is set in mono. Prose and
  headings are sans. A kWh figure in sans is a defect: it is what makes the tool
  read as an instrument rather than a website.
*/
.sv-mono   { font-family: var(--sv-font-mono); font-variant-numeric: tabular-nums; }

/* A SECRET SHOWN ONCE, TO BE COPIED OUT BY HAND - a recovery code, an authenticator setup key.
   Mono like every other measured value, and one click takes the whole of it: these are the two
   strings in the product a person is asked to transcribe under pressure, and a part-selection they
   do not notice is an account they cannot get back into. */
.sv-secret {
  font-family: var(--sv-font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--sv-text);
  user-select: all;
}
.sv-metric { font: 400 26px/1.1 var(--sv-font-mono); font-variant-numeric: tabular-nums; color: var(--sv-text); }
.sv-metric--sm { font-size: 19px; }

/* Uppercase mono labels. Never uppercase prose. */
.sv-label {
  font: 500 10px/1.2 var(--sv-font-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--sv-text-tertiary);
}

/* THE SECTION EYEBROW, and the one spacing rule that has to be kept in one place.
   .sv-label on its own is an inline chip - it appears 280 times, most of them
   beside something else in a flex row - so it carries no margin and must not
   start carrying one. A label introducing a BLOCK below it is a different thing:
   it is a heading, and with no margin it butts against the card, table or
   control it names. That is what was reported on the conditions screen and again
   on the schedules screen, and it is the same defect both times.

   The standard: a label that introduces content takes BOTH classes. It is a
   block, it owns the gap below it, and it owns a larger gap above it so two
   sections in one panel do not run together. */
/* IT MUST OUTRANK WHAT IT INTRODUCES. .sv-label is tertiary because an inline chip sits beside a
   measured value and has to stay out of its way. A heading over a block inherited that, which put
   it a step LIGHTER than the prose underneath it (secondary) - so the eye met the paragraphs first
   and the headings read as captions belonging to the text above them. Reported on the designer's
   help sheet, and true of every section label over a run of prose.

   Full-strength text and a heavier weight, at the same 10px/+0.1em uppercase mono: the size and the
   letter-spacing are the design system's and are not what was wrong. Measured on paper, contrast
   goes 3.10:1 to 17.85:1 against body prose at 5.84:1, so the heading outranks what it introduces
   instead of sitting a step under it - and a 10px label clears AA for the first time. */
.sv-label--section {
  display: block;
  font-weight: 600;
  color: var(--sv-text);
  margin-bottom: var(--sv-space-3);
}

.sv-label--section + * {
  margin-top: 0;
}

/* Not the first one in its container: that gap belongs to the container. */
.sv-label--section:not(:first-child) {
  margin-top: var(--sv-space-5);
}

/* A label sitting UNDER a page title, naming the section the title is in. With
   the back link above the title this is where the old eyebrow went, and a
   heading's own margin does not reach it. */
h1 + .sv-label {
  display: block;
  margin-top: var(--sv-space-1);
}

.sv-unit {
  font: 400 12px/1 var(--sv-font-mono);
  color: var(--sv-text-tertiary);
  margin-left: 3px;
}

/* A READABLE MEASURE. Prose set across a 1600px panel runs to about 200
   characters a line, and the eye loses the start of the next one on every
   return - which is most of what "this page is not very nice" means on a screen
   whose type and colour are already right. Roughly 68 characters is the measure
   the typographic literature settles on and the checklist copy already uses. */
.sv-prose {
  max-width: 68ch;
}

.sv-text-secondary { color: var(--sv-text-secondary); }
.sv-text-tertiary  { color: var(--sv-text-tertiary); }
.sv-text-accent    { color: var(--sv-accent); }

/* ── A page that is mostly prose ──────────────────────────────────────────────
   About, the privacy policy and the terms: a heading, some paragraphs, another
   heading. The rhythm belongs to the document, not to each of its twenty-four
   headings, which is the same rule .sv-label--section already keeps for a label
   introducing a block.
   ------------------------------------------------------------------------ */
.sv-longform > .sv-h2 {
  margin-top: var(--sv-space-6);
}

/* The first thing in the document has the section's own padding above it. */
.sv-longform > :first-child {
  margin-top: 0;
}

/* The opening line under the document's own title, with or without an eyebrow between them. NOT the
   eyebrow itself, which is the title's own kicker and sits tight under it - `h1 + .sv-label` states
   that, and a rule here matching every paragraph would outrank it and stand the eyebrow off the
   title by the step that belongs to the paragraph below it. */
.sv-longform > h1 + p:not(.sv-label),
.sv-longform > h1 + .sv-label + p {
  margin-top: var(--sv-space-5);
}

/* WHAT A DOCUMENT INTRODUCES, as opposed to what it says. A paragraph runs straight on under its
   heading and its own bottom margin is the gap to the next one, but a panel, a list of pairs, a
   photograph or a row of actions is a block - and each of the nine across about, contact, privacy
   and terms stated its own step above it, in three different values for one relationship. */
.sv-longform > .sv-panel,
.sv-longform > .sv-pairs {
  margin-top: var(--sv-space-5);
}

.sv-longform > figure,
.sv-longform > .sv-row {
  margin-top: var(--sv-space-6);
}

/* A note about the panel above it rather than a new paragraph of the document. */
.sv-longform > .sv-panel + p {
  margin-top: var(--sv-space-4);
}

/* ── A label and its value, read down the page ────────────────────────────────
   .sv-facts is the same pair laid ACROSS a panel, and it fixes the value's face -
   mono, full contrast, 15px - because everything in one is a measurement. A
   support address, a retention period and a registered address are prose, so this
   states the two gaps and leaves the words to whatever class the caller puts on
   them. Each pair is its own element because bare dt/dd pairs mis-align.
   ------------------------------------------------------------------------ */
.sv-pairs {
  display: flex;
  flex-direction: column;
  gap: var(--sv-space-4);
  margin: 0;
}

.sv-pairs > div {
  display: flex;
  flex-direction: column;
  gap: var(--sv-space-1);
}

.sv-pairs dd {
  margin: 0;
}

/* ── Surfaces ─────────────────────────────────────────────────────────────── */

/* Depth by border, not shadow. Shadow is for things that genuinely float. */
.sv-card {
  background: var(--sv-bg-raised);
  border: 1px solid var(--sv-border-subtle);
  border-radius: var(--sv-radius-card);
  padding: var(--sv-space-4);
}

/* A CARD THE READER IS BEING ASKED TO CHECK, not one they are being shown. It carries the
   machine-read datasheet panel, where every figure below is a transcription nobody has
   confirmed yet. The accent border and nothing else: amber is the only chromatic colour in
   the product and it already means "read this before continuing" on .sv-alert--success and
   --warning, so this is that decision applied, not a second one. No wash, because the panel
   holds its own alerts and two washes behind one another read as a rendering fault. */
.sv-card--accent {
  border-color: var(--sv-accent);
}

.sv-panel {
  background: var(--sv-bg-raised);
  border: 1px solid var(--sv-border);
  border-radius: var(--sv-radius-panel);
  padding: var(--sv-space-5);
}

/* THE STEP FROM ONE BLOCK OF A CARD TO THE NEXT. A card is a heading and then a few blocks - a
   sentence about it, a form, a notice, a table, the row that acts on it - and about eighty of them
   across the product stated that step on each block in turn, in four values for one relationship.

   A MARGIN RATHER THAN A FLEX GAP, for the reason .sv-pagehead__body gives and for one more: a card
   whose content is a single button or a single link would have that child stretched across it by a
   flex column, and it renders at its own width today. Margins collapse, so a block already carrying
   one of its own keeps the larger of the two, and an inline child is left exactly as it is. */
.sv-card > * + *,
.sv-panel > * + * {
  margin-top: var(--sv-space-4);
}

/* A SUB-HEADING STARTS A NEW GROUP, so it stands further off than an ordinary block does. The
   container's own heading is CardHeader's; this is the second and third group of fields inside one,
   and three surfaces each stated the step on their own. */
:is(.sv-card, .sv-panel, .sv-dialog__body) > * + .sv-h3 {
  margin-top: var(--sv-space-6);
}

.sv-card__header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sv-space-3);
  margin-bottom: var(--sv-space-3);
}

/* ── A card whose content runs to its own edges ────────────────────────────
   ONE SHAPE, DECLARED THREE TIMES. The verdicts on Compliance, the model list and the editor on
   Storage, and the SupplyStrip both of those screens wear are one object: a card with a heading
   band across the top of it and regions under it that reach the border. Written per screen, the
   band was filled on two of them and not on the third, and the heading over it was 13.5px sans on
   two and a 10.5px uppercase mono eyebrow on the third - one shape wearing three faces on two
   screens a reader moves between with one click.

   `overflow: hidden` is what lets a band's fill, a table's first row and a footer's rule reach the
   radius without any of them drawing a corner of its own. The border is the stronger token, because
   this card is divided into regions and a hairline its own dividers out-weigh reads as the regions
   having escaped it.
   ────────────────────────────────────────────────────────────────────────── */

.sv-card--flush {
  border-color: var(--sv-border);
  padding: 0;
  overflow: hidden;
}

/* The step from one block of a card to the next is what the padding paid for. A flush card's
   regions state their own edges and butt against one another by rule, so the step goes with it. */
.sv-card--flush > * + * {
  margin-top: 0;
}

/* THE HEADING BAND, AND IT IS A SURFACE RATHER THAN A ROW. Whatever sits in it lays itself out -
   `CardHeader` is already a space-between flex row, and the readout `SupplyStrip` bands is a block
   with sentences under it - so a `display: flex` here would set those sentences beside the run they
   belong under.

   The second selector is not a second declaration: `.sv-notice` is written further down this file
   and wins on equal specificity, so a notice wearing the band would otherwise keep its own sunken
   box, its own radius and its own trailing margin. */
.sv-card__band,
.sv-notice.sv-card__band {
  margin: 0;
  padding: 11px var(--sv-space-4);
  background: var(--sv-card-band);
  border: 0;
  border-bottom: 1px solid var(--sv-border-band);
  border-radius: 0;
}

/* A BAND WITH NOTHING UNDER IT IS THE CARD'S LAST ROW, and a rule along the bottom edge of a card
   rules nothing off. A screen that has computed or checked nothing yet draws the same card with its
   body absent rather than in a second arrangement. */
.sv-card__band:last-child {
  border-bottom: 0;
}

/* `CardHeader` wearing the band. Its bottom margin is the step to the block under it on a padded
   card, and inside a band's own fill it is a hole. */
.sv-card__band > .sv-card__header {
  margin: 0;
}

/* THE BAND'S HEADING, AND THE ELEMENT IS WHAT MUST NOT CHANGE: it is `CardHeader`'s h2 on every
   card in the product and only the face is restated here. A band is a label on the region under it
   rather than a title competing with the page's own name a hand's width above, so it comes down a
   step in size and up one in weight. `supply-strip.css` states this face a second time for the one
   heading in the product that is a promoted `.sv-label` rather than an h2, because a rule in this
   file may not name a family an owned stylesheet draws. */
.sv-card__band > .sv-card__header > .sv-h3 {
  font: 700 13.5px/1.3 var(--sv-font-sans);
  letter-spacing: -0.005em;
}

/* WHAT THE CARD SAYS ABOUT ITSELF, opposite the heading. `.sv-label` is a 10px uppercase chip,
   which is what a label introducing the block below it wants; here it is a count read across from a
   title, so it keeps the mono face and gives up the shouting. Written against `CardHeader`'s own
   note rather than against the band, because a band can hold a component whose labels are labels. */
.sv-card__band > .sv-card__header > .sv-label {
  font-size: 10.5px;
  letter-spacing: 0;
  text-transform: none;
  color: var(--sv-text-faint);
}

/* The segmented control owns its own spacing where it heads a list, which is every other place it
   is drawn. IN A REGION OF A CARD IT DOES NOT: the band and the toolbar each state their own
   padding, and a bottom margin inside a centred row lifts the control half of itself above
   everything beside it and stands the whole row 16px taller than the tallest thing in it. Both
   regions, because the row that narrows a card's own rows is the toolbar and the row that titles it
   is the band, and a reader meets the two a hairline apart. */
.sv-card__band .sv-segmented,
.sv-card__toolbar .sv-segmented {
  margin-bottom: 0;
}

/* A STRIP OPPOSITE A BAND'S HEADING, which is the one place it carries no rule of its own. The
   strip owns its spacing everywhere else it is drawn - `var(--sv-space-5) 0` and a bottom border -
   and both are wrong here: the band already draws the line under itself, so a second one lands a
   pixel above it, and the vertical margin would stand the tabs off a heading they share a baseline
   with. The REGION resets it, as it does for the segmented control above, rather than the caller,
   which `TabStripGuardTests` refuses. */
.sv-card__band .sv-tabs {
  gap: var(--sv-space-3);
  margin: 0;
  border-bottom: 0;
}

/* The tab keeps the strip's own face and gives up only the room it needs to sit on one line with a
   heading rather than above a page. */
.sv-card__band .sv-tabs__tab {
  padding: 2px 1px;
  margin-bottom: 0;
}

/* THE RULE BETWEEN A BAND'S HEADING AND THE FACTS AFTER IT, and it is shorter and softer than the
   one that separates two groups on a toolbar: a band is a single line of type, so a divider at the
   full 20px stands proud of the words either side of it. */
.sv-card__band .sv-divider--vertical {
  height: 13px;
  background: var(--sv-divider-band);
}

/* ── The facts after a band's heading ─────────────────────────
   `CardHeader` draws a heading and ONE thing opposite it, which is a count on fifty of them. Where
   that one thing is a run of facts and the way out of the section, this is the run: the rule that
   opens it, the labels, and the link pushed to the edge the reader's eye ends on.

   THE STRIP PUNCTUATES ITSELF, so a page lists facts and never types a separator - the same rule the
   page header's own strip follows, for the same reason: a middot per fact written by hand is a
   chance to leave one out or put two in. BETWEEN TWO FACTS AND NOWHERE ELSE: the divider is what
   separates the heading from the facts, so a middot after it punctuates a boundary already drawn,
   and the link at the far end is a way out rather than a fact.
   ───────────────────────────────────────────────────────────────────────── */
.sv-card__facts {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--sv-space-2);
  flex: 1 1 auto;
  min-width: 0;
}

/* Naming the label on both sides is what keeps the separator to the one place it means something. */
.sv-card__facts > .sv-label + .sv-label::before {
  content: "\00B7";
  margin-right: var(--sv-space-2);
  color: var(--sv-text-faint);
}

.sv-card__facts-link {
  margin-left: auto;
  font: 500 12px/1.4 var(--sv-font-sans);
  white-space: nowrap;
}

/* THE CARD'S SECOND REGION, BETWEEN THE BAND AND THE BODY. A band TITLES a section; a toolbar
   LABELS a data view - the eyebrow naming what the rows are, and the controls that narrow them.

   NEVER BOTH A BAND AND A TOOLBAR SAYING THE SAME WORDS. A card carries at most four regions in
   this order - band, toolbar, body, note strip - and a heading repeated a hairline below itself
   reads as a rendering fault rather than as two levels.

   A FILTER ROW IN HERE IS A REGION OF THIS CARD, not a control floating beside one. `FilterBar`
   sits in the toolbar and still draws no surface of its own; what stays refused is a filter BOX
   dressed as a card, which is a different defect. `Documentation/claude/ui.md` carries the rule.

   THE FILL IS THE PAGE RATHER THAN THE BAND. Two filled regions stacked read as one tall band with
   a line through it, and the reader cannot tell which of them the heading belongs to. */
.sv-card__toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--sv-space-3);
  margin: 0;
  padding: 12px var(--sv-space-4);

  /* THE CARD, NOT THE PAGE. The band above it carries the tint; a second tinted region under it
     read as one tall band with a line through it, and the reader could not tell which of the two
     the heading belonged to. */
  background: var(--sv-bg-raised);
  border-bottom: 1px solid var(--sv-border-band);
}

/* The row owns its spacing everywhere else it is drawn; in a region that states its own padding
   that trailing margin is a hole under the controls. */
.sv-card__toolbar > .sv-filters {
  flex: 1 1 auto;
  margin-bottom: 0;
}

/* THE STRIP THAT NAVIGATES ONE CARD'S OWN FACES, flush in it. It is a level below the page's own
   way across and takes the same underline grammar deliberately: sentence case, never a boxed or
   uppercase tab bar, and never a second strip inside a tab body.

   A REGION RATHER THAN A SCREEN'S CLASS, because Storage, Conditions, Financials and Electrical all
   want it and SectionSurfaceGuardTests forbids any file but this one from declaring a card's
   region - so this is the only lawful home for it. `TabStrip` is the component and the count inside
   a tab label is `.sv-tab-badge`.

   The strip owns its own spacing everywhere else it is drawn, so the region resets it here rather
   than the caller passing one. */
.sv-card__subtabs {
  padding: 0 var(--sv-space-4);
  border-top: 1px solid var(--sv-border-band);
}

.sv-card__subtabs .sv-tabs {
  margin: 0;
  gap: 20px;
  border-bottom: 0;
}

.sv-card__subtabs .sv-tabs__tab {
  padding: 10px 0 9px;
  margin-bottom: 0;
  font-size: 13px;
}

/* A strip inside a card is read against the card's own head rather than against a page title, so
   the selected face comes up a step in weight to hold its own beside it. */
.sv-card__subtabs .sv-tabs__tab[aria-current='page'] {
  font-weight: 600;
}

/* THE PADDED REGION UNDER A BAND. A table or a list of rows runs to the card's edges and wants none
   of this; prose, a form and a row of figures do. */
.sv-card__body {
  padding: var(--sv-space-4) var(--sv-space-4) 18px;
}

/* THE FIELD'S BOX IS RAISED IN HERE. `.sv-input` takes the sunken fill, which is right on a form
   drawn straight onto the well and wrong inside a raised card: a sunken box on a raised surface
   reads as disabled. The border and the radius are the product's own and are not restated. */
.sv-card__body :is(.sv-input, .sv-select) {
  background-color: var(--sv-bg-raised);
}

/* A VALUE TOO LONG FOR ITS COLUMN TRUNCATES RATHER THAN WIDENING THE CARD, whose `overflow: hidden`
   would otherwise clip it. A simulation named by its design, its condition set and its timestamp is
   longer than any column on any screen. */
.sv-card__body .sv-select {
  text-overflow: ellipsis;
}

/* A grid item's floor is its own min-content, and a labelled field inside a bounded card is exactly
   where that pushes a track wider than the card holding it. */
.sv-card__body .sv-field {
  min-width: 0;
}

/* A SENTENCE ABOUT THE REGION ABOVE IT, AS THE CARD'S LAST ROW. Left loose under the card and set
   in the type of the block above it, it reads as one more line of that block rather than as a
   statement about it. */
.sv-card__foot {
  display: flex;
  align-items: baseline;
  gap: var(--sv-space-2);
  margin: 0;
  padding: 10px var(--sv-space-4);
  background: var(--sv-bg);
  border-top: 1px solid var(--sv-border-subtle);
  font: 400 12px/1.5 var(--sv-font-sans);
  color: var(--sv-text-secondary);
  text-wrap: pretty;
}

/* THE ONE ROW ON A CARD THAT NOBODY HAS CHECKED. A note strip is the card saying something about
   the region above it; a FLAGGED one is the card saying nobody has verified what it just said, and
   the two cannot look alike or the reader has to read every footer to find the one that matters.
   The wash and the edge are the product's one emphasis, which already means "read this before
   continuing", and the label beside it names what is unreviewed rather than shouting. */
.sv-card__foot--flagged {
  background: var(--sv-accent-wash);
  border-left: 2px solid var(--sv-accent-fill);
}

.sv-card__foot--flagged > .sv-label {
  color: var(--sv-accent);
}

/* The label naming it stays on one line and the sentence beside it takes the rest. */
.sv-card__foot > .sv-label {
  white-space: nowrap;
}

/* A BUTTON IN HERE IS THE LABEL'S CASE, NOT THE SENTENCE'S. It names an action, so it keeps the
   width that name needs and the prose beside it takes what is left. A flex item's own
   `min-width: auto` let the paragraph squeeze "Add notes" onto two lines the moment that sentence
   grew by a clause, and a two-line button in a one-line row reads as a rendering fault rather than
   as a control. */
.sv-card__foot > .sv-btn {
  flex: none;
}

/* THE SENTENCE IS ALWAYS A PARAGRAPH IN HERE, never the footer's own text: the footer draws the
   fill and the rule and must run the width of the card, and a measure capped on it would cap those
   too. Prose is capped, as it is everywhere else in the product - a sentence set across a thousand
   pixels loses the start of its next line. */
.sv-card__foot > p {
  margin: 0;
  max-width: 96ch;
  font: inherit;
  color: inherit;
}

/* A TABLE IN A FLUSH CARD GIVES UP ITS OWN BOX. `DataTable`'s wrapper draws a border and a radius,
   which is right on a table standing alone in a well and is a second box inside a card that already
   carries both - the card's border is then the table's. The same override the print surface writes
   for paper, and for the same reason. */
.sv-card--flush .sv-table-wrap {
  border: 0;
  border-radius: 0;
}

.sv-divider {
  height: 1px;
  background: var(--sv-border-subtle);
  margin: var(--sv-space-4) 0;
}

/* A rule BETWEEN two groups on one line rather than between two blocks. It overrides the base's
   height and margin, so it is written as `sv-divider sv-divider--vertical`.

   The side margins are what make it work: they widen the boundary past the gap of the row it sits
   in, so the groups either side of it read as groups. A hairline with the row's own gap around it
   separates nothing. `flex: none` because the top bar has no wrap protection and a 1px item is the
   first thing a cramped flex row would consume. */
.sv-divider--vertical {
  width: 1px;
  height: 20px;
  margin: 0 var(--sv-space-2);
  flex: none;
}

/* ── Layout primitives ────────────────────────────────────────────────────── */

.sv-stack   { display: flex; flex-direction: column; gap: var(--sv-space-3); }
.sv-stack-4 { display: flex; flex-direction: column; gap: var(--sv-space-4); }
/* THE STEP BETWEEN ONE BLOCK OF A SCREEN AND THE NEXT, which about fifty `margin-top` attributes
   were each stating for themselves. A block that renders conditionally takes its gap with it,
   which a sibling holding a margin cannot. */
.sv-stack-5 { display: flex; flex-direction: column; gap: var(--sv-space-5); }
.sv-stack-6 { display: flex; flex-direction: column; gap: var(--sv-space-6); }

/* A CONTAINER THAT STATES A GAP OWNS EVERY GAP INSIDE IT, and everything in the product that
   carries a margin of its own gives it up inside one: a paragraph, a rule, a card's head, and the
   two standing statements that separate themselves from what follows them.

   A margin does not collapse into a flex gap, it lands on top of it - so a two-paragraph alert in a
   stack stands twice as far from the block below it as a one-paragraph alert does, and only one of
   those two can be the spacing anybody chose. */
:is(.sv-stack, .sv-stack-4, .sv-stack-5, .sv-stack-6, .sv-row, .sv-alert)
  > :is(p, hr, ul, ol, dl, .sv-card__header, .sv-label--section, .sv-alert, .sv-notice) {
  margin: 0;
}
.sv-row     { display: flex; align-items: center; gap: var(--sv-space-3); }
.sv-row--between { display: flex; align-items: center; justify-content: space-between; gap: var(--sv-space-3); }
.sv-row--wrap    { flex-wrap: wrap; }
/* Labelled fields side by side. Centred, they hang off each other by half the difference in their
   heights, so one hint under one field lifts its input above its neighbour's and the row reads as a
   mistake; a button beside them floats level with the labels rather than with the boxes it acts on. */
.sv-row--bottom  { align-items: flex-end; }
/* A CONTROL STANDING IN A FIELD'S ROW IS THE FIELD'S HEIGHT. A button states its own height, and a
   text box's is whatever its line box and padding come to, so Search sat four pixels shorter than
   the box it searches. Stretching the row takes the number from the field rather than restating it
   here, which is what keeps the two from drifting the next time either is touched. */
.sv-row--fields  { align-items: stretch; }
.sv-grow    { flex: 1 1 auto; min-width: 0; }

.sv-grid {
  display: grid;
  gap: var(--sv-space-4);
}

/* A ROW OF MEASUREMENTS IS A BLOCK OF THE PAGE and stands clear of what is above it and of the
   list it heads. Nine screens draw one; six stated the step by hand and three of those at only one
   end. Adjacent margins collapse, so a row under a page header keeps the larger of the two rather
   than the sum. */
.sv-grid--kpi {
  grid-template-columns: var(--sv-track-kpi);
  margin: var(--sv-space-5) 0;
}

.sv-grid--cards {
  grid-template-columns: var(--sv-track-cards);
}

/* ── The same row of figures, flattened into a card ────────────────────────
   A BORDER PER TILE IS FIVE BORDERS WHERE A READER SEES ONE. Inside a card that already draws a box
   and a radius, separately bordered tiles are boxes in a box; joined, the figures read as the body
   of the heading directly above them and one hairline tells one cell from the next.

   Two components draw it - `SupplyStrip`'s figures under a site readout, and the pipeline strip on
   Projects - so it is here rather than in either of their files. What stays theirs is how a CELL is
   marked: `supply-strip.css` owns its four markings and this file declares none of them.
   ------------------------------------------------------------------------ */
.sv-grid--kpi-flush {
  display: grid;
  grid-template-columns: var(--sv-track-kpi);
  gap: 0;
  margin: 0;
}

.sv-grid--kpi-flush > .sv-kpi {
  border: 0;
  border-right: 1px solid var(--sv-border-band);
  border-radius: 0;
  gap: 3px;
  padding: 10px var(--sv-space-4) 11px;
}

.sv-grid--kpi-flush > .sv-kpi:last-child {
  border-right: 0;
}

/* THE CAPTION IS PINNED TO THE BOTTOM, so cells whose captions run to different numbers of lines
   still put their figures on one line. */
.sv-grid--kpi-flush .sv-kpi__caption {
  margin-top: auto;
  font-size: 11.5px;
  line-height: 1.35;
}

/* EXCEPT WHERE THE FIGURE HEADS A LIST, when the list is the bottom of the cell and a caption
   pinned past it lands between the figure and its own rows. It is what put one column's rows a
   line below the two beside it, which is the misalignment an eye reads before any of the words. */
.sv-grid--kpi-flush .sv-kpi:has(.sv-kpi__rows) .sv-kpi__caption {
  margin-top: 0;
}

/* A CELL HEADING A LIST IS A TALLER CELL, and it needs the room at the foot that a cell holding one
   figure does not. `min-width: 0` because a grid item defaults to its min-content: a make and model
   in a strip cell would otherwise set the column's width and leave the two beside it narrow. */
.sv-grid--kpi-flush > .sv-kpi:has(.sv-kpi__rows) {
  min-width: 0;
  padding-bottom: 15px;
}

/* A UNIT ON A 26px FIGURE IS NOT A UNIT ON A 13px ONE. `.sv-unit` is sized for a figure in a table
   cell or a card; beside a strip's headline number it disappears, and a number whose unit cannot be
   read is a number nobody can check. Still mono, still a step down from the figure, still secondary
   to it - only the size changes. */
.sv-grid--kpi-flush .sv-unit {
  font-size: 15px;
  color: var(--sv-text-secondary);
}

/* ── The lines behind a figure ────────────────────────────────────
   A NAME ON THE LEFT AND ITS VALUE ON THE RIGHT, one per line, under the figure they add up to.
   Not `.sv-facts`, which is a panel shape - a label ABOVE a 15px mono value in a 200px auto-fit
   track - and reads as four more figures rather than as the detail behind one.

   THE NAME TRUNCATES AND THE VALUE NEVER DOES. A make and model in a third of a strip has to give
   way somewhere, and a clipped quantity is the one thing on the line a reader cannot reconstruct.
   ───────────────────────────────────────────────────────────────────────── */

.sv-kpi__rows {
  display: flex;
  flex-direction: column;
  gap: 6px;

  /* RULED OFF FROM THE FIGURE ABOVE. Without the line the block reads as four more figures at a
     smaller size; with it, it reads as the detail behind the one figure heading it - which is what
     it is, and what decides whether an eye stops at the headline or reads on. */
  margin: 12px 0 0;
  padding-top: 11px;
  border-top: 1px solid var(--sv-border-subtle);
}

.sv-kpi__rows > div {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sv-space-4);
  min-width: 0;
}

/* THE NAME RECEDES AND THE VALUE CARRIES THE INK, which is the way round the figure above them is
   set: the label is quiet and the number is what is read. Reversed, a column of names was the
   loudest thing in a strip whose whole point is the quantities beside them. */
.sv-kpi__rows dt {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font: 400 12px/1.45 var(--sv-font-sans);
  color: var(--sv-text-tertiary);
}

.sv-kpi__rows dd {
  margin: 0;
  white-space: nowrap;
  font: 400 12px/1.45 var(--sv-font-sans);
  font-variant-numeric: tabular-nums;
  color: var(--sv-text);
}

/* ── A row whose value is a VERDICT rather than a quantity ────────────────
   THREE TONES AND A DOT, and it is the product's own family of three: a pass cleared, amber is
   "read this before continuing", and the danger tone is "this is stopped". A screen picks between
   them and invents no fourth.

   THE DOT IS WHAT MAKES IT SCANNABLE. "current" and "earlier run" are the same length and the same
   shape at a glance, and a reader checking three sources is checking a column rather than reading
   it. It takes `currentColor`, so the tone is stated once per verdict and the mark follows it.

   A QUANTITY TAKES NONE OF THIS. A row reading 144 is not a verdict, and a dot on every line marks
   nothing.
   ───────────────────────────────────────────────────────────────────────── */

.sv-kpi__rows dd.sv-kpi__row--pass,
.sv-kpi__rows dd.sv-kpi__row--warn,
.sv-kpi__rows dd.sv-kpi__row--severe {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-variant-numeric: normal;
}

.sv-kpi__rows dd.sv-kpi__row--pass::before,
.sv-kpi__rows dd.sv-kpi__row--warn::before,
.sv-kpi__rows dd.sv-kpi__row--severe::before {
  content: "";
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}

.sv-kpi__rows dd.sv-kpi__row--pass {
  color: var(--sv-pass);
}

.sv-kpi__rows dd.sv-kpi__row--warn {
  color: var(--sv-accent);
}

.sv-kpi__rows dd.sv-kpi__row--severe {
  color: var(--sv-danger-ink);
}

/* ── How a cell in the strip is marked ─────────────────────────────────────
   THREE STATES AND A FACE, and a caller picks between them rather than inventing a fourth. The dot
   is for a cell whose figure is a GRADE - a count of findings, a severity step, a queue somebody is
   waiting in - because a nought and a one are the same shape at a glance and those are the cells
   nobody can pick out. A cell counting something nobody grades carries none: a dot on every cell
   marks nothing.
   ────────────────────────────────────────────────────────────────────────── */

.sv-kpi--dot .sv-label::before,
.sv-kpi--attention .sv-label::before,
.sv-kpi--severe .sv-label::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-right: 6px;
  border-radius: 50%;
  background: var(--sv-border-strong);
  vertical-align: middle;
}

/* THE ICON MARKS THE ROW, SO THE DOT STOPS. Both were drawing on the same line and a 6px disc in
   front of a glyph is not read as a grade - it is read as a bullet, which is what the label had
   grown. The dot earned its place when it was the only mark on the row; it is not, now.

   NOTHING IS LOST WITH IT. The dot never said WHICH grade - the wash across the cell and the tinted
   figure did, and both are untouched. What the dot did was pick the cell out of four, and an amber
   glyph does that better than a hairline disc ever did. A tile with no icon keeps its dot. */
.sv-kpi__label:has(.sv-kpi__icon)::before {
  content: none;
}

/* SEVERE IS THE ONE STATE THE ACCENT YIELDS TO. Amber inside a red wash reads as two verdicts on
   one cell, and this is the distinction the danger token exists to hold: one refuses a filing, the
   other is worth reading. The readout restates it because the paper red is a smudge on that navy. */
.sv-kpi--severe .sv-kpi__icon {
  color: var(--sv-danger);
}

.sv-card--readout .sv-kpi--severe .sv-kpi__icon {
  color: var(--sv-readout-severe);
}

/* WHAT NEEDS READING IS TINTED, AND ONLY WHILE THERE IS SOMETHING TO READ. Amber is the product's
   one emphasis and it already means "read this before continuing"; the tint carries it across the
   whole cell so the eye finds the cell rather than the digit. A nought is drawn plain - the figure
   is still shown, because a thing that does not apply reports zero and says so, and a nought tinted
   amber would say the opposite of what it counts. */
.sv-grid--kpi-flush > .sv-kpi--attention {
  background: var(--sv-accent-wash);
}

.sv-kpi--attention .sv-label::before {
  background: var(--sv-accent-fill);
}

.sv-grid--kpi-flush > .sv-kpi--attention .sv-kpi__value {
  color: var(--sv-accent);
}

/* SEVERE IS NOT A LOUDER ATTENTION. One refuses a filing and the other is worth reading before
   quoting a timeline, so the two cannot be the same amber: this is what the danger token exists for,
   and it is used for that and nothing else. A WASH rather than a fill, mixed from the token so it
   follows the theme - a red panel reads as the page having failed rather than the design. */
.sv-grid--kpi-flush > .sv-kpi--severe {
  background: var(--sv-danger-wash);
}

.sv-kpi--severe .sv-label::before {
  background: var(--sv-danger);
}

.sv-grid--kpi-flush > .sv-kpi--severe .sv-kpi__value {
  color: var(--sv-danger-ink);
}

/* A DATE IS NOT A COUNT AND IS NOT SET LIKE ONE. Ten characters at the figure's own 26px is the
   widest thing on the row by a factor of three, and it is the one figure nobody is comparing. */
.sv-grid--kpi-flush > .sv-kpi--date .sv-kpi__value {
  font-size: 17px;
  line-height: 1.76;
}

/* A ROW OF FORM FIELDS THAT WRAPS. Two named widths and no more: the wide one is a labelled text
   input with a hint under it, and the tight one is a numeric field with a unit, which needs less.
   Written inline nine times across five screens, they were four different numbers away from being
   four different layouts. Both floor at `min(100%, N)`, because both are drawn inside a card. */
/* TWELVE FIELDS THAT WRAP TO WHATEVER THE PANEL HOLDS. A month's box takes a five-digit figure and
   a three-letter eyebrow and nothing else, so it floors far tighter than a labelled text field -
   Consumption draws two of these and Conditions one, and written per screen they were three numbers
   away from being three layouts. */
.sv-grid--months {
  grid-template-columns: var(--sv-track-months);
}

.sv-grid--fields {
  grid-template-columns: var(--sv-track-fields);
}

.sv-grid--fields-tight {
  grid-template-columns: var(--sv-track-fields-tight);
}

/* THE CONTROL THAT COMMITS A FIELD GRID IS ITS LAST CELL, AT THE END OF THE ROW, AND IT IS THE
   FIELD'S HEIGHT. A button stands 34px by its own rule and a text box comes to 38px from its
   padding around its line, so a button set beside a labelled box by whatever row it happens to be
   in stands four pixels shorter than the box it acts on and reads as a control from another
   product. Here it takes the box's own padding around its own line - `.sv-checkbox--boxed`'s
   construction rather than a number somebody measured - and the grid places it: the last column
   (`grid-column-end: -1` counts from the end of the explicit grid, which is what `auto-fit` lays
   down), at that column's end, level with the boxes. Where the fields wrap, the button follows them
   onto the next row and keeps to its end, because a commit is read at the end of what it commits;
   and `auto-fit` collapses the columns nothing lands in, so two fields and a button share a row
   three ways rather than leaving a hole between them. The sharing dialog on the project overview
   is the shape. */
:is(.sv-grid--fields, .sv-grid--fields-tight) > .sv-btn {
  grid-column-end: -1;
  justify-self: end;
  align-self: end;
  padding-top: 9px;
  padding-bottom: 9px;
  line-height: 1.4;
}

/* The month grid is `--sv-track-months` above and has no class yet: the two screens that draw one
   still re-derive their own, and a `.sv-grid--months` declared ahead of them is a rule read as live
   by everybody searching this file and worn by nothing. */

/* THE MEASURE EVERY PAGE IS SET ON, and the band of air a section stands in.
   Public and application alike: a page that centres its own content at its own
   width is a page that lines up with nothing beside it. */
.sv-container {
  width: 100%;
  max-width: 1120px;
  margin: 0 auto;
  padding: 0 var(--sv-space-5);
}

.sv-container--narrow { max-width: 760px; }

.sv-section {
  padding: var(--sv-space-7) 0;
}

.sv-visually-hidden {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* ── The well a page sits in ──────────────────────────────────────────────────
   THE LAST ROW MUST NOT END UNDER THE FEEDBACK LAUNCHER. The launcher is fixed
   to the bottom-right corner and the well scrolls beneath it, so on a list that
   fills the page the final row's actions sit under a 34px button. The designer
   solves the same corner by stepping its map controls up 44px; this is the same
   clearance from the other side, because a page well cannot move a fixed
   element.

   NOT THE APPLICATION SHELL'S, even though the shell is what usually draws it -
   the error page, the not-found page and the access-denied notice are all a well
   with a page in it and none of them is inside that shell.
   ------------------------------------------------------------------------ */
.sv-content {
  /* THE GUTTER IS A CUSTOM PROPERTY ON THE WELL, not the number typed here, because the
     page header's band breaks OUT of this padding to reach the edges of the main column
     and has to pull by exactly what the well pushed. Redefined in the narrow rule below,
     where it cascades to the band with nothing having to remember to follow it. */
  padding: var(--sv-space-5) var(--sv-well-gutter);
  padding-bottom: calc(var(--sv-space-5) + 44px);
  flex: 1 1 auto;
  min-width: 0;
}

/* A WORKSPACE OWNS THE WHOLE WELL: no padding, no page scroll, and an exact
   height for the surface inside it to be measured against. Leaflet renders 0px
   tall in a box with no definite height, and 100vh minus a guessed bar height is
   what leaves the designer with a scrollbar it never uses. */
.sv-content--flush {
  padding: 0;
  height: 100vh;
  overflow: hidden;
}

@media (max-width: 1000px) {
  .sv-content {
    --sv-well-gutter: var(--sv-space-4);
    padding: var(--sv-space-4);
  }

  /* A workspace has no room to overlay panels on a screen this narrow, and the
     designer is deliberately not built for one. It scrolls like a page. */
  .sv-content--flush {
    height: auto;
    overflow: visible;
  }
}

/* ── Tabs, segments and explainers ────────────────────────────────────────
   THREE THINGS THAT SAT ON ONE PAGE LOOKING IDENTICAL. A subject page stacks a
   row that navigates, a row that acts and a row that filters, and all three were
   written as `sv-btn sv-btn--sm` with the chosen one amber-filled - so
   /admin/utilities showed two amber-filled pill rows and nothing said which was
   the page's own navigation. A reader cannot learn a hierarchy that is not
   drawn.

   ONE TAB STRIP PER PAGE, and it is the only underlined thing on it. Anything
   narrowing what is already on screen is a segmented control; anything that
   writes stays a button. Nesting a second strip inside a tab body puts two
   underlines on one screen and the rule is gone again.
   ------------------------------------------------------------------------ */

/* THE STRIP OWNS ITS OWN SPACING. Written inline it was `var(--sv-space-5) 0`
   on two pages and `margin-bottom` alone on the third, so one strip sat a space
   higher on the library than on the console beside it. */
.sv-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sv-space-4);
  margin: var(--sv-space-5) 0;
  border-bottom: 1px solid var(--sv-border);
}

/* The underline is the selection, so the resting tab carries a transparent one
   of the same weight - a border that appears on selection moves the label a
   pixel and the whole strip twitches. */
/* THE FACE IS WORN BY A BUTTON OR BY A LINK, and the only difference is what
   the browser draws for free. A statically rendered page navigates rather than
   handles, so `TabStrip` renders anchors there - and an anchor arrives with the
   document's own underline, which on a strip whose selection IS an underline
   reads as every tab being selected. */
.sv-tabs__tab {
  padding: var(--sv-space-2) 2px;
  border: 0;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  background: transparent;
  color: var(--sv-text-secondary);
  font: 400 13px/1.35 var(--sv-font-sans);
  text-decoration: none;
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease;
}

.sv-tabs__tab:hover {
  color: var(--sv-text);
  border-bottom-color: var(--sv-border-strong);
}

/* Driven off aria-current, not a second class: the attribute is what a screen
   reader announces, so a tab that looks selected and does not say so is not
   expressible. */
.sv-tabs__tab[aria-current="page"] {
  color: var(--sv-text);
  border-bottom-color: var(--sv-accent-fill);
  font-weight: 500;
}

/* One control holding one choice of several, which is what a filter is. The
   shared border is the whole point - three separate bordered pills read as
   three buttons that happen to sit together. */
/* A BLOCK THAT HUGS ITS CONTENT, never inline-flex. Inline, it flows after
   whatever precedes it - on the authoring form that is the BackLink, and the
   control landed on the same line and clipped it. A filter row is its own row
   wherever it is used, and fit-content is what keeps it from stretching to the
   full width once it stops being inline. */
.sv-segmented {
  display: flex;
  width: fit-content;
  max-width: 100%;
  flex-wrap: wrap;
  margin-bottom: var(--sv-space-4);
  border: 1px solid var(--sv-border-strong);
  border-radius: var(--sv-radius-control);
  overflow: hidden;
}

/* THE FACE IS WORN BY A BUTTON OR BY A LINK, for the reason `.sv-tabs__tab` is: a
   statically rendered page navigates rather than handles, so `SegmentedFilter` draws
   anchors there and the document's own underline has to go. */
.sv-segmented__option {
  padding: 6px 12px;
  border: 0;
  border-left: 1px solid var(--sv-border);
  background: transparent;
  color: var(--sv-text-secondary);
  font: 400 12px/1.35 var(--sv-font-sans);
  text-decoration: none;
  cursor: pointer;
  transition: background-color 120ms ease, color 120ms ease;
}

.sv-segmented__option:first-child {
  border-left: 0;
}

.sv-segmented__option:hover {
  background: var(--sv-bg-overlay);
  color: var(--sv-text);
}

/* The wash rather than the fill. A segmented control is a filter, and an amber
   block here would carry the same weight as the tab strip above it - which is
   the flattening this section exists to undo. */
.sv-segmented__option[aria-pressed="true"] {
  background: var(--sv-accent-wash);
  color: var(--sv-text);
  font-weight: 500;
}

.sv-segmented__option:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* WHAT A SCREEN IS, FOR SOMEBODY WHO DOES NOT KNOW YET - and out of the way of
   everybody else. An `sv-notice` is for a standing condition or a live count;
   an explanation of the screen goes here, shut, because a paragraph pinned
   above the table is read once and skipped every day after that.

   `details` rather than MudExpansionPanel: this is presentation state and it
   toggles in the browser, with no circuit round trip and nothing to dispose. */
.sv-explainer {
  margin: var(--sv-space-4) 0;
  border: 1px solid var(--sv-border);
  border-radius: var(--sv-radius-card);
  background: var(--sv-bg-overlay);
}

.sv-explainer > summary {
  padding: var(--sv-space-3) var(--sv-space-4);
  cursor: pointer;
  color: var(--sv-text-secondary);
  font: 500 10px/1.4 var(--sv-font-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  list-style: none;
}

.sv-explainer > summary::-webkit-details-marker {
  display: none;
}

/* The marker is drawn rather than inherited, because the platform triangle is
   the one glyph on the page nothing else in the system draws. */
.sv-explainer > summary::before {
  content: "+";
  display: inline-block;
  width: 1em;
  color: var(--sv-accent);
  font-family: var(--sv-font-mono);
}

.sv-explainer[open] > summary::before {
  content: "-";
}

.sv-explainer > summary:hover {
  color: var(--sv-text);
}

.sv-explainer__body {
  padding: 0 var(--sv-space-4) var(--sv-space-4);
}

/* Capped at a readable measure. These cards run the width of the page and a
   90-character line is the other way prose stops being read. */
.sv-explainer__body p {
  margin: 0 0 var(--sv-space-2);
  max-width: 90ch;
  color: var(--sv-text-secondary);
  font: 400 13px/1.55 var(--sv-font-sans);
}

.sv-explainer__body p:last-child {
  margin-bottom: 0;
}

/* ── Buttons ──────────────────────────────────────────────────────────────
   The SSR authentication and marketing pages render without MudBlazor, so they
   need real controls. Inside the interactive app, prefer MudBlazor with
   Color.Primary - it resolves to the same accent through ThemeService.
   ------------------------------------------------------------------------ */

/* A CONTROL'S HEIGHT IS THE CONTROL'S, NEVER ITS CONTENTS'. Vertical padding alone means every
   button is as tall as whatever happens to be inside it, so a Share button carrying a count pill
   stood 42px beside a 33px primary action - two controls in one row, the same weight, visibly
   different sizes. The height is stated once here; the padding is what is left over, enough that a
   label long enough to wrap still clears the border. Both size modifiers restate the pair. */
.sv-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sv-space-2);
  min-height: 34px;
  padding: 4px 16px;
  border-radius: var(--sv-radius-control);
  border: 1px solid var(--sv-border-strong);
  background: transparent;
  color: var(--sv-text);
  font: 500 13px/1 var(--sv-font-sans);
  cursor: pointer;
  transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
}

.sv-btn:hover {
  background: var(--sv-bg-overlay);
  text-decoration: none;
  color: var(--sv-text);
}

.sv-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* --sv-accent-fill, not --sv-accent: a button is a shape, and on paper the text
   accent darkens to an ochre that reads as a disabled control. */
.sv-btn--primary {
  background: var(--sv-accent-fill);
  border-color: var(--sv-accent-fill);
  color: var(--sv-on-accent);
}

.sv-btn--primary:hover {
  background: var(--sv-accent-fill-hover);
  border-color: var(--sv-accent-fill-hover);
  color: var(--sv-on-accent);
}

.sv-btn--lg {
  min-height: 42px;
  padding: 6px 22px;
  font-size: 14px;
}

.sv-btn--ghost {
  border-color: transparent;
  color: var(--sv-text-secondary);
}

/* A DESTRUCTIVE CONTROL IS THE ONE EXCEPTION TO "ONE ACCENT".
   The rule that amber is the only chromatic colour in the UI exists so nothing competes with the
   thing being emphasised, and it holds for every control that DOES something. It cannot hold for the
   control that UNDOES something: Delete sitting in the same outline as Clone and Archive is a row of
   identical buttons where one of them is not like the others, and the reader finds out which by
   pressing it. --sv-danger is already in the token file, where validation text reads it, so
   this introduces no new colour - it puts an existing one on the control that needed it.

   Outlined rather than filled, deliberately. A solid red button is heavier than a primary action and
   would make Delete the loudest thing on a page whose actual purpose is elsewhere; it fills on hover,
   at the point somebody has already aimed at it. */
.sv-btn--danger {
  border-color: color-mix(in srgb, var(--sv-danger) 55%, transparent);
  color: var(--sv-danger-ink);
}

/* A DESTRUCTIVE CONTROL THAT IS NOT ASKING TO BE PRESSED. Ghost is the borderless button and danger
   is the destructive ink, and the two compose - but --danger is declared after --ghost, so it wins
   the border back and the pair renders as the outlined one. Named together, the composite says what
   it is: on a card whose one filled control is the action somebody came for, a red outline beside it
   is a second thing competing for the same glance. The border arrives on hover, which is where a
   destructive control should start being emphatic. */
.sv-btn--ghost.sv-btn--danger {
  border-color: transparent;
  color: var(--sv-danger-ink);
}

.sv-btn--ghost.sv-btn--danger:hover {
  border-color: color-mix(in srgb, var(--sv-danger) 30%, transparent);
  background: var(--sv-danger-wash);
}

/* --sv-on-danger, not a literal white. The two modes need OPPOSITE ink: on paper --sv-danger is the
   deep #C62828 and white clears AA comfortably, but in dark mode it is the lighter #E5484D, where
   white measures about 3.3:1 and fails - so hovering Delete made its own label harder to read at the
   exact moment somebody was confirming their aim. The token is declared beside the danger colour in
   both theme blocks, the same way --sv-on-accent pairs with the accent. */
.sv-btn--danger:hover {
  background: var(--sv-danger);
  border-color: var(--sv-danger);
  color: var(--sv-on-danger);
}

.sv-btn--danger:disabled:hover {
  background: transparent;
  border-color: color-mix(in srgb, var(--sv-danger) 55%, transparent);
  color: var(--sv-danger-ink);
}

.sv-btn--block { width: 100%; }
.sv-btn--sm    { min-height: 28px; padding: 3px 11px; font-size: 12px; }

/* A GLYPH IS STILL A BUTTON AND WEARS THE SAME BOX. An action row carrying a bordered Share, a
   bare floating glyph and a bordered overflow trigger reads as three controls of three different
   weights, and the one in the middle reads as decoration rather than something to press. The
   square is stated once here, at the same height as the words beside it, so a control with no
   label is the same shape as one with a label.

   .sv-icon-btn is NOT this and stays as it is: it is the borderless glyph a TABLE ROW uses, where
   there is no bordered control in the row for it to disagree with. The two are told apart by
   whether the thing next to them has a border. */
.sv-btn--icon {
  flex: none;
  width: 34px;
  height: 34px;
  padding: 0;
}

.sv-btn--sm.sv-btn--icon {
  width: 28px;
  height: 28px;
}

/* A COUNT ON A CONTROL RIDES IN THE MIDDLE OF IT. .sv-badge sets align-self: flex-start so a badge
   in a flex COLUMN keeps its own width; in a button - a flex row - that same declaration pins the
   pill to the top edge and leaves it hanging off the label beside it. */
.sv-btn > .sv-badge {
  align-self: center;
}

/* ── A two-state switch ───────────────────────────────────────────────────────
   One decision with two answers, both worth reading - monthly against annual on
   the pricing page, and the same choice on the billing screen inside the
   application. Not a SegmentedFilter, which narrows a list already on screen,
   and not a checkbox, which has an off state rather than a second one.
   ------------------------------------------------------------------------ */
.sv-toggle {
  display: inline-flex;
  padding: 3px;
  border-radius: var(--sv-radius-pill);
  border: 1px solid var(--sv-border);
  background: var(--sv-bg-overlay);
  gap: 2px;
}

.sv-toggle button {
  border: 0;
  background: transparent;
  color: var(--sv-text-secondary);
  padding: 6px 14px;
  border-radius: var(--sv-radius-pill);
  font: 500 12px/1 var(--sv-font-sans);
  cursor: pointer;
}

.sv-toggle button.active {
  background: var(--sv-accent-fill);
  color: var(--sv-on-accent);
}

/* ── Forms ────────────────────────────────────────────────────────────────── */

.sv-form {
  display: flex;
  flex-direction: column;
  gap: var(--sv-space-4);
}

.sv-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
  /* The help mark is pinned to this box. Declared here rather than in a second .sv-field rule
     beside the mark, because two declarations of one selector in one stylesheet resolve a property
     at a time and the loser is silent. */
  position: relative;
}

.sv-field__label {
  font: 500 10px/1.2 var(--sv-font-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--sv-text-tertiary);
}

.sv-field__hint {
  font: 400 12px/1.45 var(--sv-font-sans);
  color: var(--sv-text-tertiary);
}

/* A value the source did not state and something downstream requires. Lifted out of the tertiary
   grey the ordinary hints sit in, because this one is an instruction rather than a note - but it is
   text, so it takes --sv-accent and not the fill. */
.sv-field__hint--unread {
  color: var(--sv-accent);
}

/* WHAT THE FIELD MEANS, IN A PANEL OVER THE FORM.

   The mark is pinned to the field's top right rather than set beside the label: a label is one word
   on one screen and four on another, so anything measuring the label's width puts the mark somewhere
   different on every form. Pinned, it lands in the same place whatever the label says.

   The panel is `.sv-menu__panel`'s, because this is a [data-sv-menu] borrower - what is here is only
   where it opens and how wide it is. Nothing about it pushes the form: a field that grew to show a
   sentence moved the control the reader was reaching for and set the height of every other cell in
   the row. */
/* THE WRAPPER IS WHAT IS PINNED, NOT THE MARK. `.sv-menu` states `position: relative`, so a mark
   positioned inside it resolves against the wrapper rather than the field - and the wrapper is an
   ordinary item in the field's flex column, so it took a row of its own under the label and carried
   the mark down beside the control with it. Positioning the wrapper puts the mark back on the label
   line and takes it out of the flow at the same time.

   THE CHILD SELECTOR IS LOAD ORDER, NOT DECORATION. `.sv-menu` and `.sv-field__help` are one class
   each, so they carry the same specificity and the later stylesheet wins - and page-header.css is
   loaded after this one, which handed `position: relative` back and dropped the mark a line. Two
   classes settle it in this file's favour without touching the family that is not ours. */
.sv-field > .sv-field__help {
  position: absolute;
  top: 0;
  right: 0;
}

.sv-field__help-mark {
  display: inline-flex;
  align-items: center;
  border: 0;
  padding: 3px;
  /* 14px of ink in a 20px target - a pointer needs more than the glyph - and the negative margin
     keeps the larger target from pushing the mark off the label's own line. */
  margin: -3px;
  border-radius: var(--sv-radius-item);
  background: none;
  color: var(--sv-text-tertiary);
  cursor: pointer;
}

.sv-field__help-mark > svg {
  width: 14px;
  height: 14px;
}

/* aria-expanded, not the sv-menu open class. The module maintains both, but the family belongs to
   page-header.css and a screen's own rules do not grow back into the shared stylesheet - and the
   ARIA state is the semantic hook anyway. */
.sv-field__help-mark:hover,
.sv-field__help-mark[aria-expanded="true"] {
  color: var(--sv-text-secondary);
}

.sv-field__help-mark:focus-visible {
  outline: 2px solid var(--sv-accent);
  outline-offset: 1px;
}

/* BELOW THE MARK, flush with the field's right edge, and drawn as a tooltip rather than as a card.
   It is the one surface in the product that is dark in both modes for a reason the readout strip
   shares: it floats over a form, and a panel wearing the form's own fill and border is a field-
   shaped box on top of a field. Dark, it reads as a thing said ABOUT the row rather than as another
   row. `FieldHelpMark` asks page-header.js for the same direction, and the two have to agree - a
   panel the stylesheet drops and the module lifts opens one way until something clips it and the
   other way afterwards.

   A menu's panel is a list of items and sizes to them; this one holds a sentence, so it states a
   width - without one it takes the field's and sets the sentence in a 200px column.

   TWO CLASSES, FOR LOAD ORDER. `.sv-menu__panel` states its own `bottom`, and page-header.css is
   loaded after this file, so one class each leaves that edge standing beside this rule's `top` -
   and a box given both edges of an axis takes its SIZE from them. The panel came out ten pixels
   tall with the sentence hanging outside it. Both edges are stated here for that reason, and the
   module clears every edge before it pins one, so this only ever bit where nothing clips the panel
   and the stylesheet is left to place it. */
.sv-field__help > .sv-field__help-panel {
  top: calc(100% + var(--sv-space-2));
  bottom: auto;
  right: 0;
  left: auto;
  width: max-content;
  max-width: min(320px, calc(100vw - 2 * var(--sv-space-4)));
  padding: var(--sv-space-3);
  font: 400 12px/1.5 var(--sv-font-sans);
  color: var(--sv-help-panel-ink);
  background: var(--sv-help-panel);
  border: 1px solid var(--sv-help-panel-border);
  /* The scale's control radius. The product declares six radii and no seventh is invented for one
     panel; at this size 7px and 8px are the same corner. */
  border-radius: var(--sv-radius-control);
  box-shadow: var(--sv-shadow-help);
  white-space: normal;
}

/* `.sv-field-row` IS GONE AND `.sv-grid--fields*` IS WHAT REPLACED IT. It was a second field track
   beside the two named ones, and it carried the plain `minmax(200px, 1fr)` floor that the named
   pair exist to avoid: a plain minimum demands its 200px per track whatever the container has, so
   inside a card stating `overflow: hidden` the grid asked for more width than the card and the card
   clipped its own controls with nothing reporting it. A track is named once and read. */

.sv-input,
.sv-select,
.sv-textarea {
  width: 100%;
  padding: 9px 11px;
  background-color: var(--sv-input-fill);
  border: 1px solid var(--sv-border-input);
  border-radius: var(--sv-radius-control);
  color: var(--sv-text);
  font: 400 13px/1.4 var(--sv-font-sans);
}

/* ── A choice out of a short list ─────────────────────────────────────────
   THE PLATFORM'S OWN CHROME IS NOT THIS PRODUCT'S. A native select paints its own caret, in its own
   box, at its own size, in a tone no token reaches and a shade that changes with the operating
   system - so a row of them beside the product's own inputs is a second control vocabulary on one
   form, and it is the vocabulary nobody chose. The box is the field's; what is drawn here is the
   mark that says the box opens.

   THE CARET IS TWO GRADIENTS RATHER THAN AN IMAGE, because an inline SVG cannot read a custom
   property: a data URI fixes its own colour, so it would need a second copy for the dark mode and
   the two would drift. Two triangles a background-size apart form the one arrow, and both take the
   token, so the mark follows the theme with nothing to keep in step.

   `background-color` ABOVE, NEVER THE SHORTHAND. A later `background:` anywhere in the product
   silently throws this image away, and a select with no caret reads as a text box that refuses
   typing. The card-body raise is written the same way for the same reason.
   ────────────────────────────────────────────────────────────────────────── */
.sv-select {
  appearance: none;
  padding-right: 30px;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--sv-text-tertiary) 50%),
    linear-gradient(135deg, var(--sv-text-tertiary) 50%, transparent 50%);
  background-position: right 16px center, right 11px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat, no-repeat;
  cursor: pointer;
}

/* A SELECT IN A TABLE ROW IS THE ROW'S SMALL SIZE, which is `.sv-btn--sm`'s. The two stand in one
   row - a link's run picker beside its Revoke - and a 38px box beside a 28px button sets the row's
   height from the box and leaves the button floating in it. Same height, same face; the caret
   comes in with the padding. A screen's stylesheet sets neither the font nor the padding of a
   control, so this is the modifier it composes. */
.sv-select--sm {
  min-height: 28px;
  padding: 3px 26px 3px 9px;
  font-size: 12px;
  background-position: right 13px center, right 8px center;
}

/* A NATIVE DATE OR TIME BOX CARRIES A SECOND SET OF CHROME the same way a select does - a spinner,
   a picker glyph and a clear button, all drawn by the browser. The value inside is a measurement,
   so it is mono like every other measured value in the product, and the picker's own button is
   given the tertiary tone rather than the platform's blue. */
.sv-input[type='date'],
.sv-input[type='time'],
.sv-input[type='datetime-local'] {
  font-family: var(--sv-font-mono);
  font-variant-numeric: tabular-nums;
}

.sv-input[type='date']::-webkit-calendar-picker-indicator,
.sv-input[type='time']::-webkit-calendar-picker-indicator,
.sv-input[type='datetime-local']::-webkit-calendar-picker-indicator {
  opacity: 0.45;
  cursor: pointer;
}

.sv-input[type='date']::-webkit-calendar-picker-indicator:hover,
.sv-input[type='time']::-webkit-calendar-picker-indicator:hover,
.sv-input[type='datetime-local']::-webkit-calendar-picker-indicator:hover {
  opacity: 0.9;
}

/* Values that are measurements are typed and read in mono, like everywhere else. */
.sv-input--mono {
  font-family: var(--sv-font-mono);
  font-variant-numeric: tabular-nums;
}

.sv-textarea {
  min-height: 92px;
  resize: vertical;
}

/* ── A value dragged along a track ───────────────────────────────────
   THE ONE RANGE IN THE PRODUCT AND IT IS NATIVE. A Material slider brought its own type scale, its
   own track, its own thumb and a culture parameter it had to be told about; the browser's own needs
   none of that, is keyboard correct without help, and on a touch screen is the platform's.

   THE TRACK IS THE FIELD'S SUNKEN FILL AND THE THUMB IS THE ACCENT, so a range in a row of fields
   reads as one of them. Both engines are named because neither takes the other's pseudo-element and
   a rule naming both in one selector is discarded whole by both.
   ───────────────────────────────────────────────────────────────────────── */
.sv-range {
  width: 100%;
  height: 22px;
  padding: 0;
  background: none;
  appearance: none;
  cursor: pointer;
}

.sv-range::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: var(--sv-radius-pill);
  background: var(--sv-bg-sunken);
  border: 1px solid var(--sv-border-input);
}

.sv-range::-moz-range-track {
  height: 4px;
  border-radius: var(--sv-radius-pill);
  background: var(--sv-bg-sunken);
  border: 1px solid var(--sv-border-input);
}

.sv-range::-webkit-slider-thumb {
  appearance: none;
  width: 13px;
  height: 13px;
  margin-top: -5px;
  border: 0;
  border-radius: 50%;
  background: var(--sv-accent-fill);
}

.sv-range::-moz-range-thumb {
  width: 13px;
  height: 13px;
  border: 0;
  border-radius: 50%;
  background: var(--sv-accent-fill);
}

.sv-range:focus-visible {
  outline: 2px solid var(--sv-accent);
  outline-offset: 2px;
}

/* ── Figures reached from inputs that have since moved ──────────────
   WORSE THAN NOT HAVING BEEN PRODUCED AT ALL, which is why it is marked rather than merely absent:
   the numbers are on the screen, they look current, and they are not. Two screens report it - a run
   whose design has moved and a financial model whose run has - so the shape is here and each screen
   says only what moved.

   THE WAY OUT TRAVELS WITH THE STATEMENT. A reader told the figures are stale and left to find the
   button has been told half of something.
   ───────────────────────────────────────────────────────────────────────── */
.sv-result__stale-banner {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sv-space-2) var(--sv-space-3);
  padding: 11px var(--sv-space-4);
  background: var(--sv-accent-wash);
  border-left: 2px solid var(--sv-accent-fill);
  border-radius: var(--sv-radius-control);
}

.sv-result__stale-banner > p {
  flex: 1 1 260px;
  margin: 0;
  font: 400 12px/1.5 var(--sv-font-sans);
  color: var(--sv-text-secondary);
}

.sv-input:focus,
.sv-select:focus,
.sv-textarea:focus {
  border-color: var(--sv-accent);
  outline: none;
}

.sv-input::placeholder,
.sv-textarea::placeholder {
  color: var(--sv-text-faint);
}

.sv-input[readonly] {
  color: var(--sv-text-secondary);
  background: var(--sv-bg-overlay);
}

/* ── A secret, masked without type="password" ─────────────────────────────────
   Dots without the browser credential manager. A password input makes Chrome offer to save the value
   and then offer to fill it back into the next sensitive field on the page, which on a settings screen
   means one integration's key autofilled into another's. Letter-spacing because the disc glyph is
   narrower than the mono digits around it and an unspaced run of them reads as a single blob. */
.sv-input--secret {
  -webkit-text-security: disc;
  letter-spacing: 0.12em;
}

/* THE FALLBACK IS NOT OPTIONAL, because -webkit-text-security is not a standard property. Where it is
   unimplemented - an older Firefox, an embedded webview, a hardened corporate browser - the rule above
   is simply dropped and a passphrase renders as plain readable text with nothing to say the masking
   failed. type="password" was the browser-native guarantee against exactly that, and it was given up
   to keep the credential manager out; this is what pays for it.

   Transparent text with a matching caret: the value stays selectable, copyable and editable, and is
   unreadable on screen and over a shoulder. It is a weaker mask than discs (length is still visible
   from the caret) and it is only ever reached on browsers that would otherwise show everything.
   The reveal toggle beside the field is what a person uses to check what they pasted. */
@supports not (-webkit-text-security: disc) {
  .sv-input--secret {
    color: transparent;
    text-shadow: none;
    caret-color: var(--sv-text);
  }

  .sv-input--secret::selection {
    color: transparent;
  }
}

/* An input with a control welded to its right edge - reveal, clear, a unit. The two share one
   border so they read as one field rather than a field and a button that happen to be adjacent. */
.sv-input-group {
  display: flex;
  align-items: stretch;
  width: 100%;
}

.sv-input-group > .sv-input {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
  border-right: none;
  min-width: 0;
}

.sv-input-group__btn {
  flex: 0 0 auto;
  padding: 0 12px;
  background: var(--sv-bg-overlay);
  border: 1px solid var(--sv-border);
  border-top-right-radius: var(--sv-radius-control);
  border-bottom-right-radius: var(--sv-radius-control);
  color: var(--sv-text-secondary);
  font: 500 11px/1 var(--sv-font-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background-color 120ms ease, color 120ms ease;
}

.sv-input-group__btn:hover {
  background: var(--sv-bg-raised);
  color: var(--sv-text);
}

/* ── The way back ─────────────────────────────────────────────────────────────
   Set as a label rather than as a link: it sits directly above an h1 and a
   blue-ish underlined row there would compete with the title for the first
   fixation. The affordance is the arrow and the hover, which is the same trade
   the getting-started checklist's step links make.
   ─────────────────────────────────────────────────────────────────────────── */
.sv-back {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-bottom: var(--sv-space-2);
  font: 500 10px/1.2 var(--sv-font-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--sv-text-tertiary);
  text-decoration: none;
  transition: color 120ms ease;
}

.sv-back:hover,
.sv-back:focus-visible {
  color: var(--sv-accent);
}

/* The in-page variant: a step back that navigates nothing is a button, and a button carries a
   browser's own chrome until it is told not to. Everything else about it is the rule above. */
button.sv-back {
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.sv-back__arrow {
  /* The one moving part, and it moves the way it points. */
  transition: transform 120ms ease;
}

.sv-back:hover .sv-back__arrow {
  transform: translateX(-2px);
}

@media (prefers-reduced-motion: reduce) {
  .sv-back,
  .sv-back__arrow {
    transition: none;
  }

  .sv-back:hover .sv-back__arrow {
    transform: none;
  }
}

/* ── The project section strip ────────────────────────────────────────────────
   The way sideways across one project, sitting under the page title. The back
   link above it goes UP; this goes ACROSS, and the two are drawn differently on
   purpose: the back link is a small mono label because it must not compete with
   the h1, and this is a row of sentence-case links because a reader scans it for
   a destination by name.

   Underline for the current item rather than a filled pill: depth by border, not
   shadow, and a filled chip in the one accent would read as a call to action
   sitting where a statement of position belongs.

   It scrolls horizontally on a narrow screen instead of wrapping, so the page
   title's block keeps its height and the strip never becomes two rows a reader
   has to re-scan. That is safe here only because nothing in the row carries a
   ::after hover label - the rule that a scroll container clips one.
   ─────────────────────────────────────────────────────────────────────────── */
.sv-project-nav {
  display: flex;
  gap: var(--sv-space-4);
  align-items: stretch;
  margin-top: var(--sv-space-3);
  overflow-x: auto;
  scrollbar-width: thin;
  border-bottom: 1px solid var(--sv-border);

  /* The strip is the last child of a page header, and those headers are a flex
     row on some pages and a grid row on others. Both declarations are inert in
     the container the other one describes, which is what lets one class sit
     correctly in every project header without a per-page modifier. */
  flex: 1 0 100%;
  grid-column: 1 / -1;
}

.sv-project-nav__item {
  flex: 0 0 auto;
  padding: 0 0 var(--sv-space-2);
  font: 500 13px/1.4 var(--sv-font-sans);
  color: var(--sv-text-secondary);
  text-decoration: none;
  white-space: nowrap;

  /* Reserved on every item, not only the current one, so marking a section does
     not move the row by two pixels. */
  border-bottom: 2px solid transparent;
  transition: color 120ms ease, border-color 120ms ease;
}

.sv-project-nav__item:hover,
.sv-project-nav__item:focus-visible {
  color: var(--sv-text);
  border-bottom-color: var(--sv-border-strong);
}

.sv-project-nav__item--current {
  color: var(--sv-text);
  border-bottom-color: var(--sv-accent-fill);
}

@media (prefers-reduced-motion: reduce) {
  .sv-project-nav__item {
    transition: none;
  }
}

/* ── A search box ─────────────────────────────────────────────────────────────
   A bare text input with a placeholder does not say "search" - it was reported
   as "people might not know to press enter", and the missing thing is the
   affordance rather than the behaviour (these all commit on change, so blur
   works as well as enter does). The magnifier is what names the control.

   mask-image rather than background-image, and that is not a stylistic choice: a
   data-URI SVG cannot inherit currentColor, so a background image would bake a
   hex into the stylesheet and be the wrong grey in one of the two themes. Masked,
   the icon is painted with a token and follows the mode.
   ─────────────────────────────────────────────────────────────────────────── */
/* ── The row that narrows the list under it ───────────────────────────────
   ONE SHAPE, because it was five. `sv-row sv-row--wrap` on four screens,
   `sv-row--between` on two, a bare `sv-row` on one, an inline-styled div on
   two, and a whole `sv-card` on the audit log - so one page's filters sat on a
   raised surface and the identical row on the page beside it did not.

   BOTTOM-ALIGNED, because several of these mix a bare search box with labelled
   selects and centring stands the unlabelled control half a line proud.
   ------------------------------------------------------------------------ */
.sv-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--sv-space-3);
  margin-bottom: var(--sv-space-4);
}

/* Both sit at the far end: what the narrowing found, and the way out of it. */
.sv-filters__clear {
  margin-left: auto;
}

.sv-filters__report {
  margin-left: auto;
  font: 500 10px/1.2 var(--sv-font-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--sv-text-tertiary);
}

/* A Clear beside a report keeps its place and the report takes the end. */
.sv-filters__clear + .sv-filters__report {
  margin-left: 0;
}

/* ── THE STANDARD SHAPE OF A LIST SCREEN'S FILTER ROW ─────────────────────
   Search left, one cluster right, chips underneath. This is the layout every grid in the product
   uses, and it lives here rather than on any one screen because the second screen to adopt it is
   what turns a page's own rules into a pattern - and a copy of a pattern is a copy that drifts.

   WHAT GOES ON THE ROW is what is read as often as it is set: the search, and the order. Everything
   else goes behind `FilterMenu`, whose count and the chips below are the two things that stop a
   narrowed list reading as a list with rows missing.

   NOTHING ON THE ROW WEARS A CAPTION. Each control states its own answer - "Newest first" says what
   the box does - so a 10px word above each one is a second line of chrome on every visit saying
   nothing the control does not. The accessible name moves onto the control.

   EVERY RULE HERE IS GATED ON `:has(.sv-filters__cluster)`, so a filter row that has not adopted the
   pattern is left exactly as it was. Eleven screens draw `.sv-filters`; this changes the two that
   ask for it.
   ------------------------------------------------------------------------ */

/* THE SEARCH TAKES THE SLACK, so the box used on every visit gets the width and the cluster packs
   against the far end. Both selectors are needed: `SearchBox` renders a bare `.sv-search` when it
   carries no label and wraps it in a `.sv-field` when it does. `FilterBarGuardTests` refuses a
   `Class` at its call site, which is why this is matched by what the row contains. */
.sv-filters:has(.sv-filters__cluster) > .sv-field:has(.sv-search),
.sv-filters:has(.sv-filters__cluster) > .sv-search {
  flex: 1 1 240px;
  min-width: 0;
}

/* The control's own width is 320px and this row gives it more, so it has to be told it may fill
   what it was given rather than stopping short inside it. */
.sv-filters:has(.sv-filters__cluster) .sv-search {
  max-width: none;
}

/* THE CLUSTER IS ONE INSTRUMENT. At the row's own step its controls read as unrelated things spread
   across a card; at the tighter step they are one group with one job, which is what taking the
   labels off is for. `margin-left: auto` is not needed and would be wrong - the search's own growth
   is what puts this at the end, and an auto margin would fight it the moment the row wraps. */
.sv-filters__cluster {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: var(--sv-space-2);
}

/* Wide enough for its longest answer and no wider. Uncapped it is a box built for a sentence
   holding two words. */
.sv-filters__sort {
  width: 176px;
}

/* ONE HEIGHT ACROSS THE ROW, STATED ONCE. Controls from three families meet here - an input, a
   select and a button - each sized by its own rule, so left alone they stand at three heights on one
   row and it reads as a row nobody laid out. A screen adding a control of its own adds it to its own
   copy of this rule; the star toggle on the project list is the standing example.

   THE BUTTON IS MATCHED AS A BUTTON, not as the filter menu's trigger. `.sv-menu` is
   `page-header.css`'s family and `StylesheetOwnershipGuardTests` refuses it here - correctly, since
   what this row is entitled to say is how tall a control on it stands, not what a menu looks like. */
.sv-filters:has(.sv-filters__cluster) .sv-search .sv-input,
.sv-filters__sort,
.sv-filters__cluster .sv-btn {
  height: 36px;
  min-height: 36px;
}

/* WIDER THAN A MENU OF ITEMS AND PADDED LIKE A FORM, because it holds fields rather than words. The
   menu panel's own 168px floor and 4px inset are right for "Duplicate / Archive / Delete" and wrong
   for two labelled selects: at 4px the eyebrow sits on the panel's own border and the boxes touch
   its edges, so the fields read as a list that overflowed rather than as a small form.

   TWO CLASSES, AND THAT IS THE WHOLE REASON THIS RULE WORKS. The panel wears its own family's class
   and the menu's, the two rules are equally specific, and the menu's stylesheet is linked after this
   file - so written as one class this loses every declaration to it, silently, on every screen that
   draws a filter menu. Matched inside the filter row it is a class more specific and the load order
   stops deciding. It names nothing in the `sv-menu` family, which `StylesheetOwnershipGuardTests`
   refuses here and is right to: what this states is what a panel of FILTERS is, not what a menu is. */
.sv-filters .sv-filters__panel {
  min-width: 268px;
  padding: var(--sv-space-4);
}

/* ── What is narrowing the list, on a row of its own ──────────────────────
   ROW ONE IS THE CONTROLS AND NOTHING ELSE. The way out and the count used to ride at the far end of
   it, so a row that is three controls on nearly every visit became five the moment anybody typed -
   and the two that appeared were the two nobody was reaching for. They belong with the chips,
   because all three say one thing: this is what is on, and this is the way off it.

   DRAWN FOR CHIPS OR A SEARCH, and for nothing else. Those are the two that leave a reader wondering
   why a list is short - a chip because the panel that set it is closed, a search because a term
   narrows silently. A control whose own state is visible on the row draws no chip, because a row
   restating it is chrome appearing for no reason.
   ------------------------------------------------------------------------ */

/* `flex: 1 0 100%` IS WHAT MAKES IT A ROW. `.sv-card__toolbar` is a wrapping flex row, so a sibling
   of the filter bar is a control BESIDE it - without this the chips come out at the far right of the
   row they are supposed to sit under, reading as more filters rather than as what the ones above are
   set to. Taking the full basis wraps them onto the next line; the toolbar's own gap is the space
   between, so nothing here states a margin.

   WHAT IS ON at the left, THE WAY OFF IT at the right. */
.sv-filters__applied {
  display: flex;
  flex: 1 0 100%;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sv-space-3);
  margin: 0;
}

.sv-filters__chips {
  display: flex;
  flex: 1 1 auto;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sv-space-2);
  margin: 0;
  min-width: 0;
}

/* The two read as one group: a count nobody has to hunt for, beside the control that undoes it. */
.sv-filters__applied-end {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: var(--sv-space-3);
}

/* A TEXT ACTION, at the weight of the sentence beside it rather than of the controls above. Boxed,
   it carries a bordered button's weight on the least important thing on the card and pulls the eye
   away from the chips it belongs to. Underlined on hover, which is what says it acts. */
.sv-filters__clear-all {
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--sv-accent);
  font: 500 12px/1.4 var(--sv-font-sans);
  cursor: pointer;
}

.sv-filters__clear-all:hover {
  color: var(--sv-accent-hover);
  text-decoration: underline;
}

/* SENTENCE CASE AND SANS, because it is a sentence about the list. The uppercase mono eyebrow is for
   a label on a control; worn here it reads as a heading for the chips beside it. The figure inside is
   still mono - `Measured` sets that - which is the rule everywhere in the product. */
.sv-filters__matching {
  font: 400 12px/1.4 var(--sv-font-sans);
  color: var(--sv-text-tertiary);
  white-space: nowrap;
}

/* THE WHOLE CHIP IS THE BUTTON, not a label with a cross beside it: a 10px glyph is a click target
   nobody hits first time, and there is only one thing a chip does. The cross is a mark saying so. */
.sv-filters__chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 8px 3px 10px;
  border: 1px solid var(--sv-accent-underline);
  border-radius: var(--sv-radius-pill);
  background: var(--sv-accent-badge);
  color: var(--sv-accent);
  font: 500 11px/1.5 var(--sv-font-sans);
  cursor: pointer;
}

.sv-filters__chip:hover {
  border-color: var(--sv-accent);
}

/* The mark, at the weight of a border rather than of the word beside it. */
.sv-filters__chip-x {
  font: 400 13px/1 var(--sv-font-sans);
  color: var(--sv-accent);
  opacity: 0.7;
}

.sv-filters__chip:hover .sv-filters__chip-x {
  opacity: 1;
}

/* ── A value picked out of a catalogue ───────────────────────────
   THE BOX HOLDS A VALUE AND THE LIST NARROWS AS IT IS TYPED. `PickerField` is the component; what is
   drawn here is the box, the room the clear affordance needs, and the clear itself. Five fields pick
   out of a table with hundreds of rows and every one of them wore a Material autocomplete, which is
   a popup, a caret, a floating label and a second type scale inside a field this file already draws.

   THE LIST IS DRAWN HERE, NOT BY THE BROWSER. A native datalist is the engine's own surface: it
   picks the font, the row height, the highlight and the width, and none of them are this design
   system's - which is why one appearing under a field the product drew reads as a fault rather than
   as a list. `.sv-search` still hands its completions to a datalist and is right to, because those
   are hints about a term somebody is typing; this picks the VALUE, and a value is chosen off a
   surface the product owns.

   ONE AFFORDANCE, WHICH IS THE CLEAR. No caret and no arrow: the list opens on focus, which is the
   behaviour an arrow advertises, and a glyph that only says "there is a list" costs the width a
   battery's manufacturer, model and capacity are already using.
   ──────────────────────────────────────────────────────────────────────── */
.sv-picker {
  position: relative;
  display: block;
  width: 100%;
}

/* The room is the clear's, and it is stated on the input rather than won by the button overlapping
   the text: a battery named by its manufacturer, its model and its usable capacity runs the width of
   the box, and a glyph sitting on top of the last word is a value nobody can read. */
.sv-picker > .sv-input {
  width: 100%;
  padding-right: 30px;
}

/* WHAT MATCHES, UNDER THE BOX. The panel is the header menu's - raised ground, hairline, card
   radius, the same shadow - because a surface that floats over the page is one shape in this product
   and a picker is not the screen that gets to invent a second.

   IT IS TALLER THAN THE PAGE ALLOWS AND SCROLLS INSIDE ITSELF. The caller's search states the page
   size, so what arrives is twenty rows at most; 264px is eight of them, which is enough to show the
   list is longer than the box without covering the field under it.

   z-index 40 is the tier `.sv-menu__panel` sits on: over the page and its cards, under the shell's
   own chrome. A picker inside a dialog rides that dialog's stacking context and needs nothing
   further. */
.sv-picker__list {
  position: absolute;
  top: calc(100% + var(--sv-space-1));
  left: 0;
  right: 0;
  z-index: 40;
  max-height: 264px;
  margin: 0;
  padding: var(--sv-space-1);
  overflow-y: auto;
  list-style: none;
  background: var(--sv-bg-raised);
  border: 1px solid var(--sv-border);
  border-radius: var(--sv-radius-card);
  box-shadow: var(--sv-shadow-panel);
}

/* A ROW READS AS A MENU ITEM BECAUSE THAT IS WHAT IT IS. One line, wrapping rather than truncating:
   a battery names its manufacturer, its model and its usable capacity, and an ellipsis through the
   capacity hides the one figure the choice is being made on. */
.sv-picker__option {
  padding: 7px var(--sv-space-2);
  border-radius: var(--sv-radius-control);
  color: var(--sv-text);
  font: 400 13px/1.4 var(--sv-font-sans);
  cursor: pointer;
}

/* ONE HIGHLIGHT FOR BOTH WAYS IN. The pointer's hover and the keyboard's position are the same
   state, so they wear the same ground - a list where arrowing down lights one row and hovering
   lights another is two cursors in one control. */
.sv-picker__option:hover,
.sv-picker__option--active {
  background: var(--sv-bg-overlay);
}

/* NOTHING FOUND IS A ROW, NOT AN ABSENCE. Quiet, and not selectable: it is the list answering, and
   a list that answers is the difference between a search that found nothing and a control that
   stopped working. */
.sv-picker__note {
  padding: 7px var(--sv-space-2);
  color: var(--sv-text-tertiary);
  font: 400 13px/1.4 var(--sv-font-sans);
}

/* A HIT AREA, NOT A GLYPH. It rests in the quiet tier and comes up to full ink under the pointer,
   like every other affordance that removes something without being destructive - clearing a picker
   unassigns a choice, it does not delete a row. */
.sv-picker__clear {
  position: absolute;
  top: 50%;
  right: 4px;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: var(--sv-radius-chip);
  background: transparent;
  color: var(--sv-text-tertiary);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}

.sv-picker__clear:hover {
  background: var(--sv-bg-overlay);
  color: var(--sv-text);
}

.sv-search {
  position: relative;
  display: flex;
  align-items: center;
  width: 100%;

  /* ONE WIDTH. Written inline it was 280px on three screens, 320px on three and
     absent on five, so the same control was three sizes across one console. */
  max-width: 320px;

  /* The wrapper takes the width the input used to carry, so a flex row still
     sizes it. Without min-width: 0 it refuses to shrink below its content. */
  min-width: 0;
}

.sv-search::before {
  content: "";
  position: absolute;
  left: 10px;
  width: 14px;
  height: 14px;
  pointer-events: none;
  background-color: var(--sv-text-tertiary);
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round'%3E%3Ccircle cx='7' cy='7' r='4.5'/%3E%3Cpath d='M10.4 10.4 14 14'/%3E%3C/svg%3E");
  mask-repeat: no-repeat;
  mask-size: contain;
}

.sv-search > .sv-input {
  padding-left: 32px;
}

/* The user agent's own clear affordance sits where the text does; leaving the
   default gives two glyphs of chrome in a 280px box. */
.sv-search > .sv-input::-webkit-search-decoration {
  appearance: none;
}

/* AND A BOX WITH COMPLETIONS DRAWS NO CARET. Binding a datalist makes the browser offer its own
   drop-down arrow, which puts a second affordance in a box whose affordance is already the
   magnifier - and says the list is the point when the point is free text that happens to be
   completed. The list still opens as the term is typed. */
.sv-search > .sv-input::-webkit-calendar-picker-indicator {
  display: none;
}

/* ── A note under a form control ───────────────────────────────────────────────
   Twenty screens set this, which is why it is here and not in the stylesheet of
   the screen it was first written for. FormField's own .sv-field__hint is the
   plain sentence a labelled control carries; this is the note under a MudBlazor
   input, which draws its own label and leaves the gap under it for helper text.

   PADDED TO THE INPUT'S OWN TEXT, NOT TO ITS BOX. MudBlazor's outlined input
   carries 14px of left padding, so a note flush with the box starts 14px to the
   left of the value it is explaining and hangs off the stack as a stray line.
   The 2px top margin tucks it into that helper gap; -6px puts it inside the box
   and the note and the border overlap.
   ------------------------------------------------------------------------ */
.sv-form-note {
  font-size: 12px;
  line-height: 1.5;
  color: var(--sv-text-secondary);
  margin: 2px 0 var(--sv-space-3);
  padding-left: 14px;
}

/* ── A checkbox ───────────────────────────────────────────────────────────────
   The box and the words beside it. Components/Shared/CheckboxField.razor wraps
   the label around both, which is what makes the words a click target; this is
   the pair that wrapper draws.
   ------------------------------------------------------------------------ */
.sv-checkbox {
  display: flex;
  align-items: center;
  gap: var(--sv-space-2);
  font: 400 13px/1.4 var(--sv-font-sans);
  color: var(--sv-text-secondary);
  cursor: pointer;
}

.sv-checkbox input {
  accent-color: var(--sv-accent);
  width: 15px;
  height: 15px;
}

/* ── A toggle standing in a row of labelled fields ────────────────────────
   IT STANDS IN A BOX OF THE FIELDS' OWN HEIGHT, or it floats half a line proud of a row it belongs
   to and reads as something that has come loose. Two screens draw it - the projects filter row's
   starred toggle and the condition editor's three model switches - so it is a modifier on the
   checkbox rather than a rule in either screen's stylesheet.

   THE HEIGHT IS NOT A NUMBER. It is `.sv-input`'s padding around the checkbox's own line box, so
   the box is the same height as the select beside it by construction rather than by a 34px somebody
   measured once and no test holds. `align-self` is what puts it on the controls' baseline in a row
   whose other members carry an eyebrow label above them.
   ────────────────────────────────────────────────────────────────────────── */
.sv-checkbox--boxed {
  align-self: flex-end;
  padding: 9px 11px;
  border: 1px solid var(--sv-border-input);
  border-radius: var(--sv-radius-control);
  background: var(--sv-bg-raised);
  white-space: nowrap;
}

.sv-checkbox--boxed input {
  width: 13px;
  height: 13px;
  border-radius: var(--sv-radius-chip);
}

/* ── What a multi-select type-ahead has chosen ───────────────────────────────
   The chips beneath the box. It WRAPS and never scrolls: the selection is the
   state of the form, so it has to be readable at a glance, and a picker that
   hides half of what it will act on behind a scrollbar is the thing this
   pattern exists to avoid. Twenty chips make a tall block, which is honest -
   twenty documents is a lot to ask for in one press. */

.sv-picker__chosen {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sv-space-2);
  margin-top: var(--sv-space-2);
}

.sv-validation {
  color: var(--sv-danger);
  font: 400 12px/1.45 var(--sv-font-sans);
}

/* A SUMMARY IS THE LIST, not a box around one. Blazor's ValidationSummary renders the `ul` itself
   and lets the call site's class win, so the class arrives ON the list rather than around it - and
   a descendant selector alone left every account form's summary carrying the browser's own 40px
   indent and 1em margins in the middle of a 380px column. Both, because the two shapes are one
   rule: the errors are the errors wherever the element boundary happens to fall. */
.sv-validation ul,
ul.sv-validation {
  margin: 0;
  padding-left: 18px;
}

/* ── Status messages ──────────────────────────────────────────────────────── */

/* AN ALERT SEPARATES ITSELF FROM WHAT FOLLOWS IT, exactly as .sv-notice already does: the two are
   the same shape of thing - a standing statement above the block it is about - and one carrying its
   own gap while the other left it to whoever wrote the page is the disagreement that had sixteen
   alerts stating `margin-bottom` inline and ten more sitting flush against the next block. */
.sv-alert {
  display: flex;
  gap: var(--sv-space-2);
  padding: var(--sv-space-3) var(--sv-space-4);
  border-radius: var(--sv-radius-control);
  border: 1px solid var(--sv-border);
  background: var(--sv-bg-overlay);
  font: 400 13px/1.5 var(--sv-font-sans);
  color: var(--sv-text-secondary);
  margin-bottom: var(--sv-space-4);
}

/* Nothing follows, so there is no gap before it - the container's padding is the bottom edge. */
.sv-alert:last-child {
  margin-bottom: 0;
}

.sv-alert--error {
  border-color: color-mix(in srgb, var(--sv-danger) 45%, transparent);
  color: var(--sv-danger-ink);
}

.sv-alert--success {
  border-color: var(--sv-accent);
  color: var(--sv-text);
}

/* Amber is the only chromatic colour in the product, so a warning cannot be a second hue. It is the
   accent border a success carries plus the accent wash behind it - enough to separate "this went
   well" from "read this before continuing" without inventing a colour. The text stays at full
   contrast because --sv-accent fails AA as body text on paper. */
.sv-alert--warning {
  border-color: var(--sv-accent);
  background: var(--sv-accent-wash);
  color: var(--sv-text);
}

/* ── Badges ───────────────────────────────────────────────────────────────── */

.sv-badge {
  /* align-self keeps a badge its own width inside a flex column, which would otherwise stretch it
     across the whole card. */
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: var(--sv-radius-pill);
  border: 1px solid var(--sv-border);
  background: var(--sv-bg-overlay);
  font: 500 10px/1.4 var(--sv-font-mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--sv-text-secondary);

  /* A PILL IS ONE LINE OR IT IS NOT A PILL. Folded over two lines in a column an inch too narrow it
     reads as a rendering fault rather than as a column that wants widening, and the fix a reader
     asks for is the column's, not the badge's. The columns that carry one state a width that holds
     their longest label; this is what makes a column too narrow show up as a clip rather than
     silently absorb the fault. */
  white-space: nowrap;
}

/* THE FILL IS OPAQUE AND THE WASH IS NOT, and that is the whole of the difference. This badge is
   drawn on a selected row as often as on a plain one, and a translucent amber over an amber wash
   comes out a third tone that reads as a fourth verdict. */
.sv-badge--accent {
  border-color: var(--sv-accent);
  color: var(--sv-accent);
  background: var(--sv-accent-badge);
}

/* A RULE THAT CLEARED. The one green in the product, and it is a verdict rather than
   chrome - the same category as the module blues and the wiring zones, which are also
   colour carrying data. The accent cannot say this: amber already means "read this
   before continuing" on .sv-alert--warning and on .sv-badge--accent beside this one, so
   an amber pass and an amber warning would be the same pill wearing two meanings.

   THE THREE VERDICTS ARE ONE FAMILY IN THREE TONES - a pass here, a warning in
   .sv-badge--accent, and anything nobody could check in the plain badge. A page picks
   between them; it does not invent a fourth. */
.sv-badge--pass {
  border-color: var(--sv-pass-border);
  color: var(--sv-pass);
  background: var(--sv-pass-fill);
}

/* A RULE THAT REFUSES. Not a louder warning and not a fourth invented tone: it is the danger token,
   which exists so that a failure cannot also be amber, and it is used where something is actually
   stopped rather than wherever a screen wants emphasis. */
.sv-badge--danger {
  border-color: var(--sv-danger);
  color: var(--sv-danger-ink);
  background: transparent;
}

/* ── Lifecycle badges ─────────────────────────────────────────────────────────
   A STATUS IS NOT A VERDICT, and this is the family that says so. The four verdict
   tones answer "did this rule clear", which is a judgement with four possible answers;
   a lifecycle answers "where has this got to", which has as many answers as the enum
   has members and is READ AS A COLUMN. Eight states through four tones put Draft,
   Design and anything ungraded in one grey, so a list of work in progress was one
   colour and told the reader nothing they could not have got from the words.

   THE SIX TINTS ARE ONE SHAPE - own hue, own pale fill, own border - so no state is
   louder than another. Rank is not what a lifecycle carries; position is.

   A STATUS PICKS ITS TONE IN ONE PLACE, never on the page drawing it. For a project
   that is ProjectStatusText.BadgeClass, and any other status enum that grows a colour
   gets its own single mapping the same way. A page choosing per badge is how the same
   state comes to be blue on one screen and grey on the next.
   ------------------------------------------------------------------------------ */

.sv-badge--status-progress {
  border-color: var(--sv-status-progress-border);
  color: var(--sv-status-progress);
  background: var(--sv-status-progress-fill);
}

.sv-badge--status-filed {
  border-color: var(--sv-status-filed-border);
  color: var(--sv-status-filed);
  background: var(--sv-status-filed-fill);
}

.sv-badge--status-pending {
  border-color: var(--sv-status-pending-border);
  color: var(--sv-status-pending);
  background: var(--sv-status-pending-fill);
}

/* THE END OF THE ROAD, DRAWN SOLID. Won and Lost are the two states nothing follows,
   and they are the two a reader scanning a portfolio is looking for; filled, they are
   found without reading. The ink is the paired on- token in both, never the tone
   itself, because a tone on its own fill is the pairing this family exists to stop. */
.sv-badge--status-won {
  border-color: var(--sv-status-won-fill);
  color: var(--sv-on-status-won);
  background: var(--sv-status-won-fill);
}

.sv-badge--status-lost {
  border-color: var(--sv-danger);
  color: var(--sv-on-danger);
  background: var(--sv-danger);
}

/* ── Tones ────────────────────────────────────────────────────────────────────
   THE SAME STATE COLOURS WITHOUT THE CAPSULE. A badge is a colour, a fill, a border, a face and a
   shape all at once; sometimes only the colour is wanted, and taking a badge for it means taking
   the shape too. These set the colour and nothing else, so a mark reading `currentColor` picks the
   state up and the words beside it stay ordinary words.

   THE STEM IS SHARED WITH .sv-badge--*, and deliberately: `ProjectStatusText` maps a state to one
   token stem and both class names are composed from it, so a state cannot be blue in a list and
   purple on the screen it opens. A tone added here without its badge, or the reverse, is the drift
   that pairing exists to stop.
   ─────────────────────────────────────────────────────────────────────────── */

/* NO STATE AT ALL, which is a state. Draft, and anything ungraded: the resting colour the others
   are read against. A hue here and there is nothing for the column to rest on. */
.sv-tone--resting  { color: var(--sv-text-tertiary); }

.sv-tone--accent          { color: var(--sv-accent); }
.sv-tone--pass            { color: var(--sv-pass); }
.sv-tone--status-progress { color: var(--sv-status-progress); }
.sv-tone--status-filed    { color: var(--sv-status-filed); }
.sv-tone--status-pending  { color: var(--sv-status-pending); }
.sv-tone--status-won      { color: var(--sv-status-won-fill); }
.sv-tone--status-lost     { color: var(--sv-danger); }

/* ── The status control ───────────────────────────────────────────────────────
   WHERE A PROJECT HAS GOT TO, ON THE LINE THAT CARRIES ITS NAME, and the one place in the product
   a state is drawn without a capsule.

   IT IS NOT A BADGE, AND THAT IS THE WHOLE POINT. Set as one it was 10px uppercase mono with a
   letterspaced pill around it - the same face, size, case and tracking as the run of facts beside
   it - so the box was the only thing the badge added, and a filled amber box beside a 27px name
   took the eye off the name.

   IT IS WORDLESS, AND IT SITS IN FRONT OF THE NAME. A label here is a second string competing with
   the one thing the line is for, and all eight are read together in the menu anyway - which is
   where the choice is actually made. In front rather than after, because a mark that qualifies a
   name reads as part of it while anything following the name interrupts the line after it. The
   hover label and the aria-label carry the sentence a colour cannot.

   THE HIT AREA IS PULLED BACK BY ITS OWN PADDING so the dot sits on the same left edge as the
   section strip below it: a control with an invisible box has to be measured from its ink, or every
   title in the product is indented by one control's padding and lines up with nothing.
   ─────────────────────────────────────────────────────────────────────────── */
.sv-statuspick {
  display: inline-flex;
  align-items: center;
  gap: 1px;
  flex: none;

  /* The pull is the left padding exactly, so the dot's own edge lands on the gutter and the mark
     lines up with the section strip underneath it rather than a control's box-width in from it. */
  margin-left: -7px;

  /* And a little of the right back, because the gap the title row states is the gap between two
     things that are read separately - this one is read WITH the name. */
  margin-right: -4px;

  padding: 5px 4px 5px 7px;
  border: 1px solid transparent;
  border-radius: var(--sv-radius-control);
  background: none;
  line-height: 0;
}

/* A STATE SOMEBODY MAY MOVE, which is the same object with a hand on it. A viewer gets the line
   above and nothing else: no hover, no caret, no box that turns out not to open. */
button.sv-statuspick {
  cursor: pointer;
}

button.sv-statuspick:hover {
  border-color: var(--sv-border);
  background: var(--sv-bg-overlay);
}

/* THE STATE ITSELF, and the whole of what the control says at rest. Nine pixels rather than eight:
   with the words gone it carries the state on its own, and the tones are read against each other
   rather than identified cold - which is what lets a hue work at a size no text contrast would
   pass. Reused at this size in the menu, where the words are. */
.sv-statuspick__dot {
  width: 9px;
  height: 9px;
  flex: none;
  border-radius: var(--sv-radius-pill);
  background: currentColor;
}

/* THE ONE MARK THAT SAYS THIS OPENS. Quiet at rest and full strength under the pointer: a caret at
   full weight beside a title is a second thing competing with the name, which is what the pill it
   replaced was doing before it was even worth pressing. Sized here rather than through MudIcon's
   scale, whose Small is 20px and taller than the line it sits on. */
.sv-statuspick__caret {
  width: 14px;
  height: 14px;
  font-size: 14px;
  color: var(--sv-text-tertiary);
  opacity: 0.55;
}

button.sv-statuspick:hover .sv-statuspick__caret {
  opacity: 1;
}


/* AN ACTION IN A DIALOG THAT IS NOT PART OF CONFIRMING IT, set away from the
   rest. One dialog has one: a button that emails somebody a password reset
   should not sit under the hand reaching for Save. Written inline it was a
   `style="margin-right: auto;"` on the button itself, which is the shape that
   spreads the moment a second dialog needs one. */
/* THE STEP FROM ONE BLOCK OF A DIALOG TO THE NEXT, stated once here rather than by each of the
   dialogs that carries more than one. Same rule and same reason as .sv-card, above. */
.sv-dialog__body > * + * {
  margin-top: var(--sv-space-4);
}

.sv-dialog__aside {
  margin-right: auto;
}

/* A BADGE THAT FOLLOWS SOMETHING ON THE SAME LINE - a name, a model number,
   another badge. Written inline as `style="margin-left: var(--sv-space-2);"` it
   appeared verbatim sixteen times, which is a modifier somebody had already
   written sixteen times without naming. */
.sv-badge--trailing {
  margin-left: var(--sv-space-2);
}

/* A count on the tab that owns it. The filled amber is deliberate and is the one place a data screen
   shouts: the question this answers is "is anybody waiting on me", and the whole point is being
   readable without opening the tab. --sv-on-accent, never --sv-accent - amber text on an amber fill
   is the mistake this pairing exists to stop. Zero never renders; a badge reading 0 is a claim that
   something is waiting. */
.sv-tab-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  margin-left: 6px;
  padding: 0 5px;
  border-radius: var(--sv-radius-pill);
  background: var(--sv-accent-fill);
  color: var(--sv-on-accent);
  font: 600 11px/1 var(--sv-font-mono);
}

/* ── Label-and-value pairs in a WIDE panel ────────────────────────────────── */

/* LABEL-AND-VALUE PAIRS IN A WIDE PANEL, where a row of KPI tiles would be too
   loud for what is being said - a site's facts, a plan's terms, what a schedule
   covers. A measured figure that heads a screen is a KpiTile; these are facts
   read together.

   Each pair is its own grid cell, so the label and the number cannot be
   separated by the track count however wide the panel gets. An auto-fit track
   over bare dt and dd flows them as INDEPENDENT items, and in a panel wide
   enough for four columns a value lands under the wrong label. */
.sv-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--sv-space-4);
  margin: 0;
}

.sv-facts > div {
  /* min-width: 0 down the chain - a grid item defaults to its min-content, and
     a long mono figure would otherwise push the panel past the page edge rather
     than wrapping. */
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sv-space-1);
}

.sv-facts dt {
  font: 500 10px/1.4 var(--sv-font-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--sv-text-tertiary);
}

.sv-facts dd {
  margin: 0;
  font: 400 15px/1.4 var(--sv-font-mono);
  color: var(--sv-text);
}

/* Something that is deliberately NOT the case: a capability a plan does not
   include, a state not reached. Recessive rather than absent, because the whole
   point of showing it is that the reader can see what they would gain - the same
   reason a waterfall step that does not apply reports zero rather than
   disappearing. No chromatic colour: this is not a warning, and amber is the
   product's only accent. */
.sv-badge--muted {
  border-color: var(--sv-border-subtle);
  color: var(--sv-text-tertiary);
  background: transparent;
}

/* A badge that annotates the value beside it - "1 CURRENT", "Base case DEFAULT".
   Razor's whitespace collapses to a single space, which is not a gap: the pill's
   own border lands about 4px from the digit and the two read as one token. The
   several `style="margin-left: var(--sv-space-2)"` attributes scattered through
   the component pages are this rule written out by hand; this is the one place
   for it. */
.sv-badge--annotates {
  margin-left: var(--sv-space-2);
}

/* ── A card of headline figures, drawn dark ────────────────────────────────
   THE ONE SURFACE ON THE PAGE THAT IS NOT WHITE, and nearly the whole of what it does. A strip
   summarising everything under it competes with the cards it summarises when it is drawn like them:
   same white, same rules, same weight, so a reader arriving at the screen has nothing telling them
   where to start. Inverted, it says "read me first" without a heavier border or a type size the
   rest of the page then has to be measured against.

   THE NAVY IS THE NAVIGATION COLUMN'S, RICHER. The two dark surfaces on the screen are the same
   idea - chrome around what is being read - and two darks a hue apart read as one of them being
   wrong. This one carries more blue because it sits inside the page rather than beside it, and the
   gold edge along its top is the only thing here that is an accent rather than a ground.

   IT REPAINTS ITS PARTS RATHER THAN ASKING THEM TO BE DIFFERENT. `CardHeader`, `KpiTile` and the
   divider are drawn the same everywhere; what changes is the ground under them, so the ink follows
   from the modifier and no caller passes a colour.

   ONE MODIFIER, ANY READOUT. It is on the card rather than on a screen, so the portfolio strip and
   anything else that summarises a page wear the same thing - which is what stops the next one being
   a second dark surface half a shade off this one.
   ------------------------------------------------------------------------ */

/* THE GOLD EDGE IS THE ACCENT FILL AND NOT A TOKEN OF ITS OWN. The palette already declares this
   gold, in both modes, and a --sv-readout-edge beside it would be a second name for one value -
   the same defect as two values for one name, and the one that lets a re-brand move the buttons
   and leave the strips behind.

   TWO PIXELS ON THE TOP EDGE ONLY, which is why this restates the whole border-top rather than its
   colour: the card's own 1px hairline continues round the other three sides, and a gold line all
   the way round would draw a box rather than mark a start. It is the same idea as the sidebar's
   active item - the accent as a thin edge saying where to begin, not as a fill. */
.sv-card--readout {
  background: var(--sv-readout);
  border-color: var(--sv-readout-border);
  border-top: 2px solid var(--sv-accent-fill);
  color: var(--sv-readout-ink);
}

/* The band carries no tint of its own here: the card IS the tint, and a second one over it reads as
   a bar on a bar. The rule under it stays, because it still separates the heading from the figures. */
.sv-card--readout .sv-card__band {
  background: transparent;
  border-bottom-color: var(--sv-readout-border);
}

.sv-card--readout .sv-card__band h2,
.sv-card--readout .sv-kpi__value {
  color: var(--sv-readout-ink);
}

/* An eyebrow, a unit and a fact beside the heading are one tier: present, and not what is being
   read. The caption under a figure is a step quieter again - it is a sentence, and a sentence at
   the weight of the label above it competes with the figure between them. */
/* `.sv-unit` is here as well as `.sv-kpi__unit` because a strip composed out of the parts rather
   than out of `KpiTile` writes the unit as the former - and left on its paper colour it measured
   2.6:1 on this ground, which is a measurement rendered next to a figure nobody can read it
   against. Both spellings, one tier. */
.sv-card--readout .sv-label,
.sv-card--readout .sv-kpi__unit,
.sv-card--readout .sv-unit {
  color: var(--sv-readout-ink-quiet);
}

/* The rows behind a figure take the same two quiet tiers the unit and the caption do here, and
   their three verdict tones are the readout's own: --sv-accent measures under 3:1 on this ground,
   --sv-danger-ink is a paper red on a near-black, and --sv-pass is a paper green that comes out a
   smudge on it. */
.sv-card--readout .sv-kpi__rows {
  border-top-color: var(--sv-readout-border);
}

.sv-card--readout .sv-kpi__rows dt {
  color: var(--sv-readout-ink-faint);
}

.sv-card--readout .sv-kpi__rows dd {
  color: var(--sv-readout-ink);
}

.sv-card--readout .sv-kpi__rows dd.sv-kpi__row--pass {
  color: var(--sv-readout-pass);
}

.sv-card--readout .sv-kpi__rows dd.sv-kpi__row--warn {
  color: var(--sv-readout-link);
}

.sv-card--readout .sv-kpi__rows dd.sv-kpi__row--severe {
  color: var(--sv-readout-severe-ink);
}

.sv-card--readout .sv-kpi__caption {
  color: var(--sv-readout-ink-faint);
}

/* THE MIDDOT BETWEEN TWO FACTS, a step quieter than the facts either side of it, which is the
   relationship it has on paper. --sv-text-faint is a paper value and lands BRIGHTER than the
   labels it separates on this ground: the loudest mark in the run would be the punctuation. */
.sv-card--readout .sv-card__facts > .sv-label + .sv-label::before {
  color: var(--sv-readout-ink-faint);
}

/* Every rule inside, whichever direction it runs: the hairlines between figures, the upright one
   after the heading, and the grid's own. A rule tuned for paper is invisible on this. */
.sv-card--readout .sv-grid--kpi-flush > *,
.sv-card--readout .sv-divider,
.sv-card--readout .sv-divider--vertical {
  border-color: var(--sv-readout-border);
  background-color: var(--sv-readout-border);
}

/* The upright divider is a painted shape rather than a border, so it takes the fill and nothing
   else - given the border rule above it would draw a box. */
.sv-card--readout .sv-grid--kpi-flush > * {
  background-color: transparent;
}

/* THE WAY OUT OF THE SECTION, and it is the one warm thing on this surface. --sv-accent is tuned to
   hold against paper and measures under 3:1 here; this is the same hue lifted to read on it. */
.sv-card--readout .sv-card__facts-link {
  color: var(--sv-readout-link);
}

/* A grade dot on a dark ground. --sv-border-strong is a paper value and disappears. */
.sv-card--readout .sv-kpi--dot .sv-label::before {
  background: var(--sv-readout-ink-quiet);
}

/* A MARKED CELL SURVIVES THE MOVE TO THIS GROUND RATHER THAN BEING FLATTENED ONTO IT. The marking
   is how a reader FINDS the cell - it is read afterwards - so a strip that drops its marks when it
   goes dark reports a blocking finding and a cleared one identically. It has to be restated
   because the rule above sets every cell transparent to keep the dividers from drawing boxes.

   AND ON THIS GROUND IT IS AN EDGE AND AN INK, NOT A PANEL. The paper marking is amber at 14% over
   white, which is a cream; the same tint over this navy is a grey-olive, because amber carries
   almost no blue and cancels the ground rather than colouring it - and any opaque tint dark enough
   to sit under the figures puts a second surface inside a strip whose whole argument is that it is
   one surface. So the cell keeps the navy, the bar down its edge says WHICH cell, and every word in
   it is set in the marking's colour, which for amber is the colour the icon already takes.

   THE BAR IS AN ::after ON THE CELL, `.sv-label::before` being the grade dot and already spoken
   for. It runs the full height of the cell: the strip's cells are joined on a hairline, so a bar
   stopping short of the join reads as a floating rule rather than as the edge of this cell. */
.sv-card--readout .sv-grid--kpi-flush > .sv-kpi--attention,
.sv-card--readout .sv-grid--kpi-flush > .sv-kpi--severe {
  position: relative;
}

.sv-card--readout .sv-grid--kpi-flush > .sv-kpi--attention::after,
.sv-card--readout .sv-grid--kpi-flush > .sv-kpi--severe::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 3px;
  background: var(--sv-accent-fill);
}

.sv-card--readout .sv-grid--kpi-flush > .sv-kpi--severe::after {
  background: var(--sv-readout-severe);
}

/* THE LABEL AND THE FIGURE, NOT THE FIGURE ALONE. A marked cell whose label stayed the quiet grey
   read as an ordinary cell with one coloured number in it, and the number is the part a reader was
   going to read anyway - it is the LABEL that has to carry the mark, because that is what is
   scanned. --sv-accent-fill is the icon's own colour and holds 7.5:1 here; --sv-accent is the paper
   value and measures under 3:1.

   THE CAPTION IS NOT IN THE LIST, and that is deliberate. It is the sentence saying what the figure
   counts, it reads the same whether the cell is marked or not, and coloured with the rest it made
   the cell a block of amber prose with the figure no louder than the line under it. */
.sv-card--readout .sv-grid--kpi-flush > .sv-kpi--attention :is(
  .sv-label, .sv-kpi__value, .sv-kpi__unit, .sv-unit) {
  color: var(--sv-accent-fill);
}

/* THE RED CELL SPLITS THE TWO. #E5484D is the edge and the icon, where 4.1:1 is a shape holding
   its own; the words take the lifted red, because the same colour under small type is text nobody
   can read. */
.sv-card--readout .sv-grid--kpi-flush > .sv-kpi--severe :is(
  .sv-label, .sv-kpi__value, .sv-kpi__unit, .sv-unit) {
  color: var(--sv-readout-severe-ink);
}

.sv-card--readout .sv-kpi--severe .sv-label::before {
  background: var(--sv-readout-severe);
}

/* A SENTENCE THE BAND CARRIES, which is what a readout says when one of its own answers is missing.
   --sv-text-secondary measures 2.6:1 on this ground: the consequence of a gap, set under the gap
   that caused it, in ink nobody can read. */
.sv-card--readout .sv-notice :is(p, li) {
  color: var(--sv-readout-ink-quiet);
}

/* AN ANSWER NOBODY HAS GIVEN, in this surface's own quiet tier rather than in the cool grey it
   takes from paper. It clears AA on this ground either way; what it does not do is belong. */
.sv-card--readout .sv-text-tertiary {
  color: var(--sv-readout-ink-quiet);
}

/* ── Tables ───────────────────────────────────────────────────────────────── */

.sv-table-wrap {
  overflow-x: auto;
  border: 1px solid var(--sv-border-subtle);
  border-radius: var(--sv-radius-card);
}

.sv-table {
  width: 100%;
  border-collapse: collapse;
  font: 400 13px/1.45 var(--sv-font-sans);
}

/* A TABLE THAT STOPS WHERE ITS CONTENT STOPS.
   `width: 100%` is right for a wire schedule, whose columns genuinely want the width. It is wrong
   for a short table with one prose column: the prose is capped at 68ch by .sv-prose, every other
   column is a fixed width, and the difference between the sum of those and the monitor has to go
   somewhere - it goes inside the one elastic column as a hole. Measured on the characterisation
   list at 2133px: the provenance column was 1146px wide around 489px of text, and the timestamp
   and action columns were flung to the far edge with 657px of nothing before them.

   Use this where the columns are few and their content is bounded. Do not use it where a column is
   a name that can be long, or the table will jitter between rows. */
.sv-table--fit {
  width: auto;
  max-width: 100%;
}

.sv-table thead th {
  position: sticky;
  top: 0;
  background: var(--sv-table-head);
  padding: 9px var(--sv-space-3);
  text-align: left;
  font: 500 10px/1.2 var(--sv-font-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--sv-text-tertiary);
  border-bottom: 1px solid var(--sv-border-subtle);
  white-space: nowrap;
}

/* THE RULE BETWEEN TWO ROWS IS LIGHTER THAN THE RULE UNDER THE HEADER. A table sets its rows apart
   with the faintest boundary that still separates them, or forty of them read as forty boxes; the
   header's own rule stays on --sv-border-subtle so the head is told from the body. */
.sv-table tbody td {
  padding: 10px var(--sv-space-3);
  border-bottom: 1px solid var(--sv-border-row);
  color: var(--sv-text-secondary);
  vertical-align: top;
}

.sv-table tbody tr:last-child td {
  border-bottom: 0;
}

.sv-table tbody tr:hover td {
  background: var(--sv-bg-overlay);
}

/* Numeric columns are mono and right-aligned so digits line up column-wise. */
.sv-table .sv-num {
  font-family: var(--sv-font-mono);
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
  color: var(--sv-text);
}

/* A NUMERIC HEADING SITS OVER ITS OWN DIGITS, AND THE MARKER SAYING SO HAS TO OUTWEIGH THE HEADER
   RULE. `.sv-table thead th` sets text-align: left with one class and two elements, which beats the
   single class of `.sv-table__numeric`: that marker rendered as nothing on every heading wearing it,
   so a right-aligned column of figures sat under a word pinned to the far left of it. Both spellings
   are named because the cells use two: `.sv-num`, and `.sv-table__numeric` beside `.sv-measured`.

   The colour is restored for the same reason in reverse. `.sv-table .sv-num` carries two classes, so
   on a `th` it also wins the colour and drags the measured headings up to full text while their
   neighbours stay tertiary - a header row shaded by what its column happens to hold. Alignment is
   the only thing a numeric heading takes from the column below it. */
.sv-table thead th.sv-num,
.sv-table thead th.sv-table__numeric {
  text-align: right;
  color: var(--sv-text-tertiary);
}

/* A MONO CELL HOLDS AN ID, A CORRELATION OR A CHANGE SET, AND NONE OF THOSE HAS A SPACE IN IT. With
   nowhere to break, the cell's min-content width is the whole string: one audit row carrying a
   hundred characters of JSON sets a column wider than the table and pushes every column after it
   past the right edge, where the reader finds a horizontal scrollbar and a truncated last column.
   .sv-num is excluded on purpose - a measured number split across two lines reads as two numbers. */
.sv-table tbody td.sv-mono:not(.sv-num) {
  overflow-wrap: anywhere;
}

.sv-empty {
  padding: var(--sv-space-6);
  text-align: center;
  color: var(--sv-text-tertiary);
  font: 400 13px/1.6 var(--sv-font-sans);
}

/* THE SENTENCE, THEN THE CONTROL UNDER IT. .sv-empty centres text and sets no layout at all, so an
   empty state ending in a button ran the two together on one line and left the button hanging off
   the last word of the prose.

   The box is deliberately NOT made a flex column. Only a contiguous run of TEXT becomes one
   anonymous flex item, so a sentence carrying a <b> or a link would break into a stack of one item
   per inline element - and a third of these interpolate a count or a filter term. Taking the
   direct-child control out of the flow fixes the case that was wrong and touches no other. */
.sv-empty > .sv-btn {
  display: flex;
  width: fit-content;
  margin: var(--sv-space-4) auto 0;
}

/* A TABLE INSIDE A 320px PANE. The type comes down and the padding with it, or a four-column
   monthly breakdown pushes the panel past the edge of the page. DataTable's modifier, so it is
   here beside .sv-table rather than in the stylesheet of the first screen that needed it. */
.sv-table--compact th,
.sv-table--compact td {
  padding: 3px 6px;
  font-size: 11px;
}

/* ── KPI tile ─────────────────────────────────────────────────────────────── */

.sv-kpi {
  background: var(--sv-bg-raised);
  border: 1px solid var(--sv-border-subtle);
  border-radius: var(--sv-radius-card);
  padding: var(--sv-space-4);
  display: flex;
  flex-direction: column;
  gap: var(--sv-space-2);
}

/* The icon sits ON the label rather than above it, so a tile with one and a tile without keep the
   same three rows and the same baseline. Uppercase mono at 10px is a short line; an icon set to the
   cap height beside it reads as part of the label rather than as a fourth element. */
.sv-kpi__label {
  display: flex;
  align-items: center;
  gap: 6px;
}

/* TWO CLASSES, TO OUT-SPECIFY MUDBLAZOR'S OWN. `.mud-icon-root.mud-svg-icon` fixes the size at
   1.5rem, and a single class here would lose to it whatever order the stylesheets load in.

   AMBER AS A SHAPE, WHICH IS WHAT THE ACCENT IS FOR. The label beside it stays on its quiet ink:
   two amber elements in one line would make the label look like the thing being pointed at. */
.sv-kpi .sv-kpi__icon {
  width: 16px;
  height: 16px;
  flex: none;
  color: var(--sv-accent-fill);
}

.sv-kpi__value {
  font: 400 26px/1.1 var(--sv-font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--sv-text);
}

.sv-kpi__caption {
  font: 400 12px/1.4 var(--sv-font-sans);
  color: var(--sv-text-tertiary);
}

/* THE UNIT UNDER THE FIGURE, AND IT IS MONO BECAUSE IT IS PART OF A MEASUREMENT. The caption above
   is prose about the number and this is what the number is measured in, so the two are set in
   different faces on purpose - the results page had them as one slot in the mono face and the
   dashboard as one slot in the sans, which decided the face by which screen you were on rather
   than by what the words were. */
.sv-kpi__unit {
  font: 400 10px/1.4 var(--sv-font-mono);
  color: var(--sv-text-tertiary);
}

.sv-points {
  display: flex;
  flex-direction: column;
  gap: var(--sv-space-3);
  list-style: none;
  margin: 0;
  padding: 0;
}

.sv-points li {
  display: flex;
  gap: var(--sv-space-2);
  color: var(--sv-text-secondary);
  font: 400 14px/1.5 var(--sv-font-sans);
}

.sv-points li::before {
  content: '→';
  color: var(--sv-accent);
  font-family: var(--sv-font-mono);
}

/* ── Paper ────────────────────────────────────────────────────────────────────
   The one rule that is about the page a browser prints rather than about any
   element on it. Everything a print surface draws is .sv-print and .sv-page.
   ------------------------------------------------------------------------ */
@page {
  size: A4;
  margin: 14mm;
}

/* ── MudBlazor reconciliation ─────────────────────────────────────────────
   MudBlazor has one Primary and uses it as both a text colour and a fill, so
   its light palette has to hold the darkened text amber (ThemeService keeps
   Primary = #B8791A for exactly that reason). That is right for a link and
   wrong for a button, which would come out ochre next to an .sv-btn--primary
   on the same page. One rule puts filled controls back onto the fill token.
   ------------------------------------------------------------------------ */

[data-theme='light'] .mud-button-filled.mud-button-filled-primary,
[data-theme='light'] .mud-fab.mud-fab-primary {
  background-color: var(--sv-accent-fill);
  color: var(--sv-on-accent);
}

/* ── WHY A CONTAINER'S STEP RULE IS WRITTEN LONGER THAN IT NEEDS TO BE ─────
   Read this before shortening one. MudBlazor states an input's density and the
   room its floating label needs as MARGIN on the control, in selectors carrying
   up to three classes: `.mud-input-control.mud-input-control-margin-dense
   .mud-input-outlined-with-label` is eight pixels above and four below. A
   container's `> * + *` weighs one class - `*` and `+` weigh nothing - so it
   loses to every one of them, and a container that states the step would state
   it for a paragraph and silently not for the field beside it.

   Every step rule over a container that holds MudBlazor controls therefore
   names an ancestor as well, and is written `> :not(:first-child)` rather than
   `> * + *` so that the two selectors weigh the same and ours wins on order -
   every stylesheet here is linked after MudBlazor's. The four containers whose
   children are the product's own elements keep the short form.
   ------------------------------------------------------------------------ */

[data-theme='light'] .mud-button-filled.mud-button-filled-primary:hover,
[data-theme='light'] .mud-fab.mud-fab-primary:hover {
  background-color: var(--sv-accent-fill-hover);
}

/* SIX SCREENS SET THIS, so it is here rather than in the stylesheet of the
   first one that needed it. Not .sv-tabs, which is TabStrip: this is what a
   MudTabs needs to sit correctly in this product's spacing, and every MudTabs
   in the product wears it - the one that did not drew its tabs hard against
   the block above them.
   */
.sv-mud-tabs {
  margin-top: var(--sv-space-2);
}

/* MudBlazor draws the tab row's rule as a border on the header, and the panel
   below it carries no padding of its own - so the first line of a panel that
   opens with prose is struck through by that rule. The gap belongs to the
   panel rather than to every first child, or the next panel written here has
   to remember it. */
.sv-mud-tabs .mud-tabs-panels {
  padding-top: var(--sv-space-3);
}

/* AND THE STEP FROM ONE BLOCK OF A PANEL TO THE NEXT, because a tab body is a column of blocks
   exactly as a card is: a field, a notice, a table, the row that acts on it. Its five callers each
   stated it per block in MudBlazor's own spacing names, and the panels that stated nothing left
   consecutive fields butted together.

   Against the panel rather than the container, so the first block sits on the padding above and
   nothing else has to remember it. MudBlazor renders the open panel as `display: contents`, which
   changes what lays the children out but not which element they are children of. */
.sv-mud-tabs .mud-tab-panel > * + * {
  margin-top: var(--sv-space-4);
}

/* A section label stands clear of the block above it and hugs the one it introduces, which is what
   .sv-label--section states for every other container. Restated here because the rule above names
   two classes and so outweighs it - see "MudBlazor reconciliation" for why it has to. */
.sv-mud-tabs .mud-tab-panel > .sv-label--section:not(:first-child) {
  margin-top: var(--sv-space-5);
}

.sv-mud-tabs .mud-tab-panel > .sv-label--section + * {
  margin-top: 0;
}

/* ── Blazor framework surfaces ────────────────────────────────────────────── */

#blazor-error-ui {
  display: none;
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 1200;
  padding: var(--sv-space-3) var(--sv-space-5);
  background: var(--sv-bg-overlay);
  border-top: 1px solid var(--sv-accent);
  color: var(--sv-text);
  font: 400 13px/1.45 var(--sv-font-sans);
}

#blazor-error-ui .dismiss {
  cursor: pointer;
  float: right;
  color: var(--sv-accent);
}

.blazor-error-boundary {
  padding: var(--sv-space-4);
  border: 1px solid var(--sv-border);
  border-radius: var(--sv-radius-card);
  background: var(--sv-bg-overlay);
  color: var(--sv-text-secondary);
}

/* ── Charts ────────────────────────────────────────────────────────────────
   Modelled curves on the component detail pages.

   Every colour is a token. The data line takes --sv-module-edge rather than
   --sv-module: the module tokens are tuned for drawing on satellite imagery, so
   --sv-module gets DARKER in dark mode and would sink into the page. The edge
   token is the one that stays legible against the surface in both themes.

   The maximum-power marker is the one place amber appears. It is a shape, so it
   takes --sv-accent-fill; its label is text, so it takes --sv-accent, which
   darkens to #B8791A on paper where the fill amber is only 2:1 against white.
   ──────────────────────────────────────────────────────────────────────── */

.sv-chart {
  margin: 0;
}

.sv-chart__title {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sv-space-3);
  margin-bottom: var(--sv-space-2);
}

.sv-chart__svg {
  display: block;
  width: 100%;
  height: auto;
}

/* A PLOT IS BOUNDED AND DOES NOT GROW WITH THE CARD AROUND IT. A bar chart whose height follows its
   container is a different picture on every screen it is drawn on - shallow in a panel beside a list
   and half a screen tall on a full-width card - and the reader comparing December with June is
   reading a shape, not a number.

   `max-height` with `preserveAspectRatio="…meet"` rather than a stated height with `none`: stretched
   to a stated height the month letters and the value labels stretch with it, and a distorted glyph
   on a chart reads as a broken font.

   THE BOUND IS WHAT SIZES THE CHART ON A FULL-WIDTH CARD, because `meet` scales to the smaller of
   the two fits: at 200px a 640-unit chart on a 1400px card drew at its natural width and stood in
   half a card of white, which is what a reader files as "this needs to be bigger". 300px is the
   same picture one size up and still one number for every screen; the gutter either side is what it
   costs and it is much smaller than the picture. */
.sv-chart__svg--bars {
  max-height: 300px;
}

/* Recessive: the grid orients, it does not compete with the data. */
.sv-chart__grid line {
  stroke: var(--sv-border-subtle);
  stroke-width: 1;
}

.sv-chart__axis line {
  stroke: var(--sv-border);
  stroke-width: 1;
}

/* Every number on a chart is a measured value, so it is set in mono. */
.sv-chart__tick-text text {
  font: 400 10px/1 var(--sv-font-mono);
  fill: var(--sv-text-tertiary);
}

.sv-chart__line {
  fill: none;
  stroke: var(--sv-module-edge);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.sv-chart__marker-ring {
  fill: var(--sv-bg-raised);
}

.sv-chart__marker {
  fill: var(--sv-accent-fill);
}

.sv-chart__marker-label {
  font: 500 10px/1 var(--sv-font-mono);
  letter-spacing: 0.04em;
  fill: var(--sv-accent);
}

/* ── What a second series is called ───────────────────────────────
   A CHART WITH ONE SERIES NEEDS NO LEGEND AND A CHART WITH TWO CANNOT DO WITHOUT ONE. The charting
   rules permit a second series only where something says which is which, and a stacked bar is the
   case that hides it best: four tones in one column read as one bar with texture until a reader
   tries to name the middle band.

   DIRECT LABELS WHERE THERE IS ROOM, THIS WHERE THERE IS NOT. Twelve stacked columns have nowhere to
   put four labels, so the naming goes under the plot in reading order - and in the SAME order the
   series are stacked, because a legend that lists them the other way up is worse than none.

   NO INTERACTION. Nothing here toggles a series: this renders on an Interactive Server circuit and a
   legend that hides a band is a round trip and a re-render for a glance.
   ───────────────────────────────────────────────────────────────────────── */
.sv-chart__legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sv-space-2) var(--sv-space-4);
  margin: var(--sv-space-2) 0 0;
  padding: 0;
  list-style: none;
}

.sv-chart__legend-item {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font: 400 11px/1.4 var(--sv-font-sans);
  color: var(--sv-text-tertiary);
}

/* The caller sets the fill, because the series colours are the chart's and this file has no way to
   know how many there are. Everything else about the mark is stated once, here. */
.sv-chart__swatch {
  width: 9px;
  height: 9px;
  border-radius: 2px;
  background: var(--sv-chart-bar);
  flex: 0 0 auto;
}

.sv-chart__axis-labels {
  display: flex;
  justify-content: space-between;
  margin-top: var(--sv-space-1);
}

/* Curves and their table read as one unit on a wide screen and stack on a narrow one. */
/* THE BAR CHARTS' OWN MARKS, which is the second half of one vocabulary rather
   than a second one: this section and the curves above it were both headed
   "Charts" and both declared .sv-chart and .sv-chart__svg. */
.sv-chart__axis {
  stroke: var(--sv-border);
  stroke-width: 1;
}

/* The module EDGE token, never the fill. The fill is tuned for drawing on
   satellite imagery, so it gets DARKER in dark mode and sinks into the page;
   the edge token is the one that stays legible against the surface in both. */
.sv-chart__bar {
  fill: var(--sv-module-edge);
}

/* EVERY LABEL ON A CHART IS READ OFF ONE SCALE, and it is the 10px mono the gridline ticks already
   use. A value over a bar at 9px beside an axis tick at 10px is two sizes for one kind of reading,
   and on a chart there is nothing else on the surface to measure either against. */
.sv-chart__value {
  font: 500 10px/1 var(--sv-font-mono);
  fill: var(--sv-text-secondary);
}

.sv-chart__tick {
  font: 400 10px/1 var(--sv-font-mono);
  fill: var(--sv-text-tertiary);
}

/* A second quantity beneath the month letters. Tertiary and a size down from the tick row: it is
   the axis's company, not a second headline competing with the figures over the bars. */
.sv-chart__second {
  font: 400 9px/1 var(--sv-font-mono);
  fill: var(--sv-text-tertiary);
}

.sv-chart__unit {
  margin: var(--sv-space-1) 0 0;
  text-align: right;
}

.sv-chart-pair {
  display: grid;
  gap: var(--sv-space-5);
  grid-template-columns: 1fr;
}

@media (min-width: 1100px) {
  .sv-chart-pair {
    grid-template-columns: 1fr 1fr;
  }
}

.sv-spec-grid {
  display: grid;
  gap: var(--sv-space-3) var(--sv-space-5);
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}

.sv-spec {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-bottom: var(--sv-space-2);
  border-bottom: 1px solid var(--sv-border-subtle);
}

.sv-spec__value {
  font: 500 15px/1.2 var(--sv-font-mono);
  color: var(--sv-text);
}

/* ── Measured values ──────────────────────────────────────────────────────
   Every measured quantity is set in mono: kWh, kWp, %, coordinates, tariffs,
   IDs, timestamps. Prose and headings stay in the sans face. This is the most
   load-bearing typographic rule in the system, so it gets a class of its own
   rather than being applied ad hoc.
   ------------------------------------------------------------------------- */

.sv-measured {
  font: 500 13px/1.3 var(--sv-font-mono);
  color: var(--sv-text);
  font-variant-numeric: tabular-nums;
}

.sv-measured--lg {
  font-size: 20px;
  font-weight: 600;
}

/* ══════════════════════════════════════════════════════════════════════════
   RESULTS - the reports matrix, the result detail, the shade report and the
   printable production report.
   ══════════════════════════════════════════════════════════════════════════ */

/* NO PAGE WELL OF ITS OWN, AND `.sv-result` IS GONE WITH THE ONE THAT WAS. A result page's blocks
   are direct children of `.sv-pagehead__body`, which states the gutter and the step between them
   once - the arrangement Storage and Compliance already draw. The wrapper that was here stated a
   padding of its own, so every card sat in from the title above it, and a `max-width` on it
   stranded a fifth of a wide screen beside a matrix with columns to spend it on. `--sv-well-gutter`
   is the well's only horizontal measurement and nothing is capped or centred. */

/* ── Result detail ─────────────────────────────────────────────────────── */

.sv-result__kpis {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--sv-space-3);
  margin-bottom: var(--sv-space-5);
}

/* THE PANEL AND ITS TITLE ARE GONE. Every result surface is a `.sv-card--flush` with a `CardHeader`
   band, so one of the four section-container vocabularies `STYLES.md` named has closed - and with it
   a second heading face that sat one step from `.sv-h3` and answered to nothing.

   WHAT SURVIVES IS THE SENTENCE those panels carried: the line that qualifies the figures under a
   heading. It is a face rather than a container, a card body still wants one, and it is capped to a
   measure because prose set across a whole card loses the start of its next line. */
.sv-result__panel-note {
  margin: 0 0 var(--sv-space-4);
  font: 400 12px/1.6 var(--sv-font-sans);
  color: var(--sv-text-secondary);
  max-width: 72ch;
}

.sv-result__split {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
  gap: var(--sv-space-5);
}

.sv-result__provenance {
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--sv-text-tertiary);
  padding-top: var(--sv-space-4);
  border-top: 1px solid var(--sv-border-subtle);
}

.sv-table__numeric {
  text-align: right;
}

.sv-table__row--marked td {
  background: var(--sv-bg-sunken);
  font-weight: 600;
}

/* A footer row that restates the body as one figure. Ruled off above rather than shaded, so it
   reads as a summary of the rows and not as one more of them. */
.sv-table__row--total td {
  border-top: 2px solid var(--sv-border);
  font-weight: 600;
}

/* ══════════════════════════════════════════════════════════════════════════
   THE PRINTABLE REPORT

   A4 pages on screen, real pages on paper. The shell stamps data-theme="light"
   on itself because this is the artifact that leaves the building: a dark
   report costs a customer a toner cartridge, and the charts are tuned for it.
   ══════════════════════════════════════════════════════════════════════════ */

.sv-report-shell {
  min-height: 100vh;
  background: var(--sv-bg-sunken);
}

.sv-print {
  padding: var(--sv-space-5);
}

/* A SHARED COMPONENT BRINGS ITS SCROLL WRAPPER ONTO THE PAPER WITH IT.
   The waterfall and the clipping breakdown draw the results screen and this sheet from one
   component, so wrapping their tables - which the screen needs, for the border, the radius and the
   sticky header's scroll context - put a bordered, rounded box round two tables on a printed page
   whose other seven have none. Two tables two pixels narrower than their neighbours and boxed
   differently is exactly the kind of thing a reader notices and nobody can explain.
   Neutralised here rather than by dropping the wrapper, because on screen it is doing real work.
   Not @media print: this surface is read on screen before it is ever printed, and it has to look
   like the sheet it will become. */
.sv-print .sv-table-wrap {
  overflow-x: visible;
  border: 0;
  border-radius: 0;
}

.sv-print__toolbar {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--sv-space-2);
  max-width: 210mm;
  margin: 0 auto var(--sv-space-4);
}

.sv-print__toolbar-alert {
  max-width: 210mm;
  margin: 0 auto var(--sv-space-4);
}

.sv-page {
  width: 210mm;
  min-height: 297mm;
  box-sizing: border-box;
  margin: 0 auto var(--sv-space-5);
  padding: 18mm 16mm 22mm;
  background: var(--sv-bg-raised);
  border: 1px solid var(--sv-border);
  position: relative;
}

.sv-sheet__masthead {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sv-space-4);
  padding-bottom: var(--sv-space-4);
  border-bottom: 2px solid var(--sv-border);
  margin-bottom: var(--sv-space-5);
}

.sv-sheet__title {
  margin: 2px 0 0;
  font: 600 22px/1.2 var(--sv-font-sans);
  color: var(--sv-text);
}

.sv-sheet__subtitle {
  margin: 4px 0 0;
  font: 400 12px/1.4 var(--sv-font-sans);
  color: var(--sv-text-secondary);
}

.sv-page__kpis {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--sv-space-3);
  margin-bottom: var(--sv-space-5);
}

.sv-page__kpi {
  display: block;
  margin-top: 2px;
  font: 600 18px/1.2 var(--sv-font-mono);
  color: var(--sv-text);
}

/* ── What stands with a table ─────────────────────────────────────────────────
   A COUNT ABOVE THE LIST IT COUNTS, AND A NOTE BELOW THE LIST IT IS ABOUT. Both
   are statements about the table beside them rather than blocks of the page, so
   the table's own wrapper states the step - six screens each stated it in their
   markup, at one value above and another below, and a seventh forgot to.
   ------------------------------------------------------------------------ */
.sv-row + .sv-table-wrap {
  margin-top: var(--sv-space-3);
}

.sv-table-wrap + p {
  margin-top: var(--sv-space-2);
}

.sv-page__section {
  margin-bottom: var(--sv-space-5);
}

.sv-page__section-title {
  margin: 0 0 var(--sv-space-3);
  font: 600 14px/1.3 var(--sv-font-sans);
  color: var(--sv-text);
  padding-bottom: 4px;
  border-bottom: 1px solid var(--sv-border-subtle);
}

/*
 * A TABLE ON A SHEET HAS A HARD EDGE, and .sv-table was written for a screen that has not.
 *
 * The screen table pays 24px of padding per cell and refuses to wrap a heading, which is right
 * where the container scrolls or the browser is wider than the content. A sheet is 210mm with
 * 16mm margins - 178mm, and nothing beyond it exists. The field-segment table wants nine columns
 * in that width, and at the screen's padding it measured 700px inside a 670px sheet: the module
 * count and the power ran off the right-hand edge of the paper, on every report, silently.
 *
 * Two causes, both fixed here rather than by dropping a column. `white-space: nowrap` on the
 * headings made "Row spacing" claim 101px for a phrase that reads perfectly well over two lines;
 * and the padding is a screen's, where the cost of a wide row is a scrollbar rather than a lost
 * column. Scoped to .sv-page so no screen table is touched.
 */
.sv-page .sv-table {
  font-size: 12px;
}

.sv-page .sv-table thead th {
  padding: 6px 7px;
  white-space: normal;
}

.sv-page .sv-table tbody td {
  padding: 6px 7px;
}

.sv-page__facts {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--sv-space-3);
  margin: 0;
}

.sv-page__facts dt {
  font: 400 10px/1.3 var(--sv-font-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--sv-text-tertiary);
  margin-bottom: 2px;
}

.sv-page__facts dd {
  margin: 0;
  font: 500 13px/1.3 var(--sv-font-sans);
  color: var(--sv-text);
}

.sv-page__note {
  margin: var(--sv-space-2) 0 0;
  font: 400 11px/1.6 var(--sv-font-sans);
  color: var(--sv-text-secondary);
}

.sv-page__sub {
  display: block;
  font: 400 10px/1.3 var(--sv-font-mono);
  color: var(--sv-text-tertiary);
}

.sv-page__footer {
  position: absolute;
  left: 16mm;
  right: 16mm;
  bottom: 12mm;
  padding-top: 6px;
  border-top: 1px solid var(--sv-border-subtle);
  font-size: 8px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--sv-text-tertiary);
}

@media print {
  /* Chrome that must never reach paper. */
  .sv-print__toolbar,
  .sv-print__toolbar-alert {
    display: none;
  }

  .sv-report-shell,
  .sv-print {
    /* True white rather than the warm paper token, and deliberately a literal. This
       is ink on paper and the paper is already white; printing #FCFBF9 lays a
       full-bleed tint across every sheet, which costs a customer a cartridge to
       reproduce a colour they are holding in their hand. */
    background: #fff;
    padding: 0;
  }

  .sv-page {
    width: auto;
    min-height: auto;
    margin: 0;
    padding: 14mm 12mm 18mm;
    border: none;

    /* One article, one sheet. Without this the browser flows them together and
       the footers land mid-page, which is how a five-page report prints as four
       and a half with the page numbers in the wrong places. */
    break-after: page;
    page-break-after: always;
  }

  .sv-page:last-child {
    break-after: auto;
    page-break-after: auto;
  }

  /* A table split across a page boundary loses its heading, so the reader gets
     a column of numbers with nothing saying what they are. */
  .sv-table {
    break-inside: avoid;
    page-break-inside: avoid;
  }

  .sv-page__footer {
    position: fixed;
    bottom: 6mm;
  }
}

/* ── A list beside a panel ────────────────────────────────────────────────────
   THE SHELL SEVEN SCREENS WEAR: condition sets, storage models, financial models,
   schedules, compliance, the application pack and a proposal's frozen figures.

   A WRAPPING FLEX ROW, NEVER A GRID WITH A FIXED TRACK. A fixed column beside `1fr` gives the
   editor whatever is left of the row, and an editor holding a four-up field grid then asks for more
   width than the card around it and is clipped by that card's own `overflow: hidden`. A wrapping
   row lets the two negotiate, and stacks the list above the editor rather than squeezing both.

   THE PANEL GIVES WAY FIRST AND STOPS GROWING. It holds names and two short lines under each, so
   past about 340px it is a column of whitespace; below 240px a name wraps every row. The editor
   takes the rest and states `min-width: 0`, because a flex item's floor is its own content.

   NOT `position: sticky` ON THE LIST. It is right beside an editor and wrong the moment the row
   wraps, where the list is above rather than alongside and a sticky block pins it over the form
   somebody is filling in.

   NAMED FOR THE SHAPE, NOT FOR THE FIRST SCREEN THAT NEEDED IT. Under a screen's own name this was
   seven screens wearing one page's family, so a reader improving the list thought they were editing
   that page and the screen beside it kept a whole second copy of the shell rather than join it.
   ------------------------------------------------------------------------ */

.sv-workbench {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--sv-space-4);
}

/* A page that uses the shell and has a detail with no list beside it. Wrapped rather than stacked,
   its panels are dealt onto one line and read as a broken layout rather than as a layout choice.
   Compliance, the application pack, a proposal's frozen figures and the branding screen are all
   single-column pages that want the shell's spacing and none of its split. */
.sv-workbench--stacked {
  flex-direction: column;
  align-items: stretch;

  /* THE WRAP HAS TO GO WITH THE DIRECTION, and leaving it on is what put a panel's worth of dead
     white under every one of these screens. `.sv-workbench` wraps because its two columns must drop
     onto one another on a narrow window; turned on its side, that same wrap lays the panels into
     flex LINES down the main axis, and a line stretches to fill the container it is in - so each
     panel came out as tall as the space beside it rather than as tall as its own content. Measured
     on the branding screen: 677px of panel around 358px of form, with 296px of nothing under the
     last field. `nowrap` alone takes it to 382px, which is the content plus its padding.

     It costs nothing here: a column that never has a second column has nothing to wrap. */
  flex-wrap: nowrap;
}

/* A child that stands alone rather than beside the list - an alert about the page, an empty state,
   a progress bar. Without it a centred message is centred on a 272px column nobody can see. */
.sv-workbench__full {
  flex: 1 1 100%;
}

/* THE SIDE COLUMN, and only its width. What stacks inside it is `__rows`, because on one screen the
   list is a card with a band and a footer and on the next it is the rows alone. */
.sv-workbench__list {
  flex: 1 1 272px;
  max-width: 340px;
  min-width: 240px;
}

/* THE ROWS, in a card or straight on the well. The 6px inset is what stands a row's hover fill and
   its selected border off the edge of whatever holds them. */
.sv-workbench__rows {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px;
}

/* The label above the list, which is a heading over a block rather than a chip beside something, so
   it takes the section face. Stated here rather than per screen: three of the seven wrote one and
   the rest opened with an unlabelled column of rows. */
.sv-workbench__rows > .sv-label {
  margin-bottom: var(--sv-space-1);
}

/* ONE ROW: A NAME, WHAT IT SCORED, AND ONE OR TWO LINES SAYING WHAT IT ASSUMED. Rows rather than
   bordered cards in a stack - it is the shape the sidebar's own navigation uses, and it takes the
   sidebar's active treatment for the same reason: the row somebody selected on the left and the
   head of what they selected on the right are then the same material.

   An explicit track, because this is a <button>. A button centres its own content box, so a grid
   with no stated template sizes its column to max-content and centres it, which left every row in
   the list starting at a different x decided by the length of its own longest line.
   minmax(0, 1fr) fills the button instead, so text-align below has a full-width box to align in,
   and a name longer than the row wraps rather than pushing the column wider. */
.sv-workbench__item {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  justify-content: start;
  gap: 3px;
  width: 100%;
  text-align: left;
  padding: 9px 11px;
  border: 1px solid transparent;
  border-radius: var(--sv-radius-item);
  background: none;
  color: var(--sv-text);
  font: inherit;
  cursor: pointer;
  transition: background-color 120ms ease, border-color 120ms ease;
}

.sv-workbench__item:hover {
  background: var(--sv-bg-overlay);
  border-color: var(--sv-border-subtle);
}

/* THE SELECTED ROW IS THE ONE THING IN THE LIST ALLOWED THE ACCENT, and it is the sidebar's active
   row exactly. Two treatments for one meaning is how a reader learns that selection looks like one
   thing here and another thing one panel away. The border takes the FILL rather than the text
   accent, because a border is a shape: on paper the two are an ochre against an amber and read as
   one of them being stale. */
.sv-workbench__item--selected,
.sv-workbench__item--selected:hover {
  background: var(--sv-accent-wash);
  border-color: var(--sv-accent-fill);
  color: var(--sv-text);
}

/* The name and what the row scored, on one baseline. */
.sv-workbench__item-head {
  display: flex;
  align-items: baseline;
  gap: var(--sv-space-2);
}

.sv-workbench__item-name {
  font: 600 13.5px/1.35 var(--sv-font-sans);
  letter-spacing: -0.005em;
  min-width: 0;
  overflow-wrap: anywhere;
}

/* WHAT THE ROW SCORED, AT THE END OF ITS OWN LINE. It is the one figure a reader compares down the
   column, so it is mono and right-aligned. */
.sv-workbench__item-score {
  margin-left: auto;
  font: 400 10px/1.4 var(--sv-font-mono);
  letter-spacing: 0.08em;
  color: var(--sv-text-secondary);
  white-space: nowrap;
}

.sv-workbench__item--selected .sv-workbench__item-score {
  color: var(--sv-accent);
}

/* A ROW STATES ITS OWN RESULT AND NEVER A BARE ZERO. A row nobody has computed says so in words:
   a dash in a column of percentages reads as nought per cent, which is a result rather than the
   absence of one. The face is the disabled tier, which is what that tier is for. */
.sv-workbench__item-score--none {
  color: var(--sv-text-disabled);
}

.sv-workbench__item-meta {
  font: 400 11.5px/1.4 var(--sv-font-sans);
  color: var(--sv-text-secondary);
}

/* THE WAY TO ADD ONE, AT THE END OF THE LIST IT ADDS TO. It is a control and stays an `.sv-btn` -
   it writes a row - but a bordered button in a card's footer reads as the card's own action rather
   than as the next line of the list, so it takes the accent and the borderless face. */
.sv-workbench__add {
  padding-left: 0;
  padding-right: 0;
  color: var(--sv-accent);
  font-size: 12px;
}

.sv-workbench__add:hover:not(:disabled) {
  background: none;
  color: var(--sv-accent-hover);
}

.sv-workbench__panel {
  flex: 5 1 520px;
  min-width: 0;

  /* A flex item's floor is its own min-content width. Overflow-x here is what makes the wide tables
     inside scroll in their own containers rather than widening the page. */
  overflow-x: hidden;
}

/* The step from one block of it to the next, as .sv-card states it. Seven
   screens wear this shell and none of them stated it: the financial model set
   it on one alert, and the schedules page stacked three panels with nothing
   between them at all. */
.sv-workbench__panel > * + * {
  margin-top: var(--sv-space-4);
}

/* The tab strip inside a panel is six tabs wide and the panel is not. It scrolls inside itself
   rather than widening the page - the rule wide content follows everywhere here. */
.sv-workbench__panel .mud-tabs-toolbar-wrapper,
.sv-workbench__panel .mud-tabs-tabbar-inner {
  max-width: 100%;
  overflow-x: auto;
}

/* A form laid out two-up on a wide panel and one-up on a narrow one.
   minmax(0, 1fr) on the track, because a grid item's default minimum size is its
   MIN-CONTENT: without it a long select label pushes the whole panel past the page
   edge rather than wrapping inside it. */
.sv-form-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sv-space-4);
  margin-bottom: var(--sv-space-4);
}

@media (min-width: 900px) {
  .sv-form-grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }
}

/* Inside a form the column gap already owns the spacing between rows, so the grid's own trailing
   margin lands on top of it and one two-up row sits twice as far from its neighbours as every
   one-up row does. */
.sv-form > .sv-form-grid {
  margin-bottom: 0;
}

/* A stated assumption or an outstanding item - never an error, so it takes the
   sunken surface and a border rather than a colour. */
.sv-notice {
  background: var(--sv-bg-sunken);
  border: 1px solid var(--sv-border-subtle);
  border-radius: var(--sv-radius-card);
  padding: var(--sv-space-4);
  margin-bottom: var(--sv-space-4);
}

/* Nothing follows, so there is no gap before it - see .sv-alert:last-child, which is the same rule. */
.sv-notice:last-child {
  margin-bottom: 0;
}

.sv-notice ul {
  margin: var(--sv-space-2) 0 0;
  padding-left: 1.1rem;
}

.sv-notice li,
.sv-notice p {
  font: 400 12px/1.7 var(--sv-font-sans);
  color: var(--sv-text-secondary);
  margin: 0;
}

/* THE STEP BETWEEN THE BLOCKS A NOTICE HOLDS, stated by the notice because that is where the rule
   lives everywhere else in the product. Its own paragraphs give up their margins just above, so a
   notice carrying more than one block drew them with no gap at all: the share dialog's issued link
   put a label, the URL and the sentence explaining why it is shown once into a single run of text
   with nothing separating them. One block is unaffected - there is no adjacent sibling to step
   away from.

   THE CHILD IS NAMED RATHER THAN LEFT AS `*`, and that is not tidiness. `.sv-notice > * + *` is one
   class and two universals, which loses on specificity to the `.sv-notice p` above it whatever the
   order - so the rule renders as nothing, with nothing reporting it. `:is()` takes the weight of
   its heaviest argument, so naming the elements matches that rule's weight and the later one wins.
   `.sv-notice--scenario p` is heavier still and keeps its own tighter step. */
.sv-notice > * + :is(p, ul, ol, div) {
  margin-top: var(--sv-space-2);
}

/* Something needing attention. Amber on the left edge only: the accent is the
   product's one chromatic colour, and filling a panel with it would read as an
   error state rather than as a thing to look at. */
.sv-notice--warning {
  border-left: 3px solid var(--sv-accent-fill);
}

/* The scenario an autonomy figure was computed against. It sits directly above
   the figure because an autonomy number without its conditions beside it says
   nothing at all. */
.sv-notice--scenario p {
  font: 400 16px/1.4 var(--sv-font-mono);
  color: var(--sv-text);
  margin-top: var(--sv-space-1);
}

/* ── The interruption calendar ────────────────────────────────────────────── */

/* Wide by construction - thirty-one days across - so it scrolls inside its own
   container rather than taking the panel with it.

   max-width is the NATURAL width of the viewBox, and that is the fix rather than
   a preference. An SVG at width: 100% inside a 1300px panel scaled a 592-wide
   grid by 2.2, which turned an 18px cell into a 40px block with a 22px hole
   beside it: the picture stopped reading as a calendar and started reading as a
   pattern of orange rectangles, which is precisely how it was reported. Drawn at
   its own size the day columns touch and a month of interruptions is legible. */
.sv-chart--calendar {
  display: block;
  width: 100%;
  min-width: 600px;
  max-width: 592px;
  height: auto;
}

/* The grid is 600px at its narrowest, so on a narrow panel it scrolls in its own
   box rather than pushing the page sideways. */
.sv-chart__label {
  font: 400 9px/1 var(--sv-font-mono);
  fill: var(--sv-text-secondary);
}

/* Amber AS TEXT, which is what the accent token is for. --sv-accent-fill is
   ~2.1:1 on paper and fails AA as text; --sv-accent is the one that resolves per
   mode and stays legible. */
.sv-emphasis {
  color: var(--sv-accent);
}

/* ── Shared bits the proposal screens needed ──────────────────────────────── */

.sv-input--area {
  min-height: 72px;
  resize: vertical;
  font-family: var(--sv-font-sans);
}

.sv-input--colour {
  width: 48px;
  min-width: 48px;
  padding: 2px;
  height: 34px;
}

/* Always worn with .sv-panel, so this radius OVERRIDES the panel's own - which is
   why it is the card one and not something new: a box sunk inside a panel reads as
   nested when its corners are tighter than the panel's, and identical when they
   are not. */
.sv-panel--inset {
  background: var(--sv-bg-sunken);
  border: 1px solid var(--sv-border-subtle);
  border-radius: var(--sv-radius-card);
  padding: var(--sv-space-3);
}

/* THE BORDERLESS GLYPH A TABLE ROW USES, and it is a control with a hit area rather than a
   character sitting in a cell. A 26px square at the item radius: three of these at the end of a
   dense row are one group belonging to the row, and at 2px of padding they were three targets a
   pointer had to be aimed at.

   IT RESTS ON THE QUIETEST TEXT TIER AND DECLARES ITSELF UNDER THE POINTER. An icon at full ink on
   every row of a list competes with the row it acts on; the hover is the band fill, which is the
   same surface a row's own hover takes. Not the accent: the accent is what the SELECTED row wears,
   and a glyph that turns amber on hover says the row has been chosen.

   .sv-btn--icon is NOT this and stays as it is: it is the bordered square an action ROW uses, where
   there is a bordered control beside it for it to agree with. The two are told apart by whether
   the thing next to them has a border. */
.sv-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  border-radius: var(--sv-radius-item);
  background: transparent;
  color: var(--sv-text-quiet);
  cursor: pointer;
  line-height: 0;
  transition: background-color 120ms ease, color 120ms ease;
}

.sv-icon-btn:hover {
  background: var(--sv-bg-overlay);
  color: var(--sv-text);
}

.sv-icon-btn:disabled {
  background: transparent;
  color: var(--sv-text-quiet);
  cursor: not-allowed;
  opacity: 0.5;
}

/* THE ONE DESTRUCTIVE ICON IN A ROW OF THEM. An icon carries no word to warn with, so the colour
   is the only thing separating Delete from Clone before the pointer reaches it - the same
   separation .sv-btn--danger makes among labelled buttons, and --sv-danger-ink is the token it
   makes it with. It rests in the row's own ink and only declares itself under the pointer, because
   a red glyph on every row of a list reads as a list of errors. */
.sv-icon-btn--danger:hover {
  background: color-mix(in srgb, var(--sv-danger) 12%, transparent);
  color: var(--sv-danger-ink);
}

/* THE ROW'S OWN NAME, WHICH IS WHAT THE READER CAME FOR. A list's first real column carries the
   thing being listed and a link to it, and two screens now set it identically - the project on the
   dashboard and the document on a project. Written twice it was two faces one edit apart.

   IT WRAPS, ALONE AMONG THE CELLS OF A FIXED TABLE. Everything beside it clips, because a clipped
   site name or timestamp costs nothing a reader needs; a clipped name is the row unidentified. */
.sv-table__name {
  display: block;
  font: 600 13.5px/1.35 var(--sv-font-sans);
  letter-spacing: -0.005em;
  overflow-wrap: anywhere;
}

.sv-table__meta {
  display: block;
  font: 400 11px/1.4 var(--sv-font-sans);
  color: var(--sv-text-tertiary);
}

.sv-table__wide {
  max-width: 380px;
}

/* The gap is a rule, not a whitespace accident. Buttons in a cell are separated only by the
   collapsed space between two inline-blocks - about four pixels - and one of these is usually a
   destructive control sitting against a harmless one. */
.sv-table__actions {
  white-space: nowrap;
  text-align: right;
}

.sv-table__actions > * + * {
  margin-left: var(--sv-space-2);
}

.sv-list {
  margin: var(--sv-space-2) 0 0;
  padding-left: var(--sv-space-4);
  color: var(--sv-text-secondary);
  font: 400 13px/1.6 var(--sv-font-sans);
}

/* ── Tablet and below ─────────────────────────────────────────────────────────
   What the utilities do on a narrow screen. The dashboard, the results matrix
   and the production report are readable on a tablet on site; the designer is
   not, because a three-pane canvas with a map in the middle is a desk
   instrument and pretending otherwise ships a version of it nobody can draw on.

   Everything here is a max-width query, so the desktop layout is untouched: a
   narrow screen SUBTRACTS from it rather than the wide one being built up from
   a phone. The shell's own narrow-screen rules are app-shell.css's.
   ------------------------------------------------------------------------ */

@media (max-width: 1000px) {
  /* TWO KPI COLUMNS RATHER THAN FOUR. One is a column of enormous numbers a
     reader has to scroll past; four does not fit. */
  .sv-grid--kpi {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 700px) {
  .sv-grid--kpi {
    grid-template-columns: minmax(0, 1fr);
  }

  /* Every table in the product scrolls sideways in its own container rather
     than pushing the page past its edge. min-width: 0 down the chain is what
     makes that work: a grid item defaults to its MIN-CONTENT, so a wide table
     inside one widens the whole page instead of scrolling, and a narrow screen
     is where that shows up hardest. */
  .sv-table-wrap {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }

  .sv-table {
    min-width: 640px;
  }
}

/* ── What a narrow screen does with a table ───────────────────────────────────
   IT SCROLLS INSIDE ITS OWN BOX. There is ONE answer, and the vocabulary for hiding a column at a
   width is gone with the second: a table that both drops columns and scrolls scrolls to a width
   whose columns are already hidden, which is a reader dragging sideways to reach nothing.

   A table whose columns matter states `table-layout: fixed` and a `min-width` in its own screen's
   stylesheet, and `.sv-table-wrap`'s `overflow-x` does the rest. `projects.css` is the reference.
   ------------------------------------------------------------------------ */

