/* ============================================================
   CalcHub — Design System
   Farger: #1B3A5C primær · #2E6DA4 sekundær · #E8A020 aksent
   ============================================================ */

/* ── TOKENS ── */
:root {
  --ds-primary:          #1B3A5C;
  --ds-primary-dark:     #132A42;
  --ds-primary-light:    #234B74;
  --ds-secondary:        #2E6DA4;
  --ds-secondary-dark:   #255887;
  --ds-secondary-light:  #3D84BF;
  --ds-accent:           #E8A020;
  --ds-accent-dark:      #C8881A;
  --ds-accent-light:     #F0B840;

  --ds-bg:               #F2F4F7;
  --ds-surface:          #FFFFFF;
  --ds-border:           #D1D5DB;
  /* Was #9CA3AF, which is 2.30:1 on --ds-bg. This is the border of
     .btn-outline-secondary and the stroke of the CR chart axes, so it is a
     control and graphics boundary and WCAG 1.4.11 wants 3:1. Now 3.38:1 on
     the page background and 3.73:1 on a card. */
  --ds-border-strong:    #7C8593;

  --ds-text:             #111827;
  --ds-text-secondary:   #374151;
  /* Was #6B7280, which is 4.39:1 on --ds-bg, under AA for body text. Now
     5.56:1 on the page background and 6.13:1 on a card, and still clearly
     muted against --ds-text-secondary at 9.35:1. */
  --ds-text-muted:       #5B6270;
  --ds-text-on-dark:     #F1F5F9;
  --ds-text-on-accent:   #1A1200;

  /* The status colours are only ever drawn as text or as a border on their
     own tinted background, never as a fill behind white text, so they are
     tuned for legibility as text. --ds-fail was #DC2626 at 4.41:1 on
     --ds-fail-bg, the one status colour under AA, and it is the colour that
     carries "this component does not pass". Now 5.24:1. */
  --ds-pass:             #15803D;
  --ds-pass-bg:          #F0FDF4;
  --ds-fail:             #C81E1E;
  --ds-fail-bg:          #FEF2F2;
  --ds-review:           #B45309;
  --ds-review-bg:        #FFFBEB;
  --ds-info:             #2E6DA4;
  --ds-info-bg:          #EFF6FF;

  /* The keyboard focus ring. It was written out as a raw rgba() in every
     place that needed one, in two different hues, so a focus ring could be
     Bootstrap blue on one control and the app's own blue on the next. */
  --ds-focus-ring:       rgba(46, 109, 164, 0.25);

  --ds-topbar-h:         3.86rem;
  --ds-sidebar-w:        16.57rem;

  --ds-font:             system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --ds-font-mono:        ui-monospace, "Cascadia Code", "Fira Code", monospace;
  /* The face of the figures: the TeX face that static/css/figures.css declares. */
  --ds-font-figure:      "CalcHub Figure TeX", serif;

  /* ── SPACING SCALE ──
     Every padding, margin and gap in this file comes from here, so the
     density of the whole product is one edit. The px notes assume the 14px
     end of the html clamp() below, which is what a 1440px laptop gets.

     Thirteen steps rather than the eight the plan asked for. Eight forced
     1.25rem onto 1.5rem, which moved eleven declarations by 3.5px, and a
     scale that has to shift the layout to be adopted is not the same scale.
     The resolution sits in the small end because that is where a dense
     tool lives. */
  --ds-space-1:          0.125rem;  /*  2px */
  --ds-space-2:          0.25rem;   /*  4px */
  --ds-space-3:          0.375rem;  /*  5px */
  --ds-space-4:          0.5rem;    /*  7px */
  --ds-space-5:          0.625rem;  /*  9px */
  --ds-space-6:          0.75rem;   /* 11px */
  --ds-space-7:          0.875rem;  /* 12px */
  --ds-space-8:          1rem;      /* 14px */
  --ds-space-9:          1.25rem;   /* 18px */
  --ds-space-10:         1.5rem;    /* 21px */
  --ds-space-11:         2rem;      /* 28px */
  --ds-space-12:         2.5rem;    /* 35px */
  --ds-space-13:         3rem;      /* 42px */

  /* ── TYPE SCALE ──
     Replaces 34 distinct font-size values that had no relationship to each
     other. Body text in this product is --ds-type-base, not --ds-type-md:
     an engineer reads tables and traces, not prose, and the app was already
     built at that density. */
  --ds-type-2xs:         0.64rem;   /*  9px, badges and micro labels */
  --ds-type-xs:          0.72rem;   /* 10px, section labels, help text */
  --ds-type-sm:          0.8rem;    /* 11px, dense table and nav text */
  --ds-type-base:        0.88rem;   /* 12px, body */
  --ds-type-md:          1rem;      /* 14px, emphasised body */
  --ds-type-lg:          1.15rem;   /* 16px, card and panel titles */
  --ds-type-xl:          1.35rem;   /* 19px, page titles */
  --ds-type-2xl:         1.55rem;   /* 22px, result headline figures */
  --ds-type-3xl:         1.9rem;    /* 27px, the result page title */

  /* Not a step of the type scale, and deliberately named apart from it: the
     confirmation glyph on the checkout result pages is a graphic that happens
     to be drawn with a font. The scale exists so text has a rhythm; sizing a
     decorative icon by it would either shrink the icon to look like a heading
     or push a display step into the scale that no text would ever use. */
  --ds-icon-hero:        4rem;

  /* ── SURFACES, LINES AND ELEVATION ──
     --ds-surface is a card, --ds-bg the page it sits on, --ds-surface-sunken
     an inset area inside a card (a table head, a chart well). Three levels is
     the whole depth of this product: anything that needs a fourth is really
     asking for a border.

     --ds-border is a control edge, --ds-border-subtle a separator between rows
     of the same card. They were one token, so a hairline between two table
     rows was drawn as heavily as the outline of the input above it. */
  --ds-surface-sunken:   #F6F8FA;
  --ds-border-subtle:    #E4E9EF;

  /* Elevation is a whole token, shadow and all, rather than a colour that each
     rule composes its own blur from. Two steps: resting and lifted. */
  --ds-shadow-1:         0 1px 2px rgba(16, 32, 52, 0.05);
  --ds-shadow-2:         0 4px 16px rgba(16, 32, 52, 0.08);
  --ds-overlay:          rgba(11, 20, 32, 0.45);

  /* ── ROLE TOKENS ──
     The brand colours above say what a colour *is*; these say what it is
     *for*. Only role tokens can be themed: --ds-primary is the navy of the
     topbar and, in eleven other rules, the colour of a heading. One value
     cannot both stay dark behind white chrome text and turn light to read on
     a dark page, so the two uses are now two tokens. Reach for a role token
     in new work; a raw brand token in a rule that paints text or a surface is
     a theming bug waiting for the dark palette to find it. */
  --ds-heading:          #12263F;
  --ds-link:             #2E6DA4;
  --ds-link-hover:       #1B3A5C;
  --ds-schematic-ink:    #1B3A5C;

  /* The topbar and the sidebar. Dark in both palettes, so white sits on them
     in both, which is why they are not --ds-surface with a tint. */
  --ds-chrome-bg:        #1B3A5C;
  --ds-chrome-fg:        #FFFFFF;

  /* Secondary text and hairlines *on* chrome. The login page, the landing
     hero and the chart hover tooltip all sat a dimmed white over a dark
     panel, and each wrote its own rgba(255,255,255,0.x) to do it. These are
     that, once: --ds-chrome-fg-dim for a caption or a label, --ds-chrome-line
     for a divider or a border. Both are alpha over --ds-chrome-bg rather than
     an opaque colour, so they keep working wherever chrome is painted. */
  --ds-chrome-fg-dim:    rgba(241, 245, 249, 0.72);
  --ds-chrome-line:      rgba(255, 255, 255, 0.14);
  --ds-chrome-wash:      rgba(255, 255, 255, 0.08);

  /* A filled navy surface inside the page that carries light text: the dark
     table head, the step-number badge, the cookie bar, .btn-dark-navy. */
  --ds-solid-bg:         #1B3A5C;
  --ds-solid-border:     #132A42;
  --ds-solid-fg:         #FFFFFF;

  /* The foreground of a circle, pill or chip filled with a *status* colour.
     It is not --ds-solid-fg: the status fills are light in the dark palette
     (a mint green reads as "pass" on a dark page where a forest green does
     not), and white on mint is 1.8:1. This token is what flips instead. */
  --ds-on-status:        #FFFFFF;

  /* ── FIGURE PLATE ──
     A dimensioned engineering drawing, reproduced the way the standard prints
     it: black line work on white, with a hatched body and dimension text that
     carries a white halo where it crosses a centreline.

     These stay light in both palettes, like a printed figure pasted onto the
     page, and that is a decision rather than an oversight. Inverting a
     dimensioned drawing does not give you a dark drawing, it gives you one
     whose halos punch black holes through its own leader lines, and the
     halo is what keeps the labels readable.

     They are tokens so the decision is written down once instead of being
     implicit in twenty-eight hex values, and so that a future dark-figure
     treatment is four edits rather than a re-reading of every schematic. */
  --ds-plate-bg:         #FFFFFF;
  --ds-plate-edge:       #D6DAE0;
  --ds-plate-ink:        #000000;
  --ds-plate-body:       #E6E8EB;
  --ds-plate-body-cut:   #DEDEDE;
  --ds-plate-shade:      #7A7A7A;
  --ds-plate-crack:      #DC3545;

  /* ── THE THICKNESS PROFILE CHART ──
     Part 4's critical thickness profile, drawn live from the reader's own
     grid. It is a plate for the same reason the schematics above are: the
     bands encode quantities, the profile line crosses them, and the labels
     carry a white halo where it does. Inverting that gives a chart whose
     halos punch dark holes through its own profile.

     So it sits on --ds-plate-bg in both palettes, and these are its ink. The
     three band colours are the ones a reader matches against the legend, so
     they are named for what they mean rather than for the hue they are. */

  /* ── FIGURE PALETTE ──
     static/css/figures.css draws the schematics the assessment pages include
     from shared/templates/shared/figures/. Its vocabulary used to be written
     out per rule: #111111 appeared thirty-nine times and the CTP band colours
     were a second copy of the --ds-ctp-* set below.

     Like the plate tokens above, these keep one light palette in both themes.
     A dimensioned drawing is black line work on white paper, and inverting it
     makes a section view unreadable rather than dark. The panels the drawings
     sit in follow the theme; the drawings do not. */
  --ds-fig-line:         #111111;  /* the drawing's line work: bodies, welds, dimensions */
  --ds-fig-line-soft:    #333333;  /* a zone boundary */
  --ds-fig-line-faint:   #444444;  /* a centreline */
  --ds-fig-body:         #E6E6E6;  /* the component's wall */
  --ds-fig-body-alt:     #E3E3E3;  /* the same wall on the corner-crack plate */
  --ds-fig-body-light:   #DDDDDD;  /* a pad or a nozzle wall */
  --ds-fig-weld:         #B8B8B8;  /* weld metal, darker than the wall it joins */
  --ds-fig-label:        #596572;  /* a heat-map axis or summary label */
  --ds-fig-frame:        #D5DCE3;  /* the edge of a figure panel */
  --ds-fig-frame-edge:   #D6DDE5;  /* the Figure 5.9 frame */
  --ds-fig-caption:      #6C757D;  /* a figure caption */
  --ds-fig-legend-edge:  #C8D0D8;  /* the edge of a legend bar */
  --ds-fig-swatch-edge:  #7C8791;  /* the edge of a legend swatch */
  --ds-fig-legend-text:  #536679;  /* legend text */
  --ds-fig-component-line: #5F6B76;  /* the component schematic on a result page */
  --ds-fig-component-fill: #D9DEE3;  /* its wall */
  --ds-fig-component-label: #4F5B66;  /* its label */
  --ds-fig-pit-line:     #4B5563;  /* the pit map's area and dimension lines */
  --ds-fig-pit-label:    #374151;  /* its labels and pit ids */
  --ds-fig-pit-select:   #2E6DA4;  /* the selected pit and the focus ring */
  --ds-fig-pit-edge:     #7F1D1D;  /* the edge of a pit */
  --ds-fig-pit-grid:     #E7EBF0;  /* its grid */
  --ds-fig-pit-shell:    #D7DDE5;  /* the shell it is drawn on */
  --ds-fig-pit-inspector: #F2F4F7;  /* the inspector strip under it */
  --ds-fig-pit-note:     #6B7280;  /* its note */
  --ds-fig-pit-grade-1:  #2EAF62;  /* Table 6.2 pit grade, least severe */
  --ds-fig-pit-grade-2:  #FFD034;
  --ds-fig-pit-grade-3:  #FF912C;
  --ds-fig-pit-grade-4:  #EE0000;  /* most severe */
  --ds-fig-guide-accent: #24557A;  /* the unsupported-length guide's dimensions */
  --ds-fig-guide-shell:  #344F63;  /* its shell and ring outlines */
  --ds-fig-guide-shell-fill: #EEF4F8;  /* the shell's fill */
  --ds-fig-guide-ring-fill: #DBE8F1;  /* a stiffening ring's fill */
  --ds-fig-guide-support: #B44B3E;  /* a line of support */
  --ds-fig-guide-tangent: #778894;  /* a tangent line or subdivision */
  --ds-fig-guide-title:  #1E3445;  /* its panel title */
  --ds-fig-guide-label:  #536774;  /* its labels */
  --ds-fig-guide-border: #CCD6DF;  /* the guide's own border */
  --ds-fig-guide-bg:     #F8FAFC;  /* and its background */
  --ds-fig-guide-rule:   #DBE3EA;  /* the rule under its heading */

  --ds-ctp-label:        #40566B;  /* series and point labels */
  --ds-ctp-axis-title:   #354A5F;
  --ds-ctp-tick:         #607286;
  --ds-ctp-empty:        #7B8792;  /* the "no profile yet" line */
  --ds-ctp-axis:         #506579;
  --ds-ctp-gridline:     #DFE7EE;
  --ds-ctp-reference:    #788A9C;  /* t_nom and t_c guides */
  --ds-ctp-remote:       #9AA8B5;  /* the remote-thickness guide */
  --ds-ctp-grid-limit:   #A2B0BD;  /* where the entered grid stops */
  --ds-ctp-profile:      #243746;  /* the measured profile */
  --ds-ctp-corrected:    #725A9C;  /* the same profile less FCA */
  --ds-ctp-window:       #247EB2;  /* the averaging window and its dimensions */
  --ds-ctp-window-mark:  #176C9D;
  --ds-ctp-minimum:      #A92D27;  /* the minimum-average annotation */
  --ds-ctp-deepest:      #D04B43;  /* the guide to the deepest reading */
  --ds-ctp-point:        #E0473E;  /* a measured reading */
  --ds-ctp-point-deep:   #C9362E;  /* the deepest one */
  --ds-ctp-band-loss:    #F2D39B;  /* LOSS, already lost to corrosion */
  --ds-ctp-band-fca:     #9FCFDD;  /* FCA, the future allowance */
  --ds-ctp-band-fca-ml:  #CBB8DD;  /* FCA in the metal-loss region */
  --ds-ctp-band-area:    #9FCCE7;  /* the hatch over the integrated area */
  --ds-ctp-band-area-bg: #EDF7FD;

  /* The inspection-grid heat map's severity ramp, thinnest to thickest. This
     encodes a measurement, so it is one palette in both themes for the same
     reason the CLF sensitivity series is: a reader compares a cell against the
     legend, and a ramp that changed with the theme would make two screenshots
     of one assessment disagree.

     assessment_gml/static/assessment_gml/js/ctp_profile_editor.js interpolates
     between the same four stops to paint the canvas, because interpolation
     needs the channels as numbers. tests/test_theme.py holds the two lists
     together, so neither can drift. */
  --ds-ctp-heat-1:       rgb(239, 34, 34);   /* at the minimum reading */
  --ds-ctp-heat-2:       rgb(245, 139, 18);
  --ds-ctp-heat-3:       rgb(250, 204, 21);
  --ds-ctp-heat-4:       rgb(22, 163, 74);   /* at the scale maximum */

  /* ── THE DAMAGE-SITE PLAN DRAWING ──
     Part 5's other live drawing: the shell in elevation with every damage
     site on it, the governing one picked out and the rest drawn back. It is
     painted three times, by lml_damage_sites.js while the reader edits, by
     assessment_lml/core/figure_5_9_plot.py on the result page, and by the key
     below both, which is here. tests/test_theme.py holds the key to the
     Python, which is the one of the three a report is produced from.

     A plate, for the reason the drawings above are: the reader matches a line
     against a key, and the governing site is told apart from the others by
     colour alone.

     The -other pair is the same series drawn back: a site that is not
     governing is still on the drawing, and has to read as secondary without
     reading as absent. */
  --ds-dsite-support:     #198754;  /* stiffener or support */
  --ds-dsite-ctp:         #176B87;  /* the governing site's thickness profile */
  --ds-dsite-ctp-other:   #98A2AD;
  --ds-dsite-shell:       #E67E22;  /* the governing idealized shell */
  --ds-dsite-shell-wash:  rgb(245 158 11 / 13%);
  --ds-dsite-shell-other: #7C6AA6;  /* every other site's idealized shell */
  --ds-dsite-shell-other-wash: rgb(139 123 181 / 10%);
  --ds-dsite-future:      #C43D3D;  /* the shell after the future allowance */

  /* ── RADIUS SCALE ──
     The product used to be square on purpose. It now has corners, and they
     come from here so that a card, the input inside it and the button under
     it are not three different roundnesses.

     px, not rem, and deliberately: --ds-radius-lg is the physical softness of
     a corner, not a measure of text. Scaling it with the root font would make
     a card on a 27" display look like a pill. */
  --ds-radius-xs:        4px;   /* chips, swatches, inline code */
  --ds-radius-sm:        6px;   /* inputs, selects, small buttons */
  --ds-radius-md:        8px;   /* buttons, alerts, badges */
  --ds-radius-lg:        12px;  /* cards and panels, the default */
  --ds-radius-xl:        16px;  /* the result hero and other full-width surfaces */
  --ds-radius-pill:      999px;

  /* Light is the default palette, and saying so lets the browser paint its own
     form controls, scrollbars and caret to match. */
  color-scheme: light;
}

/* ── DARK PALETTE ──
   One block, and only role and semantic tokens in it. Nothing below this point
   in the stylesheet knows which palette is active; if a rule needs to, that
   rule is reaching past the tokens and should be fixed rather than forked.

   :root[data-theme="dark"] rather than a @media (prefers-color-scheme) query.
   static/js/theme.js resolves the "system" setting to a concrete light or dark
   value and stamps it on <html> before first paint, so the media query would
   only ever be a second, quieter source of truth for the same decision.

   The values are not the light ones inverted. A dark surface needs a *lighter*
   status colour than a light one to read at the same weight, and the greens
   and reds here are chosen for the dark backgrounds they sit on;
   tests/test_design_system.py computes the contrast of both palettes from
   these declarations, so an edit that dims one of them fails there. */
:root[data-theme="dark"] {
  --ds-bg:               #0E1826;
  --ds-surface:          #16212F;
  --ds-surface-sunken:   #111C29;
  --ds-border:           #2C3A4C;
  --ds-border-subtle:    #222F40;
  --ds-border-strong:    #5E7189;

  --ds-text:             #E8EEF6;
  --ds-text-secondary:   #C2CDDA;
  --ds-text-muted:       #93A3B6;
  --ds-text-on-dark:     #E8EEF6;

  --ds-heading:          #F2F7FC;
  --ds-link:             #6BB0EA;
  --ds-link-hover:       #93C7F2;
  --ds-schematic-ink:    #C2CDDA;

  --ds-chrome-bg:        #0B1420;
  --ds-chrome-fg-dim:    rgba(232, 238, 246, 0.70);
  --ds-chrome-line:      rgba(255, 255, 255, 0.11);
  --ds-chrome-wash:      rgba(255, 255, 255, 0.06);
  --ds-solid-bg:         #1E3A5F;
  --ds-solid-border:     #2C3A4C;

  /* Both stay mid-blue rather than brightening with the rest of the palette:
     they are button and badge *fills* carrying white text, and a fill light
     enough to read as "blue" on a dark page is too light to hold white. The
     brightening happens in --ds-link, which is text and needs the opposite. */
  --ds-primary:          #2F6FB0;
  --ds-primary-dark:     #255887;
  --ds-primary-light:    #4A8BC9;
  --ds-secondary:        #2F6FB0;
  --ds-secondary-dark:   #255887;
  --ds-secondary-light:  #4A8BC9;

  --ds-pass:             #4ADE80;
  --ds-pass-bg:          #10251B;
  --ds-fail:             #FF6B6B;
  --ds-fail-bg:          #2A1518;
  --ds-review:           #FBBF24;
  --ds-review-bg:        #2A2011;
  --ds-info:             #6BB0EA;
  --ds-info-bg:          #12233A;

  --ds-focus-ring:       rgba(107, 176, 234, 0.35);

  /* Deeper and wider than the light pair: on a dark ground a shadow is read as
     separation between two dark surfaces, not as a cast shadow. */
  --ds-shadow-1:         0 1px 2px rgba(0, 0, 0, 0.35);
  --ds-shadow-2:         0 4px 18px rgba(0, 0, 0, 0.45);
  --ds-overlay:          rgba(4, 9, 16, 0.65);

  --ds-on-status:        #0B1420;

  color-scheme: dark;
}

/* ── BREAKPOINTS ──
   Three tiers, and no others. Before this the codebase used 480, 575.98, 767,
   767.98, 768, 991 and 991.98 across five stylesheets for what is really three
   decisions, so a rule could take effect one pixel away from the rule it was
   written to accompany.

   They are Bootstrap 5's own max-width breakpoints, because the app renders
   Bootstrap's responsive utility classes on the same elements. Bootstrap's
   min-width queries use whole numbers, so a hand-written `max-width: 768px`
   overlaps its `min-width: 768px` and both apply at exactly 768px. The .98
   offsets are what keep the two systems from fighting over that pixel.

     --ds-bp-sm   575.98px   below Bootstrap sm, phones
     --ds-bp-md   767.98px   below Bootstrap md, large phones and portrait tablets
     --ds-bp-lg   991.98px   below Bootstrap lg, where the sidebar goes off-canvas

   The custom properties below are declarations, not machinery: a custom
   property cannot be used inside an @media condition, so the literals still
   have to be written out. tests/test_design_system.py is what actually holds
   the set closed. */
:root {
  --ds-bp-sm: 575.98px;
  --ds-bp-md: 767.98px;
  --ds-bp-lg: 991.98px;
}

/* ── BOOTSTRAP OVERRIDES: Bootstrap's corners, mapped onto our scale ──
   These were all 0. Bootstrap ships four unrelated radii and the product used
   to answer that by flattening every one of them, which is a decision, but it
   also meant a Bootstrap component and one of ours could never agree once we
   did want corners. Now each Bootstrap variable points at the step of the
   radius scale it corresponds to, so .alert and .ds-panel round identically
   without either of them knowing about the other. */
:root {
  --bs-border-radius:             var(--ds-radius-md) !important;
  --bs-border-radius-sm:          var(--ds-radius-sm) !important;
  --bs-border-radius-lg:          var(--ds-radius-lg) !important;
  --bs-border-radius-xl:          var(--ds-radius-xl) !important;
  --bs-border-radius-2xl:         var(--ds-radius-xl) !important;
  --bs-border-radius-pill:        var(--ds-radius-pill) !important;
  --bs-btn-border-radius:         var(--ds-radius-md) !important;
  --bs-input-border-radius:       var(--ds-radius-sm) !important;
  --bs-card-border-radius:        var(--ds-radius-lg) !important;
  --bs-badge-border-radius:       var(--ds-radius-xs) !important;
  --bs-alert-border-radius:       var(--ds-radius-md) !important;
  --bs-dropdown-border-radius:    var(--ds-radius-lg) !important;
  --bs-nav-pills-border-radius:   var(--ds-radius-md) !important;
  --bs-modal-border-radius:       var(--ds-radius-lg) !important;
  --bs-toast-border-radius:       var(--ds-radius-lg) !important;
  --bs-popover-border-radius:     var(--ds-radius-lg) !important;
  --bs-pagination-border-radius:  var(--ds-radius-sm) !important;
  --bs-list-group-border-radius:  var(--ds-radius-lg) !important;
  --bs-form-select-border-radius: var(--ds-radius-sm) !important;

  /* Bootstrap's .border utilities read this, and it ships as a light grey
     that stays light grey on a dark page. Thirty-six templates draw a box with
     `class="border rounded"`, which is a reasonable thing to write and was
     a bright hairline in the dark palette every time. */
  --bs-border-color: var(--ds-border) !important;
}

/* ── BASE ── */
*, *::before, *::after { box-sizing: border-box; }

/* Fluid root size: 14px on a ~1440px laptop viewport, scaling up to 17px
   on a ~2560px (27") display, clamped at both ends so it never runs away
   on ultra-wide monitors or shrinks below legibility on narrow ones. */
html { font-size: clamp(14px, 0.268vw + 10.14px, 17px); }

body {
  font-family: var(--ds-font);
  background: var(--ds-bg);
  color: var(--ds-text);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

/* Skip link. Off-screen rather than display:none, because a hidden element
   is not focusable and a skip link that cannot be focused is not a skip
   link. z-index sits above the topbar (100) and the sidebar backdrop. */
.skip-link {
  border-radius: var(--ds-radius-md);
  position: fixed;
  top: var(--ds-space-4);
  left: var(--ds-space-4);
  z-index: 1100;
  transform: translateY(calc(-100% - var(--ds-space-10)));
  padding: var(--ds-space-4) var(--ds-space-9);
  background: var(--ds-surface);
  color: var(--ds-heading);
  font-size: var(--ds-type-sm);
  font-weight: 600;
  text-decoration: none;
  border: 2px solid var(--ds-heading);
}

.skip-link:focus {
  transform: translateY(0);
}

/* Reduced motion, globally. auth.css already gates its own entrance
   animation behind (prefers-reduced-motion: no-preference), but that only
   covered one page; every transition in this file, the off-canvas sidebar
   slide included, was unconditional. Motion is not removed, it is reduced to
   a duration short enough not to read as movement, which keeps state changes
   from happening with no transition at all. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ── APP LAYOUT ── */
.app-layout {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

/* No overflow: hidden here. It used to clip anything wider than the shell out
   of existence rather than letting it be scrolled to, which is how a wide
   trace table could lose its right-hand columns with no scrollbar to say so.
   .main-content owns the scrolling instead, and min-width: 0 on it is what
   stops a wide child from stretching the flex item and pushing the page into
   a horizontal scroll. */
.layout-shell {
  display: flex;
  flex: 1;
  min-height: 0;
}

/* ── TOPBAR ── */
.topbar {
  height: var(--ds-topbar-h);
  background: var(--ds-chrome-bg);
  border-bottom: 2px solid var(--ds-accent);
  display: flex;
  align-items: center;
  padding: 0 var(--ds-space-8);
  gap: var(--ds-space-6);
  flex-shrink: 0;
  position: sticky;
  top: 0;
  z-index: 1040;
}

.topbar-brand {
  display: flex;
  align-items: center;
  width: calc(var(--ds-sidebar-w) - 1rem);
  flex-shrink: 0;
  text-decoration: none;
}

/* Shared reversed artwork stays legible on the topbar in either theme. */
.topbar-brand-logo {
  display: block;
  width: auto;
  height: 28px;
}

.topbar-menu-btn {
  border-radius: var(--ds-radius-sm);
  display: none;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  flex: 0 0 2.25rem;
  border: 1px solid rgba(255,255,255,0.2);
  background: transparent;
  color: var(--ds-chrome-fg);
}

.topbar-menu-btn:hover,
.topbar-menu-btn:focus-visible {
  background: rgba(255,255,255,0.08);
  border-color: rgba(255,255,255,0.45);
}

.topbar-sep {
  width: 1px;
  height: 20px;
  background: rgba(255,255,255,0.15);
  flex-shrink: 0;
}

.topbar-spacer { flex: 1; }

.topbar-actions {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
}

/* Unit toggle */
.unit-toggle {
  display: flex;
  align-items: center;
  gap: 0;
  border: 1px solid rgba(255,255,255,0.2);
  border-radius: var(--ds-radius-pill);
  overflow: hidden;
}

.unit-toggle-btn {
  background: transparent;
  border: none;
  color: rgba(255,255,255,0.55);
  font-size: var(--ds-type-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  padding: var(--ds-space-2) var(--ds-space-5);
  cursor: pointer;
  font-family: var(--ds-font);
  transition: background 0.12s, color 0.12s;
}

.unit-toggle-btn.active {
  background: rgba(255,255,255,0.15);
  color: var(--ds-chrome-fg);
}

.unit-toggle-btn:hover:not(.active) {
  background: rgba(255,255,255,0.08);
  color: rgba(255,255,255,0.85);
}

/* Theme toggle. Same segmented shape as the unit toggle above, because it is
   the same kind of control: a setting with two values, both always visible,
   neither of them hidden behind the other's icon. A single icon that swaps
   between a sun and a moon has to answer "is that the current theme or the
   one I would get" and never can. */
.theme-toggle {
  display: flex;
  align-items: center;
  gap: 0;
  border: 1px solid rgba(255,255,255,0.2);
  border-radius: var(--ds-radius-pill);
  overflow: hidden;
}

.theme-toggle-btn {
  background: transparent;
  border: none;
  color: rgba(255,255,255,0.55);
  font-size: var(--ds-type-xs);
  line-height: 1;
  padding: var(--ds-space-4) var(--ds-space-6);
  cursor: pointer;
  font-family: var(--ds-font);
  transition: background 0.12s, color 0.12s;
}

.theme-toggle-btn[aria-pressed="true"] {
  background: rgba(255,255,255,0.15);
  color: var(--ds-chrome-fg);
}

.theme-toggle-btn:hover[aria-pressed="false"] {
  background: rgba(255,255,255,0.08);
  color: rgba(255,255,255,0.85);
}

.theme-toggle-btn:focus-visible {
  outline: 2px solid var(--ds-accent);
  outline-offset: -2px;
}

/* User dropdown in topbar */
.topbar-user-btn {
  border-radius: var(--ds-radius-pill);
  display: flex;
  align-items: center;
  gap: var(--ds-space-4);
  background: transparent;
  border: 1px solid rgba(255,255,255,0.2);
  color: rgba(255,255,255,0.85);
  font-size: var(--ds-type-sm);
  padding: var(--ds-space-2) var(--ds-space-6);
  cursor: pointer;
  transition: border-color 0.12s, background 0.12s, color 0.12s;
  font-family: var(--ds-font);
}

.topbar-user-btn:hover {
  border-color: rgba(255,255,255,0.45);
  background: rgba(255,255,255,0.08);
  color: var(--ds-chrome-fg);
}

.topbar-dropdown {
  background: var(--ds-primary-dark);
  border: 1px solid rgba(255,255,255,0.12);
  box-shadow: 0 8px 24px rgba(0,0,0,0.4);
  min-width: 200px;
}

.topbar-dropdown .dropdown-item {
  color: rgba(255,255,255,0.75);
  font-size: var(--ds-type-sm);
  padding: var(--ds-space-4) var(--ds-space-8);
}

.topbar-dropdown .dropdown-item:hover,
.topbar-dropdown .dropdown-item:focus {
  background: rgba(255,255,255,0.08);
  color: var(--ds-chrome-fg);
}

.topbar-dropdown .dropdown-divider {
  border-color: rgba(255,255,255,0.1);
}

.topbar-auth-link {
  border-radius: var(--ds-radius-md);
  color: rgba(255,255,255,0.7);
  text-decoration: none;
  font-size: var(--ds-type-sm);
  padding: var(--ds-space-2) var(--ds-space-5);
  border: 1px solid transparent;
  transition: color 0.12s, border-color 0.12s;
}

.topbar-auth-link:hover {
  color: var(--ds-chrome-fg);
  border-color: rgba(255,255,255,0.25);
}

.topbar-auth-link.signup {
  border-color: var(--ds-accent);
  color: var(--ds-accent);
  font-weight: 600;
}

.topbar-auth-link.signup:hover {
  background: var(--ds-accent);
  color: var(--ds-text-on-accent);
}

/* ── SIDEBAR ── */
.sidebar {
  width: var(--ds-sidebar-w);
  flex-shrink: 0;
  background: var(--ds-chrome-bg);
  overflow-y: auto;
  height: calc(100vh - var(--ds-topbar-h));
  position: sticky;
  top: var(--ds-topbar-h);
  border-right: 1px solid rgba(0,0,0,0.25);
  scrollbar-width: thin;
  scrollbar-color: rgba(255,255,255,0.12) transparent;
}

.sidebar::-webkit-scrollbar { width: 3px; }
.sidebar::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); }

.sidebar-backdrop {
  display: none;
}

.sidebar-section { padding: var(--ds-space-9) 0 var(--ds-space-4); }

.sidebar-section-label {
  padding: 0 var(--ds-space-7) var(--ds-space-4);
  font-size: var(--ds-type-2xs);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  /* Was 0.35, which composites to 2.82:1 on --ds-primary. 0.62 gives 5.52:1.
     The hierarchy against .sidebar-link is carried by size, weight and caps,
     not by being too faint to read. */
  color: rgba(255,255,255,0.62);
  display: flex;
  align-items: center;
  gap: var(--ds-space-3);
}

.sidebar-link {
  display: flex;
  align-items: center;
  gap: var(--ds-space-4);
  padding: var(--ds-space-4) var(--ds-space-7);
  font-size: var(--ds-type-sm);
  color: rgba(255,255,255,0.68);
  text-decoration: none;
  border-left: 2px solid transparent;
  transition: background 0.1s, color 0.1s, border-color 0.1s;
  margin-bottom: 1px;
}

.sidebar-link:hover {
  background: rgba(255,255,255,0.07);
  color: var(--ds-chrome-fg);
}

.sidebar-link.active {
  background: rgba(255,255,255,0.1);
  color: var(--ds-chrome-fg);
  border-left-color: var(--ds-accent);
}

.sidebar-link-badge {
  margin-left: auto;
  font-size: var(--ds-type-2xs);
  font-weight: 700;
  letter-spacing: 0.05em;
  /* Was 0.4, at 3.23:1. Measured against the pill, not the sidebar: the
     0.08 white background lightens --ds-primary to #2D4A69, so the same
     alpha buys less contrast here than it does one rule up. 0.62 would only
     reach 4.65:1 against that; 0.68 gives 5.24:1. */
  color: rgba(255,255,255,0.68);
  background: rgba(255,255,255,0.08);
  padding: var(--ds-space-1) var(--ds-space-3);
}

.sidebar-divider {
  margin: var(--ds-space-5) var(--ds-space-7);
  border: none;
  border-top: 1px solid rgba(255,255,255,0.07);
}

/* Labels never wrap. A narrow sidebar clips them like a curtain, and the
   full name is the link's title; the badge keeps its place at the edge. */
.sidebar-link,
.sidebar-planned {
  white-space: nowrap;
  overflow: hidden;
}

.sidebar-link > span:first-child {
  min-width: 0;
  overflow: hidden;
}

.sidebar-link-badge {
  flex-shrink: 0;
}

/* One group per standard. The summary is a sidebar link with a chevron that
   turns when the group is open; the entries sit one indent in. */
.sidebar-group > summary.sidebar-link {
  list-style: none;
  cursor: pointer;
}

.sidebar-group > summary.sidebar-link::-webkit-details-marker {
  display: none;
}

.sidebar-group > summary.sidebar-link::after {
  content: "";
  flex-shrink: 0;
  margin-left: auto;
  width: 0.45em;
  height: 0.45em;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transition: transform 0.1s;
}

.sidebar-group[open] > summary.sidebar-link::after {
  transform: rotate(45deg);
}

.sidebar-link--nested {
  padding-left: calc(var(--ds-space-7) + var(--ds-space-9));
}

/* A standard without a module: the same row, not a link, no hover. */
.sidebar-planned {
  display: flex;
  align-items: center;
  gap: var(--ds-space-4);
  padding: var(--ds-space-4) var(--ds-space-7);
  font-size: var(--ds-type-sm);
  color: rgba(255,255,255,0.62);
  border-left: 2px solid transparent;
  margin-bottom: 1px;
}

/* The drag handle after the sidebar. Sticky like the sidebar, overlapping its
   right edge; sidebar_resize.js moves --ds-sidebar-w and remembers it. */
.sidebar-resizer {
  flex: 0 0 5px;
  margin-left: -5px;
  align-self: flex-start;
  position: sticky;
  top: var(--ds-topbar-h);
  height: calc(100vh - var(--ds-topbar-h));
  z-index: 2;
  cursor: col-resize;
  background: transparent;
  transition: background 0.1s;
}

.sidebar-resizer:hover,
.sidebar-resizer:focus-visible,
body.sidebar-resizing .sidebar-resizer {
  background: var(--ds-accent);
  outline: none;
}

body.sidebar-resizing {
  cursor: col-resize;
  user-select: none;
}

/* One synchronous measurement for snap-to-fit: rows at their natural width,
   so the widest says how wide the sidebar has to be. Never painted. */
.sidebar--measuring :is(.sidebar-link, .sidebar-planned, .sidebar-section-label) {
  width: max-content;
}

/* ── MAIN CONTENT ── */
.main-content {
  /* Keep absolute descendants, including hidden live status messages, inside
     this scroll area instead of extending the outer document below it. */
  position: relative;
  flex: 1;
  overflow-y: auto;
  min-width: 0;
  height: calc(100vh - var(--ds-topbar-h));
}

/* 21px/28px at a 14px root. The block padding came down a step from 1.75rem
   when the scale went in, which is the one place the tokenisation was allowed
   to move the layout: A1 asks for dashboard density, and this is the rule that
   sets how much of the screen every page spends on margin. */
.content-body {
  padding: var(--ds-space-10) var(--ds-space-11);
}

/* Off-canvas shell, below --ds-bp-lg. Raised from 768px: between 768 and 991
   the fixed 16.57rem sidebar left a portrait tablet about 535px of content,
   which is narrower than the wizard forms are built for. */
@media (max-width: 991.98px) {
  .topbar {
    padding-inline: var(--ds-space-4);
    gap: var(--ds-space-3);
  }

  .topbar-menu-btn {
    display: inline-flex;
    /* 44x44 touch target. The desktop rule keeps 2.25rem, where a pointer is
       doing the aiming and WCAG 2.5.8's 24px floor already clears. */
    width: 2.75rem;
    height: 2.75rem;
    flex-basis: 2.75rem;
    min-width: 44px;
    min-height: 44px;
  }

  .topbar-brand {
    width: auto;
  }

  .topbar > .topbar-sep {
    display: none;
  }

  .topbar-user-name {
    display: none;
  }

  .topbar-user-btn {
    padding-inline: var(--ds-space-4);
  }

  .sidebar-resizer {
    display: none;
  }

  .sidebar {
    position: fixed;
    /* The off-canvas panel keeps its own width: a reader who widened the
       desktop sidebar should not get a panel covering the phone. */
    width: 16.57rem;
    z-index: 1035;
    left: 0;
    top: var(--ds-topbar-h);
    transform: translateX(-100%);
    /* visibility, not just transform. A translated-off panel still takes tab
       stops and still reads to a screen reader, so the closed sidebar was
       thirteen invisible links sitting in front of the page content.

       The 0.18s is a duration, not a delay. visibility interpolates
       discretely: given a duration it holds the visible value for the whole
       slide and flips at the end, which is exactly the behaviour wanted. The
       delay form (`visibility 0s linear 0.18s`) reads as though it should do
       the same and does not, verified in the browser: the panel stayed
       visible and tabbable indefinitely after closing. */
    visibility: hidden;
    transition: transform 0.18s ease, visibility 0.18s;
    box-shadow: 10px 0 28px rgba(0,0,0,0.28);
  }

  .sidebar.is-open {
    transform: translateX(0);
    visibility: visible;
    transition: transform 0.18s ease, visibility 0s;
  }

  .sidebar-backdrop {
    position: fixed;
    z-index: 1030;
    inset: var(--ds-topbar-h) 0 0;
    width: 100%;
    border: 0;
    background: var(--ds-overlay);
  }

  .sidebar-backdrop.is-open {
    display: block;
  }

  body.sidebar-open {
    overflow: hidden;
  }

  .main-content {
    width: 100%;
  }

  .content-body {
    padding: var(--ds-space-8) var(--ds-space-6);
  }

  /* Touch targets. Applied only here, because forcing 44px rows on the
     desktop sidebar would undo the density A1 is after, and a mouse does not
     need them. */
  .sidebar-link {
    min-height: 44px;
  }

  .unit-toggle-btn {
    min-height: 44px;
    /* min-width as well as padding: "SI" is two characters, so padding alone
       left the button 34px wide and only the height cleared 44. */
    min-width: 44px;
    padding-inline: var(--ds-space-7);
  }

  .topbar-user-btn {
    min-height: 44px;
  }
}

/* At 400px the topbar was 17px wider than the viewport, so every page in the
   product scrolled sideways on a phone: the menu button, the wordmark and
   three controls do not fit on one 400px line once each control clears the
   44px touch target.

   The dividers between the controls go first. They separate groups that are
   already separated by their own shapes, and at this width the row is short
   enough to read without them. */
@media (max-width: 575.98px) {

  .topbar-actions .topbar-sep {
    display: none;
  }

  /* The standalone mark leaves room for the theme and unit controls. */
  .topbar-brand-logo {
    height: 30px;
  }

  /* min-width already holds the 44px touch target, so the padding was only
     making "USC" wider than "SI". */
  .unit-toggle-btn {
    padding-inline: var(--ds-space-4);
  }

}

/* Auth pages (login, signup, etc.) — narrow centered layout */
.content-body.auth-page {
  max-width: 480px;
  margin: 0 auto;
  padding-top: var(--ds-space-13);
}

/* ── MESSAGES / ALERTS ── */
.messages-zone {
  padding: var(--ds-space-4) var(--ds-space-11) 0;
}

/* ── TYPOGRAPHY ── */
h1 { font-size: var(--ds-type-2xl);  font-weight: 700; }
h2 { font-size: var(--ds-type-xl);   font-weight: 600; }
h3 { font-size: var(--ds-type-lg);   font-weight: 600; }
h4 { font-size: var(--ds-type-md);   font-weight: 600; }
h5 { font-size: var(--ds-type-base); font-weight: 600; }
h6 { font-size: var(--ds-type-sm);   font-weight: 600; }

.page-title {
  font-size: var(--ds-type-xl);
  font-weight: 700;
  color: var(--ds-heading);
  margin-bottom: var(--ds-space-2);
  line-height: 1.2;
}

.page-subtitle {
  font-size: var(--ds-type-sm);
  color: var(--ds-text-muted);
  margin-bottom: var(--ds-space-10);
}

.section-label {
  font-size: var(--ds-type-xs);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ds-text-muted);
  margin-bottom: var(--ds-space-4);
}

/* ── BUTTONS ── */
.btn {
  border-radius: var(--ds-radius-md) !important;
  font-size: var(--ds-type-sm);
  font-weight: 500;
  padding: var(--ds-space-4) var(--ds-space-8);
  letter-spacing: 0.01em;
  transition: background 0.1s, border-color 0.1s, color 0.1s;
}

.btn-lg { padding: var(--ds-space-5) var(--ds-space-10); font-size: var(--ds-type-base); }
.btn-sm { padding: var(--ds-space-2) var(--ds-space-6); font-size: var(--ds-type-xs); }

.btn-primary {
  background: var(--ds-secondary);
  border-color: var(--ds-secondary);
  color: var(--ds-solid-fg);
}
.btn-primary:hover, .btn-primary:focus {
  background: var(--ds-secondary-dark);
  border-color: var(--ds-secondary-dark);
  color: var(--ds-solid-fg);
}

.btn-dark-navy {
  border-radius: var(--ds-radius-md);
  background: var(--ds-solid-bg);
  border: 1px solid var(--ds-solid-border);
  color: var(--ds-solid-fg);
}
.btn-dark-navy:hover {
  background: var(--ds-solid-border);
  color: var(--ds-solid-fg);
}

.btn-accent {
  border-radius: var(--ds-radius-md);
  background: var(--ds-accent);
  border: 1px solid var(--ds-accent-dark);
  color: var(--ds-text-on-accent);
  font-weight: 600;
}
.btn-accent:hover {
  background: var(--ds-accent-dark);
  color: var(--ds-text-on-accent);
}

.btn-outline-secondary {
  border-color: var(--ds-border-strong);
  color: var(--ds-text-secondary);
  background: transparent;
}
.btn-outline-secondary:hover {
  background: var(--ds-bg);
  border-color: var(--ds-text-secondary);
  color: var(--ds-text);
}

/* ── PICKER BUTTON ──
   The button that opens a table of standard values under the fields: the
   pipe, bend and connection pickers. A raised tab with the accent edge and a
   chevron that turns when the table is open; open, its bottom corners square
   off and it joins the .ds-card below, so button and table read as one
   piece. The scripts set aria-expanded; the stylesheet reads it. The row
   owns its bottom margin so the join can close the gap. */
.picker-toggle-row {
  margin-bottom: var(--ds-space-4);
}

.btn-picker {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-4);
  background: var(--ds-surface-sunken);
  color: var(--ds-text);
  border: 1px solid var(--ds-border);
  border-left: 3px solid var(--ds-accent);
  border-radius: var(--ds-radius-sm) !important;
  font-weight: 600;
  box-shadow: var(--ds-shadow-1);
  transition: background 0.1s, border-color 0.1s, color 0.1s, box-shadow 0.1s, transform 0.1s;
}

.btn-picker::after {
  content: "";
  width: 0.4em;
  height: 0.4em;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-20%) rotate(45deg);
  transition: transform 0.12s;
}

.btn-picker:hover,
.btn-picker:focus-visible {
  background: var(--ds-surface-sunken);
  color: var(--ds-text);
  border-color: var(--ds-border-strong);
  border-left-color: var(--ds-accent);
  box-shadow: var(--ds-shadow-2);
  transform: translateY(-1px);
}

.btn-picker[aria-expanded="true"] {
  position: relative;
  z-index: 1;
  margin-bottom: -1px;
  border-bottom-left-radius: 0 !important;
  border-bottom-right-radius: 0 !important;
  border-bottom-color: var(--ds-surface-sunken);
  box-shadow: none;
  transform: none;
}

.btn-picker[aria-expanded="true"]::after {
  transform: translateY(20%) rotate(-135deg);
}

.picker-toggle-row:has(.btn-picker[aria-expanded="true"]) {
  margin-bottom: 0;
}

.picker-toggle-row:has(.btn-picker[aria-expanded="true"]) + .ds-card {
  border-top-left-radius: 0;
}

@media (prefers-reduced-motion: reduce) {
  .btn-picker,
  .btn-picker::after {
    transition: none;
  }
}

/* ── CARDS ──
   overflow: hidden is what makes the corners real. Without it a full-bleed
   header or a table as the last child paints its own square corner over the
   card's rounded one, which is the single most common way a rounded surface
   ends up looking broken. */
.ds-card {
  background: var(--ds-surface);
  border: 1px solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-lg);
  box-shadow: var(--ds-shadow-1);
  margin-bottom: var(--ds-space-9);
  overflow: hidden;
}

.ds-card-header {
  display: flex;
  align-items: baseline;
  gap: var(--ds-space-6);
  border-bottom: 1px solid var(--ds-border-subtle);
  padding: var(--ds-space-7) var(--ds-space-9);
  background: var(--ds-surface-sunken);
}

.ds-card-title {
  font-size: var(--ds-type-base);
  font-weight: 700;
  color: var(--ds-heading);
  margin: 0;
  letter-spacing: 0.01em;
}

/* overflow-x: auto is the other half of the card's overflow: hidden. Wide
   engineering tables are written straight into a card body, and a clipped
   card would drop their right-hand columns with no scrollbar to say so, which
   is the same failure .layout-shell has a test against. The card keeps its
   corners; the table scrolls inside it. */
.ds-card-body {
  padding: var(--ds-space-9);
  overflow-x: auto;
}

/* The card had a header and a body but never a footer, so the three dashboard
   cards drew one by hand, three times, with the same two declarations. A card
   that ends in an action row is a pattern, not a one-off. */
.ds-card-footer {
  padding: var(--ds-space-6) var(--ds-space-9);
  border-top: 1px solid var(--ds-border-subtle);
}

.ds-card-footer--actions {
  display: flex;
  gap: var(--ds-space-4);
}

/* A card tinted by an outcome: the sensitivity panel's safety-margin summary,
   and anything else whose whole point is the status it carries. The status
   tokens bring a readable foreground with them in both palettes, which
   Bootstrap's bg-success with white text did not. */
.ds-card--pass   { background: var(--ds-pass-bg);   border-color: var(--ds-pass); }
.ds-card--review { background: var(--ds-review-bg); border-color: var(--ds-review); }
.ds-card--fail   { background: var(--ds-fail-bg);   border-color: var(--ds-fail); }

.ds-card--pass   .ds-card-header { background: none; color: var(--ds-pass); }
.ds-card--review .ds-card-header { background: none; color: var(--ds-review); }
.ds-card--fail   .ds-card-header { background: none; color: var(--ds-fail); }

/* Cards standing side by side in a row, where the footers should line up even
   though the bodies hold different amounts. Pair with Bootstrap's .h-100. */
.ds-card--fill {
  display: flex;
  flex-direction: column;
}

.ds-card--fill > .ds-card-body {
  flex: 1;
}

/* ── FORM ELEMENTS ── */
/* Numeric values are entered directly throughout the app, including inputs
   added dynamically. Hide browser increment/decrement controls. */
input[type="number"] {
  -moz-appearance: textfield;
  appearance: textfield;
}

input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

.form-control,
.form-select {
  border-radius: var(--ds-radius-sm) !important;
  border-color: var(--ds-border);
  font-size: var(--ds-type-sm);
  color: var(--ds-text);
  background: var(--ds-surface);
  padding: var(--ds-space-3) var(--ds-space-5);
}

.form-control:focus,
.form-select:focus {
  color: var(--ds-text);
  background: var(--ds-surface);
  border-color: var(--ds-secondary);
  box-shadow: 0 0 0 2px var(--ds-focus-ring);
  outline: none;
}

.form-control::placeholder {
  color: var(--ds-text-muted);
  opacity: 1;
}

/* A number typed beside a slider or in a sentence: as wide as the number. */
.form-control--short { width: 12ch; flex: none; }

.form-label {
  font-size: var(--ds-type-sm);
  font-weight: 500;
  color: var(--ds-text-secondary);
  margin-bottom: var(--ds-space-2);
}

.form-text {
  font-size: var(--ds-type-xs);
  color: var(--ds-text-muted);
}

.input-group-text {
  border-radius: var(--ds-radius-sm) !important;
  background: var(--ds-bg);
  border-color: var(--ds-border);
  color: var(--ds-text-muted);
  font-size: var(--ds-type-sm);
}

/* Input groups join two rounded controls edge to edge, so the corners that
   meet in the middle have to be squared again. Bootstrap does this already;
   the !important radii on .form-control and .input-group-text above outrank
   it, so its rules are repeated here at the weight they now need. */
.input-group > :not(:first-child):not(.dropdown-menu):not(.form-floating):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) {
  border-top-left-radius: 0 !important;
  border-bottom-left-radius: 0 !important;
}

.input-group > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating) {
  border-top-right-radius: 0 !important;
  border-bottom-right-radius: 0 !important;
}

/* ── STATUS BADGES ── */
.badge { border-radius: var(--ds-radius-xs) !important; }

.status-badge {
  border-radius: var(--ds-radius-xs);
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-2);
  font-size: var(--ds-type-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: var(--ds-space-2) var(--ds-space-4);
  border: 1px solid;
}

.status-badge-pass   { background: var(--ds-pass-bg);   color: var(--ds-pass);   border-color: currentColor; }
.status-badge-fail   { background: var(--ds-fail-bg);   color: var(--ds-fail);   border-color: currentColor; }
.status-badge-review { background: var(--ds-review-bg); color: var(--ds-review); border-color: currentColor; }
.status-badge-info   { background: var(--ds-info-bg);   color: var(--ds-info);   border-color: currentColor; }

/* No outcome: a flag that is off, a plan with nothing to say. The shape of a
   status badge without the claim that something passed or failed. */
.status-badge-muted  { background: var(--ds-surface-sunken); color: var(--ds-text-muted); border-color: var(--ds-border); }

/* ── Normalized acceptance-check utilization ───────────────────────────── */
.utilization-summary {
  width: min(100%, 34rem);
  min-height: 3.5rem;
  margin: var(--ds-space-6) auto var(--ds-space-8);
  padding: var(--ds-space-5) var(--ds-space-7);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ds-space-8);
  background: var(--ds-surface);
  border: 1px solid var(--ds-border);
  border-left: 4px solid var(--ds-info);
  border-radius: var(--ds-radius-sm);
  box-shadow: var(--ds-shadow-1);
}

.utilization-summary--safe {
  background: var(--ds-pass-bg);
  border-left-color: var(--ds-pass);
}

.utilization-summary--warning {
  background: var(--ds-review-bg);
  border-left-color: var(--ds-review);
}

.utilization-summary--critical {
  background: var(--ds-fail-bg);
  border-left-color: var(--ds-fail);
}

.utilization-summary-copy {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.utilization-summary-label {
  color: var(--ds-text);
  font-size: var(--ds-type-xs);
  font-weight: 700;
  letter-spacing: 0.035em;
  text-transform: uppercase;
}

.utilization-summary-name {
  overflow: hidden;
  color: var(--ds-text-muted);
  font-size: var(--ds-type-xs);
  line-height: 1.25;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.utilization-summary-value {
  flex: 0 0 auto;
  color: var(--ds-info);
  font-family: var(--ds-font-mono);
  font-size: var(--ds-type-xl);
  line-height: 1;
}

.utilization-summary--safe .utilization-summary-value { color: var(--ds-pass); }
.utilization-summary--warning .utilization-summary-value { color: var(--ds-review); }
.utilization-summary--critical .utilization-summary-value { color: var(--ds-fail); }

.utilization-checks {
  width: min(100%, 78rem);
  margin-right: auto;
  margin-left: auto;
}

.utilization-checks-body {
  padding: 0;
}

.utilization-checks table {
  font-size: var(--ds-type-sm);
}

.utilization-checks th:last-child,
.utilization-checks td:last-child {
  min-width: 8.5rem;
}

.utilization-checks .status-badge {
  white-space: nowrap;
}

.utilization-check-name {
  font-weight: 500;
}

.utilization-measure {
  font-family: var(--ds-font-mono);
  font-size: var(--ds-type-sm);
  white-space: nowrap;
}

.utilization-placeholder {
  color: var(--ds-text-muted);
}

.utilization-check-row--failed {
  background: var(--ds-fail-bg);
}

.utilization-value {
  font-family: var(--ds-font-mono);
  font-size: var(--ds-type-sm);
  white-space: nowrap;
}

.utilization-value--safe { color: var(--ds-pass); }
.utilization-value--warning { color: var(--ds-review); }
.utilization-value--critical { color: var(--ds-fail); }

@media (max-width: 575.98px) {
  .utilization-summary {
    align-items: flex-start;
  }

  .utilization-summary-name {
    white-space: normal;
  }
}

.part-badge {
  border-radius: var(--ds-radius-xs);
  font-size: var(--ds-type-2xs);
  font-weight: 700;
  letter-spacing: 0.05em;
  background: rgba(46,109,164,0.1);
  color: var(--ds-link);
  border: 1px solid rgba(46,109,164,0.2);
  padding: var(--ds-space-1) var(--ds-space-3);
}

/* ── RESULT BANNERS ── */
.result-banner {
  border-radius: var(--ds-radius-md);
  display: flex;
  align-items: center;
  gap: var(--ds-space-6);
  padding: var(--ds-space-7) var(--ds-space-9);
  font-size: var(--ds-type-base);
  font-weight: 600;
  border-left: 4px solid;
  margin-bottom: var(--ds-space-10);
}

.result-banner i { font-size: var(--ds-type-md); }

.result-banner-pass   { background: var(--ds-pass-bg);   border-color: var(--ds-pass);   color: var(--ds-pass); }
.result-banner-fail   { background: var(--ds-fail-bg);   border-color: var(--ds-fail);   color: var(--ds-fail); }
.result-banner-info { background: var(--ds-info-bg); border-color: var(--ds-info); color: var(--ds-info); }
.result-banner-review { background: var(--ds-review-bg); border-color: var(--ds-review); color: var(--ds-review); }

/* ── VERDICT CARD HEADER — the accept/reject decision baked into the
   Acceptance Criteria card itself, instead of a separate floating banner ── */
.verdict-header {
  display: flex;
  align-items: center;
  gap: var(--ds-space-6);
  padding: var(--ds-space-8) var(--ds-space-9);
  border-bottom: 3px solid;
}

.verdict-header-icon {
  font-size: var(--ds-type-2xl);
  line-height: 1;
  flex-shrink: 0;
}

.verdict-header-text {
  font-size: var(--ds-type-md);
  font-weight: 700;
}

.verdict-header-value {
  font-weight: 400;
  font-size: var(--ds-type-base);
  opacity: 0.85;
}

.verdict-header-ref {
  margin-left: auto;
  font-size: var(--ds-type-xs);
  font-weight: 400;
  opacity: 0.7;
  white-space: nowrap;
}

.verdict-header--pass { background: var(--ds-pass-bg);   border-color: var(--ds-pass);   color: var(--ds-pass); }
.verdict-header--fail { background: var(--ds-fail-bg);   border-color: var(--ds-fail);   color: var(--ds-fail); }
.verdict-header--review { background: var(--ds-review-bg); border-color: var(--ds-review); color: var(--ds-review); }

.verdict-reasons {
  padding: var(--ds-space-6) var(--ds-space-9);
  margin: 0;
  font-size: var(--ds-type-sm);
  border-bottom: 1px solid var(--ds-border);
  background: var(--ds-bg);
}

.verdict-reasons li { margin-bottom: var(--ds-space-2); }
.verdict-reasons li:last-child { margin-bottom: 0; }

/* ── ALERTS ── */
.alert {
  border-radius: var(--ds-radius-md) !important;
  font-size: var(--ds-type-sm);
  padding: var(--ds-space-6) var(--ds-space-8);
  border-width: 0;
  border-left: 3px solid;
}

.alert-info    { background: var(--ds-info-bg);   color: var(--ds-info);   border-color: var(--ds-info); }
.alert-success { background: var(--ds-pass-bg);   color: var(--ds-pass);   border-color: var(--ds-pass); }
.alert-warning { background: var(--ds-review-bg); color: var(--ds-review); border-color: var(--ds-review); }
.alert-danger  { background: var(--ds-fail-bg);   color: var(--ds-fail);   border-color: var(--ds-fail); }

/* Raised by static/js/unit_guard.js when a unit switch cleared values that
   were typed under the previous system. Styled like a warning alert, but not
   dismissible: losing entered values is not something to be able to miss. */
.unit-guard-notice {
  background: var(--ds-review-bg);
  color: var(--ds-review);
  border-left: 3px solid var(--ds-review);
  font-size: var(--ds-type-sm);
  padding: var(--ds-space-6) var(--ds-space-8);
  margin-bottom: var(--ds-space-8);
}

/* ── TABLES ── */
/* Bootstrap paints a cell, not a row: `.table > :not(caption) > * > *` sets
   background-color and color on every td and th from its own --bs-table-*
   variables, which default to #fff on #212529. Styling .table itself therefore
   does nothing to the cells, and every engineering table in the product was a
   block of white with black text sitting inside a dark panel. The variables
   are the surface Bootstrap actually reads, so the palette is set here.

   --bs-table-bg is transparent rather than --ds-surface: a table sits in a
   panel, a card and a disclosure, and each of those already paints its own
   background. Naming a surface here would put the wrong one under a table in
   two of the three. */
.table {
  font-size: var(--ds-type-sm);
  --bs-table-bg: transparent;
  --bs-table-color: var(--ds-text);
  --bs-table-border-color: var(--ds-border);
  --bs-table-striped-bg: var(--ds-surface-sunken);
  --bs-table-striped-color: var(--ds-text);
  --bs-table-hover-bg: var(--ds-surface-sunken);
  --bs-table-hover-color: var(--ds-text);
  --bs-table-active-bg: var(--ds-surface-sunken);
  --bs-table-active-color: var(--ds-text);
  color: var(--ds-text);
  border-color: var(--ds-border);
}

.table thead th {
  font-size: var(--ds-type-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ds-text-secondary);
  background: var(--ds-bg);
  border-bottom: 2px solid var(--ds-border);
  padding: var(--ds-space-4) var(--ds-space-6);
  white-space: nowrap;
}

.table td {
  padding: var(--ds-space-4) var(--ds-space-6);
  border-color: var(--ds-border);
  vertical-align: middle;
}

/* ── DS-TABLE ──
   The product's own table, so a table stops being each author's choice.
   Three decisions, made once:

   No zebra striping. An engineering table here has four to twelve rows and is
   read by the hairline between them; a stripe adds a second rhythm the reader
   has to ignore, and in the dark palette it is one more surface to get wrong.
   --bordered draws the grid for a matrix that is read in both directions.

   The head is an inset strip, set in the eyebrow style, and it never wraps.
   Numbers align right under .num, in tabular figures, so a column of values
   lines up at the decimal point. A summary row is set off on the sunken
   surface; a row that carries a status carries the status tint.

   .ds-table-wrap is the overflow: a table is one of the three things allowed
   to scroll sideways at 400px, and it does so inside its own box. */
.ds-table-wrap {
  max-width: 100%;
  overflow-x: auto;
}

.ds-table {
  width: 100%;
  margin-bottom: 0;
  border-collapse: collapse;
  color: var(--ds-text);
  font-size: var(--ds-type-sm);
  vertical-align: middle;
}

.ds-table th,
.ds-table td {
  padding: var(--ds-space-4) var(--ds-space-6);
  border-bottom: 1px solid var(--ds-border-subtle);
  vertical-align: middle;
  text-align: left;
}

.ds-table thead th {
  font-size: var(--ds-type-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ds-text-secondary);
  background: var(--ds-surface-sunken);
  border-bottom: 2px solid var(--ds-border);
  white-space: nowrap;
}

.ds-table tbody tr:last-child > * { border-bottom-color: transparent; }

.ds-table--bordered th,
.ds-table--bordered td {
  border: 1px solid var(--ds-border);
}

.ds-table--hover tbody tr:hover > * { background: var(--ds-surface-sunken); }
/* Tables stacked under each other and read as one: every column the same width. */
.ds-table--even { table-layout: fixed; }

.ds-table .num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* A total or a section row, set off from the rows it summarises. */
.ds-table__row--summary > * { background: var(--ds-surface-sunken); }

/* The row the reader is looking for: the governing iteration, the selected
   point. Info rather than a status, because being picked out is not passing. */
.ds-table__row--highlight > * { background: var(--ds-info-bg); }
/* Selectable rows: click, Ctrl, Shift, Space; the row is the control. */
[data-assessment-row][tabindex] { cursor: default; }
[data-assessment-row]:focus-visible { outline: 2px solid var(--ds-accent); outline-offset: -2px; }
[data-assessment-row][aria-disabled="true"] { cursor: progress; }
.ds-table__row--info > *      { background: var(--ds-info-bg); }
.ds-table__row--pass > *      { background: var(--ds-pass-bg); }
.ds-table__row--review > *    { background: var(--ds-review-bg); }
.ds-table__row--fail > *      { background: var(--ds-fail-bg); }

/* Shared CLF sensitivity plot/table key. */
.clf-sensitivity-plot-column {
  width: 3.5rem;
  text-align: center;
}

.ds-table thead th.clf-sensitivity-dimension-header {
  text-transform: none;
}

.clf-sensitivity-point-marker {
  display: inline-block;
  width: 0.7rem;
  height: 0.7rem;
  border: 1px solid var(--ds-text-secondary);
  vertical-align: middle;
  transform: scale(0.6);
}

.clf-sensitivity-point-marker--circle {
  border-radius: 50%;
}

.clf-sensitivity-point-marker--diamond {
  transform: rotate(45deg) scale(0.6);
}

.clf-sensitivity-point-marker--triangle {
  width: 0;
  height: 0;
  border: 0;
  border-right: 0.4rem solid transparent;
  border-bottom: 0.7rem solid var(--clf-sensitivity-point-color);
  border-left: 0.4rem solid transparent;
}

/* Dark-header calculation table */
.table-calc thead th {
  background: var(--ds-solid-bg);
  color: var(--ds-solid-fg);
  border-color: var(--ds-solid-border);
}

code {
  border-radius: var(--ds-radius-xs);
  font-family: var(--ds-font-mono);
  font-size: 0.82em;
  background: var(--ds-bg);
  border: 1px solid var(--ds-border);
  padding: var(--ds-space-1) var(--ds-space-2);
  color: var(--ds-link);
}

/* ── WIZARD STEPPER ── */
.calculator-clear-all-actions {
  display: flex;
  justify-content: flex-end;
  margin-bottom: var(--ds-space-6);
}

.calculator-clear-all-actions[hidden] {
  display: none !important;
}

.calculator-clear-all-button {
  white-space: nowrap;
}

.content-body.calculator-inputs-cleared
  :is(.wizard-stepper .stepper-item, .wizard-side-nav .wizard-side-step)
  .stepper-circle {
  background: var(--ds-accent) !important;
  color: var(--ds-text-on-accent) !important;
  transition: none !important;
}

.wizard-stepper {
  /* One knob for the circle. The connector line derives its vertical offset
     from it, so the two cannot drift apart the way they did while nine
     wizards each carried their own copy of this component. */
  --ds-stepper-circle: 40px;
  box-sizing: border-box;
  width: fit-content;
  max-width: 100%;
  margin-inline: auto;
  overflow-x: auto;
  background: var(--ds-surface);
  padding: 20px;
  border-radius: var(--ds-radius-md);
  box-shadow: var(--ds-shadow-1);
}

.wizard-stepper > .stepper-container {
  display: flex;
  align-items: center;
  justify-content: center;
  width: max-content;
  max-width: none;
  min-width: 0;
  margin: 0 auto;
}

.stepper-item {
  display: flex;
  flex: 0 0 auto;
  flex-direction: column;
  align-items: center;
  cursor: pointer;
  outline: none;
}

.module-home-wizard-stepper .stepper-item {
  cursor: default;
}

.stepper-item:focus-visible .stepper-circle {
  box-shadow: 0 0 0 0.2rem var(--ds-focus-ring);
}

.stepper-circle {
  width: var(--ds-stepper-circle);
  height: var(--ds-stepper-circle);
  border-radius: 50%;
  background: var(--ds-border);
  color: var(--ds-text-muted);
  font-size: var(--ds-type-md);
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  position: relative;
  transition: all 0.3s;
}

.stepper-item.active .stepper-circle {
  background: var(--ds-info);
  color: var(--ds-on-status);
}

.stepper-item.completed .stepper-circle {
  background: var(--ds-pass);
  color: var(--ds-on-status);
}

.stepper-label {
  max-width: 100px;
  /* Room for the active-step bar drawn under the circle. */
  margin-top: 12px;
  color: var(--ds-text-muted);
  font-size: 12px;
  font-weight: 500;
  text-align: center;
  white-space: nowrap;
}

.stepper-item.completed .stepper-label {
  color: var(--ds-pass);
}

/* After .completed, so the active step's label is ink even when it is also
   completed. Status colours stay on the circle; the label and the bar under
   the circle are what say "you are here". */
.stepper-item.active .stepper-label {
  color: var(--ds-text);
  font-weight: 700;
}

.wizard-stepper > .stepper-container > .stepper-line {
  flex: 0 0 64px;
  width: 64px;
  min-width: 32px;
  height: 2px;
  /* Meet the circle at its centre. The container centres its children on the
     item's full height, label included, which left the line sitting 13.6px
     low. Three of the nine copied blocks patched that with a bottom margin;
     deriving the offset fixes it once, for every stepper. */
  align-self: flex-start;
  margin: calc(var(--ds-stepper-circle) / 2 - 1px) 10px 0;
  background: var(--ds-border);
}

.stepper-item.completed + .stepper-line,
.stepper-item.status-complete + .stepper-line,
.stepper-item.status-ok + .stepper-line {
  background: var(--ds-pass);
}

@media (max-width: 767.98px) {
  .wizard-stepper {
    width: 100%;
    padding: 16px 12px;
  }

  .wizard-stepper > .stepper-container {
    min-width: 0;
  }

  .wizard-stepper > .stepper-container > .stepper-line {
    flex-basis: 16px;
    width: 16px;
    min-width: 12px;
    margin-inline: 4px;
  }

  .wizard-stepper:not(.result-wizard-stepper) .stepper-label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }

  .wizard-stepper:not(.result-wizard-stepper) {
    --ds-stepper-circle: 35px;
  }

  .wizard-stepper:not(.result-wizard-stepper) .stepper-circle {
    font-size: 14px;
  }
}

/* ── STEP STATUS COLOURS ──
   Both the horizontal stepper and the desktop side nav list their steps as
   [data-step] items and must agree, so the suffix is applied inside :is()
   rather than after a comma, which would split it into two unrelated
   selectors. These rules were built as a string in wizard_step_status.js and
   injected as a <style> element at runtime, which meant the product's step
   colours were unreviewable, uncacheable and unreachable for a strict CSP.
   The !important is kept: it is what lets a status beat the plain
   active/completed colours further up this file. */
:is(.wizard-stepper .stepper-item, .wizard-side-nav .wizard-side-step).status-complete .stepper-circle,
:is(.wizard-stepper .stepper-item, .wizard-side-nav .wizard-side-step).status-ok .stepper-circle {
  background: var(--ds-pass) !important;
  color: var(--ds-on-status) !important;
}

:is(.wizard-stepper .stepper-item, .wizard-side-nav .wizard-side-step).status-missing .stepper-circle {
  background: var(--ds-accent) !important;
  color: var(--ds-text-on-accent) !important;
}

:is(.wizard-stepper .stepper-item, .wizard-side-nav .wizard-side-step).status-warning .stepper-circle {
  background: var(--ds-review) !important;
  color: var(--ds-on-status) !important;
}

:is(.wizard-stepper .stepper-item, .wizard-side-nav .wizard-side-step).status-error .stepper-circle {
  background: var(--ds-fail) !important;
  color: var(--ds-on-status) !important;
}

:is(.wizard-stepper .stepper-item, .wizard-side-nav .wizard-side-step).status-neutral .stepper-circle {
  background: var(--ds-info) !important;
  color: var(--ds-on-status) !important;
}

/* The active marker: a bar under the circle in the text colour. A status
   class repaints the circle, so a completed active step was the same green
   as its neighbours and the old faint ring was the only cue. The bar reads
   over any fill, in both palettes, and on a phone where the labels are
   hidden. It is positioned, not laid out, so no step changes size and the
   connector line stays put. */
:is(.wizard-stepper .stepper-item, .wizard-side-nav .wizard-side-step).active .stepper-circle::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -7px;
  height: 3px;
  border-radius: var(--ds-radius-pill);
  background: var(--ds-text);
}

:is(.wizard-stepper .stepper-item, .wizard-side-nav .wizard-side-step).active .stepper-label {
  color: var(--ds-text) !important;
  font-weight: 700;
}

/* ── SELECTION CARDS (geometry/variant/damage-type picker grids, all wizards) ── */
.selection-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--ds-space-8);
  margin-top: var(--ds-space-6);
}

.variant-cards {
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}

.pc-level2-variant-symbol {
  display: block;
  width: 6.5rem;
  height: 4rem;
  margin: 0 auto var(--ds-space-5);
  overflow: visible;
}

/* Two wizards need a denser grid than the 260px default. Both used to get it
   by redefining .selection-cards in their own <style> block, and because both
   pages also tag the grid .crack-cards, the same marker class meant a 140px
   grid on one page and a wrap-flex row on the other. Named variants say which
   is which. */
.selection-cards--compact {
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--ds-space-7);
  margin-top: var(--ds-space-4);
}

.selection-cards--compact .selection-card {
  padding: var(--ds-space-8) var(--ds-space-5);
}

.selection-cards--inline {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ds-space-8);
}

.selection-cards--inline .selection-card {
  flex: 1;
  min-width: 150px;
  max-width: 200px;
}

@media (max-width: 767.98px) {
  .selection-cards--inline {
    flex-direction: column;
  }

  .selection-cards--inline .selection-card {
    max-width: none;
  }
}

.selection-card {
  border-radius: var(--ds-radius-lg);
  border: 2px solid var(--ds-border);
  padding: var(--ds-space-9);
  text-align: center;
  cursor: pointer;
  background: var(--ds-surface);
  transition: border-color 0.15s, background 0.15s;
}

.selection-card:hover:not(.disabled) {
  border-color: var(--ds-secondary);
  background: var(--ds-info-bg);
}

.selection-card.selected,
.selection-card.active {
  border-color: var(--ds-secondary);
  background: var(--ds-info-bg);
  outline: 2px solid var(--ds-secondary);
  outline-offset: -2px;
}

.selection-card.disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.selection-card h5 {
  margin: var(--ds-space-4) 0;
}

.selection-card ul {
  list-style: none;
  padding: 0;
  margin-top: var(--ds-space-7);
}

.selection-card ul li {
  padding: var(--ds-space-2) 0;
}

.selection-card ul li::before {
  content: "✓ ";
  color: var(--ds-pass);
  font-weight: bold;
}

/* Reference diagrams (e.g. crack geometry) embedded inside a selection card */
.crack-diagram {
  margin-bottom: var(--ds-space-5);
}

.crack-diagram svg {
  display: block;
  margin: 0 auto;
}

/* ── FORM GRID (legacy form.as_p compatibility) ── */
.form-grid p {
  display: grid;
  grid-template-columns: minmax(0,180px) minmax(0,120px) auto;
  column-gap: var(--ds-space-4);
  align-items: center;
  margin-bottom: var(--ds-space-4);
}

.form-grid p input,
.form-grid p select { max-width: 100%; }

/* The twelve GML level 1 and level 2 form pages lay their fields out in a
   table and want one fixed input width for the column to line up. Each of
   them carried the same rule in its own <style> block, unscoped, so it
   resized every input and select on the page. Scoped to the form now. */
.fixed-width-fields input[type="text"],
.fixed-width-fields input[type="number"],
.fixed-width-fields select {
  width: 230px;
}

/* ── APP NAVBAR (legacy override) ── */
.app-navbar {
  background: var(--ds-chrome-bg) !important;
  border-bottom: 2px solid var(--ds-accent);
  padding: 0;
  position: sticky;
  top: 0;
  z-index: 1040;
}

/* ── DROPDOWN menus in Bootstrap context ── */
.dropdown-menu {
  border-radius: var(--ds-radius-lg) !important;
  overflow: hidden;
}

/* ── WIZARD FOOTER (the back/continue bar under a wizard step) ──
   Written out in two GML wizards and referenced by a third. */
.wizard-footer {
  border-top: 1px solid var(--ds-border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--ds-space-7) var(--ds-space-10);
}

/* ── COLLAPSE CHEVRON ──
   The caret that flips when a Bootstrap collapse opens. */
.collapse-icon {
  transition: transform 0.2s ease-in-out;
}

[data-bs-toggle="collapse"][aria-expanded="true"] .collapse-icon {
  transform: rotate(180deg);
}

/* ── TOOLTIPS AND THE HELP ICON THAT OPENS THEM ──
   Both were written inline on the CLF cylinder and round bar wizards, which
   meant a help icon looked like a help icon on two pages out of the product.
   The 13px tooltip text is snapped to the type scale here. */
.info-tooltip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  margin-left: 6px;
  color: var(--ds-text-muted);
  cursor: help;
  vertical-align: middle;
  transition: color 0.2s ease;
}

.info-tooltip:hover {
  color: var(--ds-info);
}

.info-tooltip i {
  font-size: var(--ds-type-md);
  line-height: 1;
  vertical-align: middle;
}

.reference-thickness-info {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.25rem;
  height: 1.25rem;
  color: var(--ds-text-muted);
  line-height: 1;
  text-decoration: none;
}

.reference-thickness-info:hover,
.reference-thickness-info:focus-visible {
  color: var(--ds-info);
}

.tooltip-inner {
  max-width: 300px;
  padding: 8px 12px;
  font-size: var(--ds-type-base);
  line-height: 1.4;
  text-align: left;
}

/* ── UTILITY ── */
.text-muted   { color: var(--ds-text-muted) !important; }
.text-primary { color: var(--ds-heading) !important; }

/* ── BOOTSTRAP'S OWN PALETTE CLASSES, PUT ON THE THEME ──
   These are a second colour system and they do not follow the palette on
   their own: .table-light stays a light grey row on a dark page, .text-success
   stays a forest green that does not read as "pass" there, and .bg-light
   paints a white panel inside a dark card.

   They are remapped here rather than renamed in each template because ninety
   templates use them and a rename is ninety chances to miss one. New work
   still reaches for the token directly; this is what keeps the pages that
   have not been rewritten yet from looking broken in the meantime.

   .text-danger and .text-success take the status tokens rather than the
   status *tints*: these paint text on the page, and the tints are the
   backgrounds that text sits on. */
.text-success { color: var(--ds-pass) !important; }
.text-danger  { color: var(--ds-fail) !important; }
.text-warning { color: var(--ds-review) !important; }
.text-info    { color: var(--ds-info) !important; }
.text-white   { color: var(--ds-solid-fg) !important; }
.bg-light     { background-color: var(--ds-surface-sunken) !important; }
.bg-dark      { background-color: var(--ds-chrome-bg) !important; }
.border-primary { border-color: var(--ds-border-strong) !important; }

/* Not an outcome, so it takes no status colour: an assumption, a limitation or
   a note qualifying the result above it. .alert-light is Bootstrap's own
   near-white and lands here beside it, because a note that is white on a dark
   page reads as a hole rather than as a note. */
.alert-secondary,
.alert-light {
  background: var(--ds-surface-sunken);
  color: var(--ds-text);
  border-color: var(--ds-border-strong);
}

/* Bootstrap's own .card is white with near-black text, and 494 usages of it
   are still in the product outside the .ds-card surfaces this design system
   defines. Every one of them was a white panel on a dark page. The rule is the
   same one the tables needed: remap it here rather than rename it in ninety
   templates, and let new work reach for .ds-card directly.

   .card-header keeps its own utilities: a .bg-dark.text-white header carries
   !important and still wins, which is what the export panel wants. */
.card {
  background-color: var(--ds-surface);
  border-color: var(--ds-border-subtle);
  color: var(--ds-text);
}

.card-body,
.card-title,
.card-text {
  color: var(--ds-text);
}

.card-header,
.card-footer {
  background-color: var(--ds-surface-sunken);
  border-color: var(--ds-border-subtle);
  color: var(--ds-text);
}

/* ── DS-MODAL ──
   A native <dialog>, so the browser owns what Bootstrap's modal reimplemented:
   the focus trap, Escape, the backdrop and the inert page behind it. The
   product's script, static/js/ds_modal.js, only wires the openers and closers
   and closes on a backdrop click. Nothing here reads a Bootstrap variable.

   Three widths, and no others: the default for a note or a help table, --wide
   for a picker with a form in it, --xl for a guide read like a page. */
.ds-modal {
  width: min(100% - (2 * var(--ds-space-11)), 36rem);
  max-height: calc(100vh - (2 * var(--ds-space-11)));
  margin: auto;
  padding: 0;
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-lg);
  background: var(--ds-surface);
  color: var(--ds-text);
  box-shadow: var(--ds-shadow-2);
  overflow: hidden;
}

.ds-modal--wide { width: min(100% - (2 * var(--ds-space-11)), 52rem); }
.ds-modal--xl   { width: min(100% - (2 * var(--ds-space-11)), 72rem); }

.ds-modal::backdrop { background: var(--ds-overlay); }

/* The dialog is a flex column so the body scrolls and the head and foot
   stay put, which is what makes a long guide usable on a phone. */
.ds-modal__frame {
  display: flex;
  flex-direction: column;
  max-height: calc(100vh - (2 * var(--ds-space-11)));
}

.ds-modal__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--ds-space-8);
  padding: var(--ds-space-8) var(--ds-space-9);
  border-bottom: 1px solid var(--ds-border-subtle);
}

.ds-modal__title {
  margin: 0;
  font-size: var(--ds-type-md);
  font-weight: 700;
  color: var(--ds-heading);
}

.ds-modal__lede {
  margin: var(--ds-space-2) 0 0;
  font-size: var(--ds-type-xs);
  color: var(--ds-text-muted);
}

.ds-modal__body {
  padding: var(--ds-space-9);
  overflow: auto;
}

.ds-modal__foot {
  display: flex;
  justify-content: flex-end;
  gap: var(--ds-space-6);
  padding: var(--ds-space-8) var(--ds-space-9);
  border-top: 1px solid var(--ds-border-subtle);
}

/* The close control: an icon button, square, quiet until hovered. */
.ds-modal__close {
  flex: none;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--ds-radius-sm);
  background: transparent;
  color: var(--ds-text-muted);
  font-size: var(--ds-type-md);
  line-height: 1;
}

.ds-modal__close:hover {
  background: var(--ds-surface-sunken);
  color: var(--ds-text);
}

.ds-modal__close:focus-visible {
  outline: 3px solid var(--ds-focus-ring);
  outline-offset: 1px;
}

@media (max-width: 575.98px) {
  .ds-modal,
  .ds-modal--wide,
  .ds-modal--xl {
    width: calc(100% - (2 * var(--ds-space-6)));
    max-height: calc(100vh - (2 * var(--ds-space-6)));
  }
  .ds-modal__frame { max-height: calc(100vh - (2 * var(--ds-space-6))); }
  .ds-modal__head,
  .ds-modal__body,
  .ds-modal__foot { padding: var(--ds-space-7); }
}

/* Bootstrap paints a disabled control with its light "secondary" grey, which
   the dark palette leaves as near-white text on #E9ECEF. Part 5's diameter
   field is disabled whenever the other basis is selected, so this is a control
   a reader meets rather than a hypothetical one.

   A readonly field takes the sunken background but keeps the text colour: it
   is not editable, but what it says is a value somebody has to read. */
.form-control:disabled,
.form-select:disabled {
  background-color: var(--ds-surface-sunken);
  color: var(--ds-text-muted);
  border-color: var(--ds-border-subtle);
}

.form-control[readonly],
output.form-control {
  background-color: var(--ds-surface-sunken);
  border-color: var(--ds-border-subtle);
}

/* An <hr> is currentColor at 25%, so a section rule between wizard steps was a
   near-black line drawn on a near-black page. */
hr {
  color: var(--ds-border);
  opacity: 1;
}

/* Bootstrap draws a table row through its own --bs-table-* variables, so a
   colour set on the row itself would be overridden by them. */
.table-light {
  --bs-table-bg: var(--ds-surface-sunken);
  --bs-table-color: var(--ds-text);
  --bs-table-border-color: var(--ds-border-subtle);
}

.border-accent { border-color: var(--ds-accent) !important; }

hr { border-color: var(--ds-border); opacity: 1; }

/* ── UTILITIES THAT REPLACED style="" ATTRIBUTES (A7 part 2) ──
   Every class here exists because the same declaration was written inline in
   the templates, often dozens of times. They are named u-* so it stays
   obvious which layer they belong to, and they take their values from the
   scales rather than restating the pixel value that was inline.

   Two deliberate choices:

   .u-hidden is not `display: none !important`, unlike Bootstrap's .d-none.
   About forty of these elements are shown again by a script writing
   element.style.display, which is still allowed (style-src-attr governs the
   attribute in the markup, not CSSOM writes). An inline declaration beats a
   class rule only while that rule is not !important, so !important here would
   break every one of those toggles silently.

   The font sizes are snapped to the nearest step of the type scale. Inline,
   they were 0.6, 0.75, 0.76, 0.78, 0.8, 0.82, 0.85 and 0.9rem plus four em
   variants, which is the same drift A1 removed from the stylesheet. */
.u-hidden { display: none; }

.u-type-2xs  { font-size: var(--ds-type-2xs); }
.u-type-xs   { font-size: var(--ds-type-xs); }
.u-type-sm   { font-size: var(--ds-type-sm); }
.u-type-base { font-size: var(--ds-type-base); }
.u-type-md   { font-size: var(--ds-type-md); }

/* An inset area inside a surface: a tinted panel, a well, a sub-form. This is
   what Bootstrap's bg-light was reached for, and bg-light is a near-white that
   turns into a bright block on a dark card. */
.u-surface-sunken { background: var(--ds-surface-sunken); }

.u-mono      { font-family: var(--ds-font-mono); }
.u-nowrap    { white-space: nowrap; }
.u-scroll-x  { overflow-x: auto; }
.u-flush     { padding: 0; }
.u-center    { display: block; margin-inline: auto; }

/* Column width hints for the wide engineering tables. These percentages are
   the tables' own data, not a scale: a sensitivity run has ten columns whose
   proportions were chosen for their contents, and there is nothing to round
   them to. They are here rather than inline only so the attribute can go. */
.u-col-status { width: 80px; }
.u-col-9  { width: 9%; }
.u-col-10 { width: 10%; }
.u-col-11 { width: 11%; }
.u-col-13 { width: 13%; }
.u-col-15 { width: 15%; }
.u-col-17 { width: 17%; }

/* A CSV or file preview: as much as fits, then scroll. */
.u-preview-scroll {
  max-height: 200px;
  overflow-y: auto;
  font-size: var(--ds-type-sm);
}

/* The first rows of a pasted or uploaded CSV, shown back before it is used:
   an inset box in the mono face. Part 10's copy was `border rounded p-2
   bg-light`, which is Bootstrap's light grey on a dark page. */
.csv-preview-content {
  margin: 0;
  padding: var(--ds-space-4) var(--ds-space-6);
  border: 1px solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-sm);
  background: var(--ds-surface-sunken);
  color: var(--ds-text);
  font-family: var(--ds-font-mono);
}

/* A card body that hands its whole width to a wide table: no padding of its
   own, and the table scrolls inside it rather than pushing the page sideways
   (A2 requires no horizontal scroll on the page itself). */
.ds-card-body.u-table-host {
  padding: 0;
  overflow-x: auto;
}

/* ── LEGACY FORM TABLE ──
   The GML, LML and PC geometry forms render `{% for field in form %}` into a
   two-column table: the widget on the left, its label and help text on the
   right. Twenty-six templates carried the same three inline declarations for
   it. Keep the selectors on the class rather than on bare td, so the result
   tables elsewhere are untouched. */
.form-table td {
  vertical-align: top;
  padding: var(--ds-space-2) var(--ds-space-4);
}

.form-table .form-table-submit {
  padding-top: var(--ds-space-7);
}

/* Help text under a field label. Left at the size it was rendering at, the
   type scale's nearest step, rather than moved to --ds-type-xs: A5 is about
   to rewrite what these strings say, and changing what they say and how big
   they are in the same pass makes neither reviewable. */
.form-help {
  font-size: var(--ds-type-sm);
}

/* ── DEV COMPONENTS PAGE ── */
.ds-swatch {
  width: 56px;
  height: 56px;
  display: inline-block;
  border: 1px solid var(--ds-border);
}

.ds-swatch-label {
  font-size: var(--ds-type-xs);
  color: var(--ds-text-muted);
  margin-top: var(--ds-space-2);
  font-family: var(--ds-font-mono);
}

/* Scale rulers on /dev/components/. The step is carried in a data attribute
   rather than a style attribute so this page does not add to the inline-style
   pile that A7 has to clear before style-src can drop 'unsafe-inline'. */
.ds-scale-row {
  display: flex;
  align-items: center;
  gap: var(--ds-space-6);
  padding: var(--ds-space-2) 0;
  border-bottom: 1px solid var(--ds-border);
}

.ds-scale-name {
  flex: 0 0 9rem;
  font-family: var(--ds-font-mono);
  font-size: var(--ds-type-xs);
  color: var(--ds-text-secondary);
}

.ds-scale-value {
  flex: 0 0 5rem;
  font-family: var(--ds-font-mono);
  font-size: var(--ds-type-2xs);
  color: var(--ds-text-muted);
}

.ds-space-bar {
  height: 0.75rem;
  background: var(--ds-secondary);
  flex: 0 0 auto;
}

.ds-space-bar[data-step="1"]  { width: var(--ds-space-1); }
.ds-space-bar[data-step="2"]  { width: var(--ds-space-2); }
.ds-space-bar[data-step="3"]  { width: var(--ds-space-3); }
.ds-space-bar[data-step="4"]  { width: var(--ds-space-4); }
.ds-space-bar[data-step="5"]  { width: var(--ds-space-5); }
.ds-space-bar[data-step="6"]  { width: var(--ds-space-6); }
.ds-space-bar[data-step="7"]  { width: var(--ds-space-7); }
.ds-space-bar[data-step="8"]  { width: var(--ds-space-8); }
.ds-space-bar[data-step="9"]  { width: var(--ds-space-9); }
.ds-space-bar[data-step="10"] { width: var(--ds-space-10); }
.ds-space-bar[data-step="11"] { width: var(--ds-space-11); }
.ds-space-bar[data-step="12"] { width: var(--ds-space-12); }
.ds-space-bar[data-step="13"] { width: var(--ds-space-13); }

.ds-type-sample[data-step="2xs"]  { font-size: var(--ds-type-2xs); }
.ds-type-sample[data-step="xs"]   { font-size: var(--ds-type-xs); }
.ds-type-sample[data-step="sm"]   { font-size: var(--ds-type-sm); }
.ds-type-sample[data-step="base"] { font-size: var(--ds-type-base); }
.ds-type-sample[data-step="md"]   { font-size: var(--ds-type-md); }
.ds-type-sample[data-step="lg"]   { font-size: var(--ds-type-lg); }
.ds-type-sample[data-step="xl"]   { font-size: var(--ds-type-xl); }
.ds-type-sample[data-step="2xl"]  { font-size: var(--ds-type-2xl); }

.component-section {
  margin-bottom: var(--ds-space-12);
}

.component-section-title {
  font-size: var(--ds-type-xs);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ds-text-muted);
  border-bottom: 1px solid var(--ds-border);
  padding-bottom: var(--ds-space-4);
  margin-bottom: var(--ds-space-9);
}

/* ── INLINE VALIDATION BADGES ──
   The pass/warn/fail pill that gml_validation.js and lml_validation.js put
   under a field while the engineer types. Both scripts built this CSS as a
   string and injected it as a <style> element, in Bootstrap's subtle palette
   rather than the product's, so the same badge read as two different things
   depending on which module you were in. One definition, on the status
   tokens. The class names keep their module prefixes because the scripts
   assign them. */
.gml-vbadge,
.lml-vbadge {
  display: none;
  padding: var(--ds-space-2) var(--ds-space-4);
  border-radius: var(--ds-radius-sm);
  font-size: var(--ds-type-sm);
  transition: all 0.2s ease;
}

.gml-vbadge.gml-ok,
.lml-vbadge.lml-ok {
  display: block !important;
  background: var(--ds-pass-bg);
  color: var(--ds-pass);
}

.gml-vbadge.gml-warn,
.lml-vbadge.lml-warn {
  display: block !important;
  background: var(--ds-review-bg);
  color: var(--ds-review);
}

.gml-vbadge.gml-fail,
.lml-vbadge.lml-fail {
  display: block !important;
  background: var(--ds-fail-bg);
  color: var(--ds-fail);
}

/* ── MAIN STEP RESULTS (main_step_results.html) ──
   The summary of output values that sits above the trace. */
.main-step-results__list {
  display: grid;
}

.main-step-result {
  border-bottom: 1px solid var(--ds-border);
  padding: var(--ds-space-7) var(--ds-space-8);
}

.main-step-result:last-child {
  border-bottom: 0;
}

.main-step-result__heading {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: var(--ds-space-4);
}

.main-step-result__number {
  color: var(--ds-heading);
  font-weight: 700;
}

.main-step-result__name {
  font-weight: 600;
}

.main-step-result__outputs {
  display: grid;
  gap: var(--ds-space-4);
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  margin-top: var(--ds-space-5);
}

.main-step-result__output {
  border-radius: var(--ds-radius-sm);
  align-items: baseline;
  background: var(--ds-bg);
  border: 1px solid var(--ds-border);
  display: flex;
  gap: var(--ds-space-5);
  justify-content: space-between;
  min-width: 0;
  padding: var(--ds-space-4) var(--ds-space-5);
}

.main-step-result__label,
.main-step-result__empty {
  font-size: var(--ds-type-sm);
}

.main-step-result__value {
  font-family: var(--ds-font-mono);
  font-size: var(--ds-type-sm);
  text-align: right;
}

/* ── RESULTS BLOCK (results_block.html) ── */

/* Step row */
.rb-step {
  border-bottom: 1px solid var(--ds-border);
}

.rb-step--nested {
  background: var(--ds-bg);
  border-left: 2px solid var(--ds-border);
  margin: var(--ds-space-4) var(--ds-space-4) 0;
  border-bottom: none;
}

.rb-step-header {
  display: flex;
  align-items: center;
  padding: var(--ds-space-5) var(--ds-space-8);
  cursor: pointer;
  font-size: var(--ds-type-sm);
  font-weight: 500;
  color: var(--ds-text);
  user-select: none;
  gap: var(--ds-space-2);
  transition: background 0.1s;
  flex-wrap: wrap;
}

.rb-step-header:hover { background: rgba(46,109,164,0.05); }

.rb-step-name { font-weight: 600; }

/* step_number was carried all the way through the trace and rendered nowhere. */
.rb-step-number {
  font-size: var(--ds-type-2xs);
  font-weight: 700;
  color: var(--ds-link);
  letter-spacing: 0.03em;
}

/* CalcRecord.description, the prose the orchestrator wrote about the step. */
.rb-step-description {
  font-size: var(--ds-type-xs);
  color: var(--ds-text-secondary);
  margin: 0 0 var(--ds-space-4);
}

/* Permalink to one step, so it can be cited in an email. Kept quiet until the
   row is hovered or the link itself is focused. */
.rb-step-anchor {
  color: var(--ds-text-muted);
  font-size: var(--ds-type-2xs);
  margin-left: var(--ds-space-2);
  opacity: 0;
  text-decoration: none;
  transition: opacity 0.1s;
}

.rb-step-header:hover .rb-step-anchor,
.rb-step-anchor:focus-visible {
  opacity: 1;
}

.rb-step-anchor:hover { color: var(--ds-link); }

.ds-ref-badge {
  border-radius: var(--ds-radius-xs);
  font-size: var(--ds-type-2xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  background: rgba(46,109,164,0.08);
  color: var(--ds-link);
  border: 1px solid rgba(46,109,164,0.18);
  padding: var(--ds-space-1) var(--ds-space-3);
}

/* The badge reads at two sizes, and until A7 part 2 only one of them was in
   the stylesheet. Thirty-four of its thirty-eight uses are the geometry,
   level and variant badges in a result-page header, and every one of those
   overrode the size inline. Those are this variant; the base size stays for
   the GOVERNING markers and the equation references inside a trace step,
   which sit in running text and should not compete with it. */
.ds-ref-badge--title {
  font-size: var(--ds-type-sm);
}

.step-number-badge {
  font-size: var(--ds-type-2xs);
  font-weight: 700;
  background: var(--ds-solid-bg);
  color: var(--ds-solid-fg);
  padding: var(--ds-space-1) var(--ds-space-3);
  letter-spacing: 0.03em;
}

.rb-step-preview {
  font-size: var(--ds-type-sm);
  font-family: var(--ds-font-mono);
  margin-left: auto;
}

/* Step body */
.rb-step-body {
  border-radius: var(--ds-radius-md);
  padding: var(--ds-space-6) var(--ds-space-8) var(--ds-space-8) var(--ds-space-11);
  border-top: 1px solid var(--ds-border);
  background: var(--ds-surface);
}

/* Open/closed is expressed once, as aria-expanded on the header, and the two
   visual consequences follow from it here. The JS used to write both an inline
   display and an inline transform, which meant the state lived in three places
   and the server-rendered state was only right by luck. */
.rb-step-header[aria-expanded="false"] + .rb-step-body {
  display: none;
}

.rb-chevron {
  font-size: var(--ds-type-xs);
  transition: transform 0.15s;
}

.rb-step-header[aria-expanded="true"] .rb-chevron {
  transform: rotate(90deg);
}

.rb-section { margin-bottom: var(--ds-space-7); }

.rb-section-label {
  font-size: var(--ds-type-2xs);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ds-text-muted);
  margin-bottom: var(--ds-space-3);
}

/* Variable grid */
.rb-var-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--ds-space-4);
}

.rb-var-card {
  border-radius: var(--ds-radius-sm);
  background: var(--ds-bg);
  border: 1px solid var(--ds-border);
  padding: var(--ds-space-4) var(--ds-space-5);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ds-space-2) var(--ds-space-2);
  font-size: var(--ds-type-sm);
}

.rb-var-card--output {
  border-color: var(--ds-info);
  background: var(--ds-info-bg);
}

.rb-symbol { font-family: var(--ds-font-mono); color: var(--ds-text-secondary); }
.rb-eq     { color: var(--ds-text-muted); }
.rb-val    { font-family: var(--ds-font-mono); font-weight: 600; }
.rb-val--result { color: var(--ds-link); font-size: var(--ds-type-base); }
.rb-unit   { font-size: var(--ds-type-xs); color: var(--ds-text-muted); }
.rb-desc   { width: 100%; color: var(--ds-text-muted); font-size: var(--ds-type-xs); margin-top: var(--ds-space-1); }

/* KaTeX formula containers */
.rb-formula-katex {
  border-radius: var(--ds-radius-sm);
  background: var(--ds-bg);
  border: 1px solid var(--ds-border);
  border-left: 3px solid var(--ds-secondary);
  padding: var(--ds-space-5) var(--ds-space-8);
  overflow-x: auto;
  font-size: var(--ds-type-md);
}

.rb-formula-substituted {
  margin-top: var(--ds-space-3);
  border-left-color: var(--ds-accent);
  font-size: var(--ds-type-base);
}

.rb-formula-ref {
  margin-top: var(--ds-space-3);
  text-align: right;
  font-size: var(--ds-type-xs);
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--ds-text-muted);
}

/*
 * KaTeX fallback. If KaTeX fails to load, static/js/calc_trace.js puts
 * .katex-unavailable on <html>. The LaTeX source would then sit on the page
 * as raw backslash noise, so we swap it for the plain-text formula, which is
 * always readable. A step that has no plain formula keeps its LaTeX source,
 * unreadable beats invisible.
 */
.rb-formula-fallback,
.rb-katex-notice { display: none; }
.katex-unavailable .rb-katex-notice { display: block; }
.katex-unavailable .rb-formula-fallback { display: block; }
.katex-unavailable .rb-formula-src { display: none; }

/* Plain-text (non-LaTeX) formula box */
.rb-formula-plain code {
  font-family: var(--ds-font-mono);
  font-size: var(--ds-type-base);
  color: var(--ds-text);
  background: none;
  padding: 0;
  white-space: normal;
  word-break: break-word;
}

/* Chart embedded in a calculation trace step */
.rb-chart-wrap {
  border-radius: var(--ds-radius-md);
  background: var(--ds-surface);
  border: 1px solid var(--ds-border);
  padding: var(--ds-space-6);
  max-width: 640px;
}

/* Children container */
.rb-children {
  border: 1px solid var(--ds-border);
  border-left: 2px solid var(--ds-border);
}

/* Heading-only step — visual section divider within a calculation trace */
.rb-step-heading {
  padding: var(--ds-space-4) var(--ds-space-8);
  font-size: var(--ds-type-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ds-link);
  background: var(--ds-bg);
  border-bottom: 1px solid var(--ds-border);
}


/* ===========================================================================
   Shared assessment-result components
   Used by every module's result_summary so per-module CSS (.lml-/.pc-/.cr-*)
   can be retired in favour of one consistent design-system look.
   =========================================================================== */

/* Metric / key-value cards (RSF, MAWP, thicknesses, …) */
.ds-metric-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--ds-space-5);
}
.ds-metric-card {
  border-radius: var(--ds-radius-md);
  background: var(--ds-bg);
  border: 1px solid var(--ds-border);
  padding: var(--ds-space-5) var(--ds-space-6);
}
.ds-metric-card--pass { border-color: var(--ds-pass);   background: var(--ds-pass-bg); }
.ds-metric-card--fail { border-color: var(--ds-fail);   background: var(--ds-fail-bg); }
.ds-metric-card--warn { border-color: var(--ds-review); background: var(--ds-review-bg); }
.ds-metric-label {
  font-size: var(--ds-type-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ds-text-muted);
  margin-bottom: var(--ds-space-2);
}
.ds-metric-value {
  font-family: var(--ds-font-mono);
  font-size: var(--ds-type-md);
  font-weight: 600;
  color: var(--ds-text);
}

/* Inline check chip used in compact criteria lists */
.ds-check-line {
  display: flex;
  align-items: center;
  gap: var(--ds-space-3);
  font-size: var(--ds-type-sm);
  padding: var(--ds-space-1) 0;
}

/* Discreet disclaimer link at the bottom of every app page (punkt 15) */
.app-footer {
  padding: var(--ds-space-8) var(--ds-space-11) var(--ds-space-10);
  font-size: var(--ds-type-sm);
  color: var(--ds-text-muted);
  border-top: 1px solid var(--ds-border);
  margin-top: var(--ds-space-10);
}
.app-footer a {
  color: var(--ds-text-muted);
  text-decoration: underline;
}

/* Cookie choice, see home/_cookie_consent.html. The two choices are the same
   size and weight: refusing must be as easy as accepting. */
.cookie-consent {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1050;
  display: none;
  align-items: center;
  justify-content: space-between;
  gap: var(--ds-space-8);
  flex-wrap: wrap;
  background: var(--ds-solid-bg);
  color: var(--ds-solid-fg);
  padding: var(--ds-space-7) var(--ds-space-10);
  font-size: var(--ds-type-base);
}
.cookie-consent--visible {
  display: flex;
}
.cookie-consent p {
  margin: 0;
}
.cookie-consent a {
  color: var(--ds-text-on-dark);
  text-decoration: underline;
}
.cookie-consent__actions {
  display: flex;
  gap: var(--ds-space-4);
  flex-wrap: wrap;
}
.cookie-consent__choice {
  min-width: 11rem;
  border: 1px solid var(--ds-text-on-dark);
  color: var(--ds-text-on-dark);
  background: transparent;
  font-weight: 600;
}
.cookie-consent__choice:hover,
.cookie-consent__choice:focus-visible {
  background: var(--ds-text-on-dark);
  color: var(--ds-solid-bg);
}
.cookie-consent__choice--accept {
  background: var(--ds-text-on-dark);
  color: var(--ds-solid-bg);
}
.cookie-consent__choice--accept:hover,
.cookie-consent__choice--accept:focus-visible {
  background: transparent;
  color: var(--ds-text-on-dark);
}

/* Module home header. The heading reads left like the rest of the app, while
   the shared schematic is centered against the full content page rather than
   against a narrow right-hand column. */
.module-home-header {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--ds-space-8);
  margin-bottom: var(--ds-space-11);
}

.module-home-heading {
  min-width: 0;
  flex: 1 1 auto;
  max-width: 60ch;
}

/* .page-subtitle carries a large bottom margin so it can end a page header on
   its own. Here the intro follows it, so the gap belongs after the pair. */
.module-home-standard {
  margin-bottom: var(--ds-space-2);
}

.module-home-intro {
  font-size: var(--ds-type-base);
  color: var(--ds-text-secondary);
  margin-bottom: 0;
}

.module-home-figure {
  width: min(100%, 300px);
  max-width: 100%;
  align-self: center;
  text-align: center;
}

.module-home-figure :is(svg, img) {
  display: block;
  margin-inline: auto;
  max-width: 100%;
  height: auto;
}

/* Header artwork keeps its red damage marks; only structural ink follows the
   page palette. Dimensioned drawings on white figure plates are separate. */
.module-home-figure [stroke="#1B3A5C" i] {
  stroke: var(--ds-schematic-ink);
}

.module-home-figure [fill="#1B3A5C" i] {
  fill: var(--ds-schematic-ink);
}

.module-home-icon {
  font-size: var(--ds-type-2xl);
  color: var(--ds-link);
}

@media (max-width: 767.98px) {
  .module-home-header {
    gap: var(--ds-space-8);
  }

  .module-home-figure {
    width: min(100%, 300px);
  }
}

/* Component selection grid — the card grid every assessment module lands on.
   Each card links straight into that module's wizard. */
.component-selection-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18.75rem, 1fr));
  gap: var(--ds-space-10);
  margin-bottom: var(--ds-space-12);
}

/* Cards share the grid's row tracks so the header/body divider lines up across
   a row however long each title or description runs. Without subgrid the flex
   fallback still bottom-aligns the button; only the divider drifts. */
.component-card {
  border-radius: var(--ds-radius-lg);
  display: flex;
  flex-direction: column;
  border: 1px solid var(--ds-border);
  background: var(--ds-surface);
  overflow: hidden;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

@supports (grid-template-rows: subgrid) {
  .component-card {
    display: grid;
    grid-row: span 2;
    grid-template-rows: subgrid;
  }
}

.component-card:hover {
  border-color: var(--ds-secondary);
  box-shadow: 0 0.5rem 1.5rem rgba(46, 109, 164, 0.15);
}

.component-card-header {
  padding: var(--ds-space-11) var(--ds-space-10) var(--ds-space-9);
  text-align: center;
  border-bottom: 1px solid var(--ds-border);
  background: var(--ds-bg);
}

.component-card-icon {
  color: var(--ds-link);
  margin-bottom: var(--ds-space-7);
}

.component-card-figure {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 5rem;
  margin-bottom: var(--ds-space-5);
  /* The figures are line art drawn with stroke: currentColor, so this is the
     ink for the whole drawing. A fixed grey is invisible on a dark card. */
  color: var(--ds-text-muted);
}

.component-card-figure svg {
  display: block;
  width: 8.25rem;
  height: 5rem;
  overflow: visible;
}

.component-card-figure .component-figure-body,
.component-card-figure .component-figure-detail {
  fill: none;
  stroke: currentColor;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.component-card-figure .component-figure-body {
  stroke-width: 2;
}

.component-card-figure .component-figure-fill {
  fill: var(--ds-surface-sunken);
}

.component-card-figure .component-figure-detail {
  stroke-width: 1.4;
}

.component-card-figure .component-figure-crack {
  fill: none;
  stroke: var(--ds-fail);
  stroke-width: 2.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.component-card-header h5 {
  font-size: var(--ds-type-xl);
  font-weight: 600;
  margin-bottom: var(--ds-space-4);
  color: var(--ds-text);
}

.component-card-desc {
  font-size: var(--ds-type-base);
  color: var(--ds-text-muted);
  margin-bottom: 0;
}

.component-card-body {
  display: flex;
  flex-direction: column;
  padding: var(--ds-space-9) var(--ds-space-10) var(--ds-space-10);
  text-align: center;
}

.component-card-body .btn {
  margin-top: auto;
}

@media (max-width: 767.98px) {
  .component-selection-grid {
    grid-template-columns: 1fr;
  }
}

/* Compact Level 1 / Level 2 picker used by CLF assessment wizards. */
.selection-cards.level-cards {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 220px));
  gap: var(--ds-space-6);
}

.level-cards .level-card {
  display: flex;
  align-items: center;
  gap: var(--ds-space-5);
  flex: none;
  min-width: 0;
  width: auto;
  min-height: 48px;
  padding: var(--ds-space-6) var(--ds-space-7);
  color: var(--ds-text);
  font: inherit;
  font-weight: 600;
  justify-content: center;
  text-align: center;
  text-decoration: none;
}

.module-home-level-cards {
  margin-bottom: var(--ds-space-12);
}

/* Shared responsive frame for every CLF residual-stress preview. Chart.js is
   configured to fill this frame, giving the engineering plots one consistent
   proportion without hard-coded canvas dimensions in individual calculators. */
.clf-residual-stress-chart-frame {
  position: relative;
  width: 100%;
  max-width: 900px;
  aspect-ratio: 2 / 1;
  margin-inline: auto;
}

.clf-residual-stress-chart-frame > canvas {
  position: absolute;
  inset: 0;
  width: 100% !important;
  height: 100% !important;
}

@media (max-width: 767.98px) {
  .clf-residual-stress-chart-frame {
    aspect-ratio: 1.45 / 1;
  }
}

/* ── DASHBOARD (A7 part 2) ──
   accounts/dashboard.html. D1 asked for a working surface rather than a
   profile page, and E6 built it: start an assessment, pick one up, then
   account. The three-equal-card layout and the classes that served it
   (.dashboard-assessment, .dashboard-note, .dashboard-empty) are gone with
   it. .dashboard-facts survives because the account panel still lists
   facts. */

.dashboard-facts {
  width: 100%;
  font-size: var(--ds-type-sm);
  border-collapse: collapse;
}

.dashboard-facts td:first-child {
  color: var(--ds-text-muted);
  padding: var(--ds-space-3) var(--ds-space-4) var(--ds-space-3) 0;
  width: 35%;
}

/* A band of the workspace. The page is a sequence of these, not a grid of
   cards, so that the order on screen is the order of the work. */
.workspace-section {
  margin-bottom: var(--ds-space-12);
}

.workspace-section:last-child {
  margin-bottom: 0;
}

.workspace-heading {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--ds-space-6);
  margin-bottom: var(--ds-space-6);
  padding-bottom: var(--ds-space-4);
  border-bottom: 1px solid var(--ds-border);
}

/* .section-label is the shared small-caps label; the heading here is an <h2>
   carrying it, so the element is semantic and the look is borrowed. */
.workspace-heading .section-label {
  margin: 0;
}

.workspace-heading__action {
  flex-shrink: 0;
  font-size: var(--ds-type-xs);
  font-weight: 600;
  color: var(--ds-link);
  text-decoration: none;
}

.workspace-heading__action:hover {
  text-decoration: underline;
}

.workspace-heading__search {
  width: 14rem;
  align-self: center;
}

/* One tile per damage type. This is the control D1 said the dashboard was
   missing: before this, an engineer who logged in had to find the sidebar
   before they could start anything. */
.start-card {
  border-radius: var(--ds-radius-md);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ds-space-4);
  height: 100%;
  min-height: 44px;
  padding: var(--ds-space-6) var(--ds-space-7);
  background: var(--ds-surface);
  border: 1px solid var(--ds-border);
  text-decoration: none;
  transition: border-color 0.18s ease, background 0.18s ease;
}

.start-card:hover {
  border-color: var(--ds-secondary);
  background: var(--ds-surface);
  text-decoration: none;
}

.start-card:focus-visible {
  outline: 2px solid var(--ds-secondary);
  outline-offset: 2px;
}

.start-card__name {
  font-size: var(--ds-type-sm);
  font-weight: 600;
  line-height: 1.35;
  color: var(--ds-heading);
}

.start-card__title {
  display: block;
  font-size: var(--ds-type-xs);
  color: var(--ds-text-muted);
}

.start-card__go {
  flex-shrink: 0;
  font-size: var(--ds-type-2xs);
  color: var(--ds-border-strong);
  transition: transform 0.18s ease, color 0.18s ease;
}

.start-card:hover .start-card__go {
  transform: translateX(3px);
  color: var(--ds-link);
}

/* A saved assessment, with enough on the row to recognise it: module,
   geometry, level, date and outcome. D1 listed all five; the old row showed
   a title, a level and a date, so two assessments of different components
   read identically. */
.recent-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ds-space-6);
  padding: var(--ds-space-6) var(--ds-space-9);
  border-bottom: 1px solid var(--ds-border);
}

.recent-row:last-child {
  border-bottom: 0;
}

.recent-row__main {
  min-width: 0;
}

.recent-row__title {
  display: block;
  font-size: var(--ds-type-sm);
  font-weight: 600;
  color: var(--ds-text);
  text-decoration: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.recent-row__title:hover {
  text-decoration: underline;
}

.recent-row__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ds-space-2) var(--ds-space-6);
  margin-top: var(--ds-space-2);
  font-size: var(--ds-type-xs);
  color: var(--ds-text-muted);
}

.recent-row__end {
  display: flex;
  align-items: center;
  gap: var(--ds-space-4);
  flex-shrink: 0;
}

/* An empty state names what is missing and what to do about it. */
.workspace-empty {
  padding: var(--ds-space-11) var(--ds-space-9);
}

.workspace-empty__lead {
  font-size: var(--ds-type-md);
  font-weight: 600;
  color: var(--ds-heading);
  margin-bottom: var(--ds-space-2);
}

.workspace-empty__body {
  font-size: var(--ds-type-sm);
  color: var(--ds-text-muted);
  max-width: 52ch;
  margin-bottom: 0;
}

@media (max-width: 575.98px) {
  .recent-row {
    align-items: flex-start;
    flex-direction: column;
    gap: var(--ds-space-4);
  }
}

/* ── PRICING (D2, second half) ──
   payments/pricing.html renders inside the app shell, so unlike the landing
   page it follows the --ds-* scales rather than landing.css. One plan is
   offered, so this is a statement beside a plan, not a tower comparison. */
.pricing-lead {
  font-size: var(--ds-type-md);
  line-height: 1.7;
  color: var(--ds-text-secondary);
  max-width: 54ch;
  margin-bottom: var(--ds-space-10);
}

.pricing-fact {
  display: grid;
  grid-template-columns: 9rem 1fr;
  gap: var(--ds-space-6);
  padding: var(--ds-space-6) 0;
}

.pricing-fact + .pricing-fact {
  border-top: 1px solid var(--ds-border);
}

.pricing-fact dt {
  font-size: var(--ds-type-sm);
  font-weight: 700;
  color: var(--ds-heading);
}

.pricing-fact dd {
  margin: 0;
  font-size: var(--ds-type-sm);
  line-height: 1.65;
  color: var(--ds-text-muted);
}

.plan-card {
  border-radius: var(--ds-radius-lg);
  background: var(--ds-surface);
  border: 1px solid var(--ds-border);
  border-top: 3px solid var(--ds-accent);
  padding: var(--ds-space-11);
}

.plan-card__name {
  font-size: var(--ds-type-lg);
  font-weight: 700;
  color: var(--ds-heading);
  margin-bottom: var(--ds-space-6);
}

.plan-card__price {
  display: flex;
  align-items: baseline;
  gap: var(--ds-space-4);
  margin-bottom: var(--ds-space-2);
}

/* Tabular figures so a price never shifts width between renders. */
.plan-card__amount {
  font-size: var(--ds-type-2xl);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--ds-heading);
  line-height: 1.1;
}

.plan-card__unit,
.plan-card__note {
  font-size: var(--ds-type-xs);
  color: var(--ds-text-muted);
}

.plan-card__note {
  margin-bottom: 0;
}

/* Hairlines between features, none above the first or below the last, so the
   group reads as a group rather than as a table. No checkmark column: a tick
   beside every line says nothing that the line being present does not. */
.plan-features {
  list-style: none;
  margin: var(--ds-space-10) 0;
  padding: 0;
}

.plan-features li {
  font-size: var(--ds-type-sm);
  line-height: 1.6;
  color: var(--ds-text-secondary);
  padding: var(--ds-space-6) 0;
}

.plan-features li + li {
  border-top: 1px solid var(--ds-border);
}

.plan-card__active {
  font-size: var(--ds-type-xs);
  color: var(--ds-text-muted);
  margin: var(--ds-space-6) 0 0;
}

@media (max-width: 575.98px) {
  .pricing-fact {
    grid-template-columns: 1fr;
    gap: var(--ds-space-2);
  }

  .plan-card {
    padding: var(--ds-space-9);
  }
}

/* The busy state from static/js/pending_action.js. The spin is damped to a
   near-still 0.01ms by the global prefers-reduced-motion rule, which is why
   the label carries the meaning and the spinner only decorates it. */
.pending-spinner {
  display: inline-block;
  width: var(--ds-space-7);
  height: var(--ds-space-7);
  margin-right: var(--ds-space-4);
  vertical-align: -2px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: pending-spin 0.7s linear infinite;
}

@keyframes pending-spin {
  to { transform: rotate(360deg); }
}

/* A button that holds an icon and nothing else. */
.btn.btn-icon {
  padding: var(--ds-space-1) var(--ds-space-3);
}

/* The dashboard's pin: a control, lit while the row is pinned to the top. */
.btn-pin { color: var(--ds-text-secondary); }
.btn-pin[aria-pressed="true"] { color: var(--ds-accent); }

.u-rule { margin-block: var(--ds-space-6); }

/* Small shell details that were written inline in base.html. */
.unit-toggle-btn.active { cursor: default; }
.u-contents { display: contents; }
.u-flush-block { margin: 0; }
.sidebar-section--tight { padding-top: var(--ds-space-4); }
.sidebar-section-label--tight { padding-top: var(--ds-space-2); }

.dev-link-list {
  font-size: var(--ds-type-base);
  line-height: 2.2;
}

/* ── CLF SENSITIVITY POINT PALETTE (A7 part 2) ──
   The sensitivity tables paint one marker per iteration, and the colour is
   chosen per row in Python. That was style="background-color: {{ ... }}",
   which style-src-attr no longer allows. The palette is a closed set, so the
   choice is expressible as a class; the colours themselves are still the ones
   in assessment_clf/core/sensitivity_display.py, and tests/test_security.py
   fails if the two lists stop matching. */

.clf-point-color-1 { --clf-sensitivity-point-color: var(--clf-sensitivity-color-1, #0072B2); }
.clf-point-color-2 { --clf-sensitivity-point-color: var(--clf-sensitivity-color-2, #E69F00); }
.clf-point-color-3 { --clf-sensitivity-point-color: var(--clf-sensitivity-color-3, #009E73); }
.clf-point-color-4 { --clf-sensitivity-point-color: var(--clf-sensitivity-color-4, #CC79A7); }
.clf-point-color-5 { --clf-sensitivity-point-color: var(--clf-sensitivity-color-5, #D55E00); }
.clf-point-color-6 { --clf-sensitivity-point-color: var(--clf-sensitivity-color-6, #6F42C1); }
.clf-point-color-7 { --clf-sensitivity-point-color: var(--clf-sensitivity-color-7, #56B4E9); }
.clf-point-color-8 { --clf-sensitivity-point-color: var(--clf-sensitivity-color-8, #8C564B); }
.clf-point-color-9 { --clf-sensitivity-point-color: var(--clf-sensitivity-color-9, #17A2B8); }
.clf-point-color-10 { --clf-sensitivity-point-color: var(--clf-sensitivity-color-10, #7A8B00); }
.clf-point-color-baseline { --clf-sensitivity-point-color: var(--clf-sensitivity-color-baseline, #343A40); }

/* The browser chart reads these same tokens, keeping its points and table
   markers in agreement. Stored results and printed reports retain the light
   palette; the screen palette follows the active theme. */
:root[data-theme="dark"] {
  --clf-sensitivity-color-1: #73C8FF;
  --clf-sensitivity-color-2: #F3BD55;
  --clf-sensitivity-color-3: #65D6AC;
  --clf-sensitivity-color-4: #E9A5D1;
  --clf-sensitivity-color-5: #F7A56B;
  --clf-sensitivity-color-6: #C4A7FF;
  --clf-sensitivity-color-7: #A7DBFA;
  --clf-sensitivity-color-8: #DCA995;
  --clf-sensitivity-color-9: #68D7E4;
  --clf-sensitivity-color-10: #C9D979;
  --clf-sensitivity-color-baseline: #E2E8F0;
}

/* The marker shapes read the property, so a shape and a colour compose
   instead of every combination needing its own rule. */
.clf-sensitivity-point-marker--circle,
.clf-sensitivity-point-marker--diamond {
  background-color: var(--clf-sensitivity-point-color);
}


.clf-sensitivity-chart { max-height: 320px; }

.clf-sensitivity-max-flaw-row { border-top: 2px solid var(--ds-info); }

/* The stress-distribution preview a wizard draws while the engineer is still
   entering the profile. Same height cap as the sensitivity chart, different
   thing, so it gets its own name rather than borrowing that one. */
.clf-preview-chart { max-height: 320px; }

/* An acceptance-criteria table: the check, the numbers it compared, and
   whether it passed. Eleven result templates render one, and each of them
   carried the failing-value styling as a Django {% if %} inside a style
   attribute, which is the one shape a static stylesheet cannot take over.
   It is a conditional class now. */
.criteria-value {
  font-family: var(--ds-font-mono);
  font-size: var(--ds-type-sm);
}

.criteria-value--failing {
  color: var(--ds-fail);
  font-weight: 600;
}

.criteria-row--failing {
  background: var(--ds-fail-bg);
}

/* The palette on /dev/components/. Each swatch now paints itself from the
   token it is labelled with, rather than from a hex copied next to it, so a
   changed token shows up on the page that exists to show the tokens. The hex
   in the caption is still text and still hand-written. */
/* /dev/components/: one swatch per token the gallery names, reading the token
   itself so the swatch is the current theme's value. A brand token, then the
   role tokens in the order tokens.md lists them. */
.ds-swatch--primary         { background: var(--ds-primary); }
.ds-swatch--secondary       { background: var(--ds-secondary); }
.ds-swatch--accent          { background: var(--ds-accent); }
.ds-swatch--chrome-bg       { background: var(--ds-chrome-bg); }
.ds-swatch--solid-bg        { background: var(--ds-solid-bg); }
.ds-swatch--bg              { background: var(--ds-bg); }
.ds-swatch--surface         { background: var(--ds-surface); }
.ds-swatch--surface-sunken  { background: var(--ds-surface-sunken); }
.ds-swatch--border          { background: var(--ds-border); }
.ds-swatch--border-subtle   { background: var(--ds-border-subtle); }
.ds-swatch--border-strong   { background: var(--ds-border-strong); }
.ds-swatch--text            { background: var(--ds-text); }
.ds-swatch--text-secondary  { background: var(--ds-text-secondary); }
.ds-swatch--text-muted      { background: var(--ds-text-muted); }
.ds-swatch--heading         { background: var(--ds-heading); }
.ds-swatch--link            { background: var(--ds-link); }
.ds-swatch--pass            { background: var(--ds-pass); }
.ds-swatch--fail            { background: var(--ds-fail); }
.ds-swatch--review          { background: var(--ds-review); }
.ds-swatch--info            { background: var(--ds-info); }
.ds-swatch--pass-bg         { background: var(--ds-pass-bg); }
.ds-swatch--fail-bg         { background: var(--ds-fail-bg); }
.ds-swatch--review-bg       { background: var(--ds-review-bg); }
.ds-swatch--info-bg         { background: var(--ds-info-bg); }
.ds-swatch--on-status       { background: var(--ds-on-status); }

.ds-swatch--radius-xs   { border-radius: var(--ds-radius-xs); }
.ds-swatch--radius-sm   { border-radius: var(--ds-radius-sm); }
.ds-swatch--radius-md   { border-radius: var(--ds-radius-md); }
.ds-swatch--radius-lg   { border-radius: var(--ds-radius-lg); }
.ds-swatch--radius-xl   { border-radius: var(--ds-radius-xl); }
.ds-swatch--radius-pill { border-radius: var(--ds-radius-pill); }

.u-text-secondary { color: var(--ds-text-secondary); }

/* ── THE LAST OF THE INLINE DECLARATIONS (A7 part 2) ── */

.u-inline      { display: inline; }
.u-pre-line    { white-space: pre-line; }
.u-no-caps     { text-transform: none; }
.u-clickable   { cursor: pointer; }
.u-scroll-box  { max-height: 500px; overflow-y: auto; }
.u-rule--wide  { margin-block: var(--ds-space-9); }
.u-col-20      { width: 20%; }
.u-col-25      { width: 25%; }
.u-col-30      { width: 30%; }
.u-col-50      { width: 50%; }

.u-preview-scroll--sm { max-height: 180px; }

/* A bordered box inside a panel: a picker's chosen row, a figure's frame, a
   choice card. Four templates spelled this as `border rounded bg-white` or
   `bg-light`, which is white or light grey on a dark page. */
.u-inset-box {
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-md);
  background: var(--ds-surface-sunken);
}
.u-preview-scroll--md { max-height: 240px; }

/* A short explanatory block set slightly tighter than body copy. */
.note-block {
  font-size: var(--ds-type-sm);
  line-height: 1.45;
}

/* A caption under a figure or a preview image. */
.figure-caption-sm {
  font-size: var(--ds-type-sm);
  margin-top: var(--ds-space-3);
}

/* An indented sub-parameter row in the CLF FAD parameter table. */
.fad-subparameter { padding-left: var(--ds-space-11); }

.fad-plot-marker {
  font-size: var(--ds-type-lg);
  color: var(--ds-text);
}

/* A fixed-height frame a chart is drawn into. */
.chart-frame-360 {
  position: relative;
  height: 360px;
}

/* The narrow single-purpose pages: change password, and its confirmation. */
.form-page-narrow {
  max-width: 440px;
  margin: var(--ds-space-11) auto 0;
}

.form-page-icon {
  color: var(--ds-pass);
  font-size: var(--ds-type-2xl);
  margin-bottom: var(--ds-space-6);
}

/* A footer that sits under a card body without a rule of its own above the
   page background. */
.ds-card-footer--plain {
  border-bottom: none;
  background: var(--ds-surface);
}

/* A destructive action that should not shout: red text, ordinary border. */
.btn-quiet-danger {
  color: var(--ds-fail);
  border-color: var(--ds-border);
}

/* Secondary figures beside a verdict: present, not competing with it. */
.result-subtle {
  font-size: var(--ds-type-xs);
  font-weight: 400;
  opacity: 0.82;
}

/* Part 6: a quiet, unscaled metal-surface illustration beside its grade input. */
.pit-density-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
  align-items: start;
  gap: var(--ds-space-11);
}

.pit-density {
  width: 100%;
  max-width: 560px;
  margin-bottom: 0;
  overflow: hidden;
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-xs);
  background: var(--ds-surface);
}

.pit-density__header {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--ds-space-4) var(--ds-space-8);
  padding: var(--ds-space-8) var(--ds-space-9) 0;
  color: var(--ds-heading);
  font-size: var(--ds-type-base);
  font-weight: 600;
}

.pit-density__grade {
  color: var(--ds-text-muted);
  font-family: var(--ds-font-mono);
  font-size: var(--ds-type-sm);
  font-weight: 400;
  white-space: nowrap;
}

.pit-density__image {
  display: block;
  width: 100%;
  height: auto;
}

.pit-density__empty {
  display: grid;
  place-items: center;
  min-height: 180px;
  padding: var(--ds-space-10);
  color: var(--ds-text-muted);
  text-align: center;
}

.pit-density .u-hidden { display: none; }

.pit-density__caption {
  border-radius: var(--ds-radius-sm);
  margin: 0;
  padding: var(--ds-space-8) var(--ds-space-9);
  border-top: 1px solid var(--ds-border);
  color: var(--ds-text-muted);
  font-size: var(--ds-type-sm);
  line-height: 1.6;
}

@media (max-width: 991.98px) {
  .pit-density-layout { grid-template-columns: minmax(0, 1fr); }
}

/* The bar under the last step of the GML wizard. */
.wizard-summary-bar {
  border-radius: var(--ds-radius-md);
  border-top: 1px solid var(--ds-border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--ds-space-7) var(--ds-space-11);
}

.u-space-between  { justify-content: space-between; }
.u-stack-stretch  { flex-direction: column; align-items: stretch; }
.u-gap-6          { gap: var(--ds-space-6); }
.u-hero-icon      { font-size: var(--ds-icon-hero); }

.u-w-220 { width: 220px; }
.u-max-w-400  { max-width: 400px; }
.u-max-w-420  { max-width: 420px; }
.u-max-w-520  { max-width: 520px; }
.u-max-w-820  { max-width: 820px; }
.u-max-w-880  { max-width: 880px; }

.u-min-w-9rem  { min-width: 9rem; }
.u-min-w-22rem { min-width: 22rem; }

/* The failure reasons listed under an outcome banner. */
.outcome-reason {
  font-size: var(--ds-type-sm);
  font-weight: 400;
  padding-left: var(--ds-space-9);
}

/* The circled step number in the component-type assumption panel. */
.assumption-step-badge {
  width: 1.75rem;
  height: 1.75rem;
  font-weight: 700;
  line-height: 1;
}

/* /dev/ user list: which row is the session's own user. */
.dev-current-user { background: rgba(46,109,164,0.08); }

/* ── ERROR PAGES (404 / 500) ────────────────────────────────────────────────
   500.html extends nothing and renders without a request, so it cannot rely
   on the app shell. Everything it needs is here, including the standalone
   page frame, because a CSP without 'unsafe-inline' leaves no other route. */
.error-page { max-width: 40rem; }

.error-page__code {
  font-size: var(--ds-type-2xl);
  font-weight: 700;
  color: var(--ds-text-muted);
  margin-bottom: var(--ds-space-2);
  line-height: 1;
}

.error-page__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ds-space-6);
  margin-top: var(--ds-space-10);
}

/* The 500 page has no sidebar or topbar to sit inside, so it centres itself. */
.error-page-standalone {
  background: var(--ds-bg);
  color: var(--ds-text);
}

.error-page--standalone {
  margin: 0 auto;
  padding: var(--ds-space-13) var(--ds-space-10);
}

/* ===========================================================================
   RESULT SURFACE
   ---------------------------------------------------------------------------
   The components a result page is built from. One vocabulary for all eight
   assessment modules, so that Part 4 and Part 9 differ in what they calculate
   and in nothing else.

   The shape a result page takes:

     .result-head        eyebrow, title, route, and the two page actions
     .verdict            the decision, its one-line reading, and the numbers
                         that carry it
     .result-grid        the evidence: a wide column for the figure that
                         explains the verdict, a narrow one for the panels
                         that support it
     .ds-panel           every box in either column
     .numbers            label / value / unit rows inside a panel
     .status-line        a one-line check with an icon and a disclosure
     .admin-panel        the calculation trace, for staff

   A module adds nothing to this list. If a result needs something that is not
   here, it belongs here, named for what it is rather than for the module that
   asked first.
   =========================================================================== */

/* ── HEAD ── */
.result-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--ds-space-8);
  flex-wrap: wrap;
  margin-bottom: var(--ds-space-10);
}

.result-head__eyebrow {
  display: block;
  font-size: var(--ds-type-2xs);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ds-text-muted);
  margin-bottom: var(--ds-space-4);
}

.result-head__title {
  font-size: var(--ds-type-3xl);
  font-weight: 700;
  line-height: 1.15;
  color: var(--ds-heading);
  margin: 0;
}

/* The standard reference under the title. It is provenance, not a sentence:
   the route code is set in the mono face so it reads as an identifier, and
   the rest stays quiet. */
.result-head__route {
  font-size: var(--ds-type-sm);
  color: var(--ds-text-muted);
  margin: var(--ds-space-4) 0 0;
}

.result-head__route code,
.result-head__route .result-head__code {
  font-family: var(--ds-font-mono);
  font-size: var(--ds-type-sm);
  color: var(--ds-text-secondary);
  background: none;
  padding: 0;
}

.result-head__actions {
  display: flex;
  align-items: center;
  gap: var(--ds-space-5);
  flex-wrap: wrap;
}

/* ── VERDICT ──
   The whole decision in one surface: the word, one line of reading, and the
   two or three numbers that produced it. Nothing else goes in here. The rail
   down the left is the only place a result page states its outcome in colour,
   so the colour is not also repeated as a fill behind the text. */
.verdict {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--ds-space-11);
  flex-wrap: wrap;
  background: var(--ds-surface);
  border: 1px solid var(--ds-border-subtle);
  border-left: 4px solid var(--ds-border-strong);
  border-radius: var(--ds-radius-xl);
  box-shadow: var(--ds-shadow-1);
  padding: var(--ds-space-10) var(--ds-space-11);
  margin-bottom: var(--ds-space-10);
}

.verdict--pass   { border-left-color: var(--ds-pass); }
.verdict--fail   { border-left-color: var(--ds-fail); }
.verdict--review { border-left-color: var(--ds-review); }

.verdict__main {
  flex: 1 1 22rem;
  min-width: 0;
}

.verdict__status {
  display: flex;
  align-items: center;
  gap: var(--ds-space-5);
  font-size: var(--ds-type-lg);
  font-weight: 700;
  margin-bottom: var(--ds-space-5);
}

.verdict--pass   .verdict__status { color: var(--ds-pass); }
.verdict--fail   .verdict__status { color: var(--ds-fail); }
.verdict--review .verdict__status { color: var(--ds-review); }

/* One sentence, and it says what the numbers mean, never what they are. */
.verdict__lede {
  font-size: var(--ds-type-base);
  color: var(--ds-text-secondary);
  margin: 0;
  max-width: 62ch;
}

.verdict__ref {
  display: block;
  font-size: var(--ds-type-xs);
  color: var(--ds-text-muted);
  margin-top: var(--ds-space-6);
}

.verdict__reasons {
  margin: var(--ds-space-6) 0 0;
  padding-left: var(--ds-space-9);
  font-size: var(--ds-type-sm);
  color: var(--ds-text-secondary);
}

.verdict__reasons li { margin-bottom: var(--ds-space-2); }
.verdict__reasons li:last-child { margin-bottom: 0; }

/* ── HEADLINE METRICS ──
   Two or three, never more. They are the numbers a reader would quote if
   asked why the verdict came out the way it did; everything else belongs in
   .numbers further down the page. */
.verdict__metrics {
  display: flex;
  gap: var(--ds-space-11);
  flex-wrap: wrap;
}

.metric__label {
  display: block;
  font-size: var(--ds-type-2xs);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ds-text-muted);
  margin-bottom: var(--ds-space-3);
  white-space: nowrap;
}

.metric__value {
  display: block;
  font-family: var(--ds-font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--ds-type-2xl);
  font-weight: 700;
  line-height: 1.1;
  color: var(--ds-heading);
}

.metric__note {
  display: block;
  font-size: var(--ds-type-2xs);
  color: var(--ds-text-muted);
  margin-top: var(--ds-space-3);
  white-space: nowrap;
}

/* The one metric that is the margin itself, coloured like the verdict. */
.metric--pass   .metric__value { color: var(--ds-pass); }
.metric--fail   .metric__value { color: var(--ds-fail); }
.metric--review .metric__value { color: var(--ds-review); }

/* ── EVIDENCE GRID ──
   The figure earns the wide column because it is what explains the verdict.
   A result with no figure drops .result-grid entirely and stacks its panels
   full width rather than leaving a hole where the chart would have been. */
.result-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
  gap: var(--ds-space-10);
  align-items: start;
}

.result-grid > * { min-width: 0; }

.result-grid__aside {
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-10);
}

@media (max-width: 991.98px) {
  .result-grid { grid-template-columns: minmax(0, 1fr); }
}

/* ── PANEL ──
   The new card. .ds-card keeps its filled header for the dense tabular
   sections it was built for; a panel's head is just a title and, on the
   right, the standard reference or a disclosure. */
.ds-panel {
  background: var(--ds-surface);
  border: 1px solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-lg);
  box-shadow: var(--ds-shadow-1);
  overflow: hidden;
}

.ds-panel__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--ds-space-8);
  flex-wrap: wrap;
  padding: var(--ds-space-9) var(--ds-space-9) 0;
}

.ds-panel__title {
  font-size: var(--ds-type-md);
  font-weight: 700;
  color: var(--ds-heading);
  margin: 0;
}

.ds-panel__meta {
  font-size: var(--ds-type-xs);
  color: var(--ds-link);
}

.ds-panel__lede {
  font-size: var(--ds-type-xs);
  color: var(--ds-text-muted);
  margin: var(--ds-space-3) 0 0;
  padding: 0 var(--ds-space-9);
}

.ds-panel__body {
  padding: var(--ds-space-9);
  overflow-x: auto;
}

.ds-panel__foot {
  padding: var(--ds-space-6) var(--ds-space-9);
  border-top: 1px solid var(--ds-border-subtle);
  background: var(--ds-surface-sunken);
}

/* A panel whose whole content is one row: an icon, a line of text, and a
   disclosure on the right. Used for the validity summary and for anything
   else that is a single check rather than a table of them. */
.status-line {
  display: flex;
  align-items: flex-start;
  gap: var(--ds-space-6);
  padding: var(--ds-space-9);
}

.status-line__icon {
  font-size: var(--ds-type-md);
  line-height: 1.35;
  flex-shrink: 0;
}

.status-line--pass   .status-line__icon { color: var(--ds-pass); }
.status-line--fail   .status-line__icon { color: var(--ds-fail); }
.status-line--review .status-line__icon { color: var(--ds-review); }
/* Not an outcome: a statement of fact about the route that was taken. Part 4's
   storage-tank result says which Table 4.7 row it used, and that was an
   alert-warning, which coloured a fact as a caution. */
.status-line--info   .status-line__icon { color: var(--ds-info); }

.status-line__main { flex: 1; min-width: 0; }

.status-line__title {
  font-size: var(--ds-type-base);
  font-weight: 700;
  color: var(--ds-heading);
  margin: 0;
}

.status-line__note {
  display: block;
  font-size: var(--ds-type-xs);
  color: var(--ds-text-muted);
  margin: var(--ds-space-2) 0 0;
}

.status-line__title { display: block; }

/* The status line is usually the <summary> of the panel it heads, so the whole
   line is the control rather than a link sitting beside one. The default
   triangle goes; this is the affordance instead, and it changes wording with
   the state so it never says "Show" on an open panel. */
summary.status-line {
  list-style: none;
  cursor: pointer;
}

summary.status-line::-webkit-details-marker { display: none; }

summary.status-line:hover .status-line__action { color: var(--ds-link-hover); }

summary.status-line:focus-visible {
  outline: 2px solid var(--ds-link);
  outline-offset: -2px;
  border-radius: var(--ds-radius-lg);
}

/* The affordance. A chevron rather than a word that has to change with the
   state: swapping "Show" for "Hide" costs either a second element or a
   generated-content hack, and the arrow says the same thing in both. */
.status-line__action {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-3);
  flex-shrink: 0;
  align-self: center;
  font-size: var(--ds-type-xs);
  font-weight: 600;
  color: var(--ds-link);
}

.status-line__action::after {
  content: "";
  width: 0.45em;
  height: 0.45em;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translate(-0.1em, -0.1em);
  transition: transform 0.15s ease;
}

details[open] > summary.status-line .status-line__action::after {
  transform: rotate(225deg) translate(-0.1em, -0.1em);
}

/* ── NUMBERS ──
   Label on the left, value and unit on the right, one hairline between rows.
   This is where a result page puts the quantities that are worth naming and
   that are not headline metrics. It is a definition list because that is what
   it is; a table here would promise columns it does not have. */
.numbers {
  margin: 0;
  padding: 0 var(--ds-space-9);
}

.numbers__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--ds-space-8);
  padding: var(--ds-space-6) 0;
  border-bottom: 1px solid var(--ds-border-subtle);
}

.numbers__row:last-child { border-bottom: 0; }

.numbers__label {
  font-size: var(--ds-type-sm);
  color: var(--ds-text-secondary);
  margin: 0;
  min-width: 0;
}

/* A qualifier on the label, not a second value: which basis a figure was taken
   on, or the assumption it depends on. One short clause. */
.numbers__note {
  display: block;
  font-size: var(--ds-type-2xs);
  font-weight: 400;
  color: var(--ds-text-muted);
  margin-top: var(--ds-space-1);
}

.numbers__value {
  display: flex;
  align-items: baseline;
  gap: var(--ds-space-4);
  margin: 0;
  flex-shrink: 0;
  font-family: var(--ds-font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--ds-type-base);
  font-weight: 700;
  color: var(--ds-heading);
  white-space: nowrap;
}

/* A value that is words, such as the basis a factor was taken on: wraps on
   the right instead of running under its label. */
.numbers__value--prose {
  flex-shrink: 1;
  white-space: normal;
  text-align: right;
  font-family: inherit;
  font-size: var(--ds-type-sm);
  font-weight: 400;
}

.numbers__unit {
  font-family: var(--ds-font);
  font-size: var(--ds-type-xs);
  font-weight: 400;
  color: var(--ds-text-muted);
}

/* ── CHART WELL ──
   A canvas needs a box with a height, or Chart.js grows it without limit on
   the first resize. aspect-ratio rather than a fixed height so the figure
   keeps its proportions from a phone to a 27" display. */
.chart-well {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 3;
  min-height: 16rem;
}

/* Bound the frame itself so a legend follows the canvas without a blank gap. */
.chart-well--landscape {
  aspect-ratio: 8 / 5;
  max-height: 400px;
}

.chart-well > canvas {
  position: absolute;
  inset: 0;
  width: 100% !important;
  height: 100% !important;
}

/* ── CHART HOVER READOUT ──
   A crosshair with a dot on every curve at the pointer and a panel of their
   values. The creep charts draw it in SVG, the crack-growth charts on their
   canvas; both use these classes. */
.cr-chart-hover-layer {
  pointer-events: none;
}

.cr-chart-hover-crosshair {
  stroke: var(--ds-text-muted);
  stroke-dasharray: 2 3;
  stroke-opacity: .72;
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

.cr-chart-hover-intersections circle {
  vector-effect: non-scaling-stroke;
}

.cr-chart-hover-tooltip {
  background: var(--ds-chrome-bg);
  border: 1px solid var(--ds-border-strong);
  border-radius: var(--ds-radius-xs);
  box-shadow: var(--ds-shadow-2);
  color: var(--ds-chrome-fg);
  font-size: var(--ds-type-xs);
  line-height: 1.35;
  max-width: min(390px, calc(100% - 24px));
  min-width: 230px;
  padding: var(--ds-space-4) var(--ds-space-5);
  pointer-events: none;
  position: absolute;
  transform: translate(12px, -50%);
  z-index: 4;
}

.cr-chart-hover-tooltip[data-side="left"] {
  transform: translate(calc(-100% - 12px), -50%);
}

.cr-chart-hover-tooltip__heading {
  border-bottom: 1px solid var(--ds-border-strong);
  font-weight: 700;
  margin-bottom: var(--ds-space-3);
  padding-bottom: var(--ds-space-3);
}

.cr-chart-hover-tooltip__row {
  align-items: baseline;
  display: grid;
  gap: var(--ds-space-2) var(--ds-space-3);
  grid-template-columns: .65rem minmax(6rem, 1fr) auto;
}

.cr-chart-hover-tooltip__row + .cr-chart-hover-tooltip__row {
  margin-top: var(--ds-space-2);
}

.cr-chart-hover-tooltip__swatch {
  align-self: center;
  background: var(--cr-series-color);
  border-radius: 50%;
  display: inline-block;
  height: .5rem;
  width: .5rem;
}

.cr-chart-hover-tooltip__swatch--dash {
  border-radius: 0;
  height: var(--cr-series-width, 2.4px);
}

.cr-chart-hover-tooltip__series {
  color: var(--ds-text-on-dark);
}

.cr-chart-hover-tooltip__value {
  font-family: var(--ds-font-mono);
  font-weight: 700;
  text-align: right;
}

.chart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ds-space-5) var(--ds-space-9);
  padding-top: var(--ds-space-7);
  font-size: var(--ds-type-xs);
  color: var(--ds-text-muted);
}

/* The key of a chart drawn in the figure face. */
.chart-legend--figure {
  font-family: var(--ds-font-figure);
}

.chart-legend__key {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-4);
}

/* ── DISCLOSURE ──
   Everything a reader does not need in order to accept the verdict, but may
   want: the per-parameter validity table, the input echo, the working. A
   <details> element, so it works with no script and prints open when the
   browser is asked to. */
.disclosure { border: 0; }

.disclosure > summary {
  list-style: none;
  cursor: pointer;
  font-size: var(--ds-type-xs);
  font-weight: 600;
  color: var(--ds-link);
}

.disclosure > summary::-webkit-details-marker { display: none; }

.disclosure > summary:hover { color: var(--ds-link-hover); }

.disclosure > summary:focus-visible {
  outline: 2px solid var(--ds-link);
  outline-offset: 2px;
  border-radius: var(--ds-radius-xs);
}

.disclosure[open] > summary { margin-bottom: var(--ds-space-7); }

/* ── ADMIN PANEL ──
   The calculation trace and anything else that exists for whoever maintains
   the calculation rather than for whoever is reading the result. It sits last
   on the page, it is visibly marked as staff-only, and it is collapsed. The
   reader's version of "show me the working" is the PDF. */
.admin-panel {
  border: 1px dashed var(--ds-border-strong);
  border-radius: var(--ds-radius-lg);
  background: var(--ds-surface-sunken);
  padding: var(--ds-space-9);
  margin-top: var(--ds-space-12);
}

.admin-panel__tag {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-3);
  font-size: var(--ds-type-2xs);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ds-text-muted);
  margin-bottom: var(--ds-space-6);
}

/* A page that stacks its panels straight under the result head, rather than
   in a .result-grid, gets the same rhythm between them as the grid's gap and
   the verdict's margin. Without it Part 7 stacked its tables, disclosures and
   notes edge to edge. */
.result-head ~ .ds-panel,
.result-head ~ .result-grid {
  margin-bottom: var(--ds-space-10);
}

/* ── SAVE AND EXPORT ──
   The two panels under a result, side by side and the same height, with their
   buttons on one line in each panel's foot. They stack on a narrow screen, and
   a lone panel (an assessment that could not be saved) takes the full width.
   The page actions are the result head's, not a row here. */
.result-actions {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
  gap: var(--ds-space-9);
  align-items: stretch;
  margin: var(--ds-space-11) 0;
}

.result-actions__panel,
.result-actions__form {
  display: flex;
  flex-direction: column;
}

.result-actions__form {
  flex: 1 1 auto;
}

.result-actions__panel .ds-panel__body {
  flex: 1 1 auto;
}

.result-actions__foot {
  display: flex;
  align-items: center;
  gap: var(--ds-space-6);
  flex-wrap: wrap;
}

@media (max-width: 575.98px) {
  .result-head { flex-direction: column; }
  .result-head__title { font-size: var(--ds-type-2xl); }
  .verdict { padding: var(--ds-space-9); }
  .verdict__metrics { gap: var(--ds-space-9); }
}
