/* ============================================
   calcsheet - design tokens and base type

   The system locked in docs/brainstorms/ui-direction-brainstorm.md: a
   technical publication that computes, not a SaaS dashboard. Paper surface,
   near-black ink, STIX Two Text for prose and JetBrains Mono for every value
   that can change.

   Two rules the rest of the stylesheet may not break:

   - Blueprint blue means interactive. Nothing decorative is blue.
   - Green, amber and red mean pass, caution and fail, and nothing else, and
     they never carry the meaning alone: a verdict is always also a word or a
     glyph, because roughly one engineer in twelve cannot read the colour.

   Token names are semantic (--surface, --ink) rather than literal (--white,
   --black) so a dark mode is a value swap in one block. Light only ships now.
   ============================================ */

/* ============================================
   Fonts - self-hosted, latin-subset variable woff2

   STIX Two Text (body serif, scientific-publishing pedigree) and JetBrains
   Mono (every live value, table and check). Both are the official OFL
   releases as distributed by Google Fonts, latin subset only: 128KB total
   against a ~450KB budget. `unicode-range` matches the subset the files
   actually contain, so a character outside it falls back rather than
   rendering blank. STIX Two Math, a subset `.claude/scripts/mathfont.py`
   builds from every character the calcs' MathML uses, draws the maths
   (#290).
   ============================================ */
@font-face {
    font-family: 'STIX Two Text';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('fonts/STIXTwoText-Roman-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: 'STIX Two Text';
    font-style: italic;
    font-weight: 400 700;
    font-display: swap;
    src: url('fonts/STIXTwoText-Italic-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 400 800;
    font-display: swap;
    src: url('fonts/JetBrainsMono-Roman-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: 'JetBrains Mono';
    font-style: italic;
    font-weight: 400 800;
    font-display: swap;
    src: url('fonts/JetBrainsMono-Italic-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: 'STIX Two Math';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('fonts/STIXTwoMath-Regular.woff2') format('woff2');
}

/* ============================================
   Design tokens
   ============================================ */
:root {
    /* Surfaces - warm paper, the Tufte value */
    --surface: #fffff8;
    --surface-sunken: #f7f7ef;
    --surface-raised: #fffffd;

    /* Ink */
    --ink: #16160f;
    --ink-muted: #55554c;
    --ink-faint: #86867c;

    /* Rules - hairlines, not borders */
    --rule: #dedad0;
    --rule-strong: #b8b4a8;

    /* Blueprint blue: interactive elements only */
    --accent: #1a4f8b;
    --accent-strong: #113a69;
    --accent-wash: #e8effa;

    /* Verdicts: pass / caution / fail only, always paired with a word or glyph */
    --pass: #2c6b3f;
    --pass-wash: #e7f2e9;
    --caution: #8a5600;
    --caution-wash: #faf0dd;
    --fail: #a32118;
    --fail-wash: #fbe9e7;

    /* Reinforcement: plum, clear of blue (interactive) and the three verdicts; the only decorative colour a figure may use */
    --rebar: #7b3f7b;

    /* Value-change cue (U2.4). Neutral on purpose: a changed number is not a verdict. */
    --flash-wash: #f2eeda;

    /* Type families */
    --font-body: 'STIX Two Text', Georgia, 'Times New Roman', serif;
    --font-mono: 'JetBrains Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace;

    /* Type scale, 1.2 ratio off a 17px reading size */
    --text-xs: 0.8125rem;
    --text-sm: 0.875rem;
    --text-base: 1.0625rem;
    --text-lg: 1.25rem;
    --text-xl: 1.5rem;
    --text-2xl: 1.8rem;
    --text-3xl: 2.2rem;

    --leading-tight: 1.25;
    --leading-body: 1.6;

    /* The page width: one column on every page (#105), except that a calc page
       widens to --workspace-width at the workspace breakpoint. A px value, not ch,
       so it does not resolve against whichever face an element is set in.
       Wide enough for a calc title on one line beside its two buttons. */
    --page-width: 1100px;

    /* Spacing */
    --space-1: 0.25rem;
    --space-2: 0.5rem;
    --space-3: 0.75rem;
    --space-4: 1rem;
    --space-6: 1.5rem;
    --space-8: 2rem;
    --space-12: 3rem;

    /* The pinned verdict strip's height: what an anchor jump stops short by,
       so the strip does not cover the step it lands on. This is the desktop
       strip with its stamp, which a page opened at `#S-<name>` scrolls by before
       any script runs; calcsheet.js then sets the measured height. */
    --strip-height: 4.25rem;

    /* Radii - a technical document is not rounded */
    --radius-sm: 2px;
    --radius-md: 4px;

    /* Motion: value-change cues and focus, and the index hero's loop (#306), which shows a value changing and stops under reduced motion. No entrance animation exists. */
    --flash-duration: 600ms;
    --transition-fast: 120ms ease;
}

/* ============================================
   Base

   Enough to prove the tokens on a real page. Layout is U1.1's job.
   ============================================ */
*, *::before, *::after {
    box-sizing: border-box;
}

body {
    margin: 0;
    background: var(--surface);
    color: var(--ink);
    font-family: var(--font-body);
    font-size: var(--text-base);
    line-height: var(--leading-body);
    -webkit-font-smoothing: antialiased;
}

h1, h2, h3 {
    font-family: var(--font-body);
    font-weight: 600;
    line-height: var(--leading-tight);
}

/* Every number that can change is mono and tabular, so a recompute cannot
   shift the layout under the reader.

   The size step is metric, not taste. Measured on this page at a 100px em,
   x-heights are STIX Two 48, JetBrains Mono 55. Setting mono
   to 48/55 puts a mono value on the same x-height as the prose around it. */
code, kbd, samp, pre, input, select, button, table {
    font-family: var(--font-mono);
    font-size: 0.875em;
    font-variant-numeric: tabular-nums;
}

/* The maths is MathML drawn in STIX Two Math. `OS/2.sxHeight` is 473/1000 for
   both STIX Two Text and STIX Two Math (the measurement the mono ratio above
   was taken by, read from the font tables), so the maths inherits the prose
   size and no ratio is tuned, where KaTeX's Computer Modern needed a 9%
   enlargement. The rest restates what the dropped vendor sheet gave the
   page, its weight and style reset included: only the regular face is
   declared, so maths inside a heading or an italic note would otherwise be
   synthesised bold or oblique. */
.katex {
    font-style: normal;
    font-weight: 400;
    line-height: 1.2;
    text-indent: 0;
}

.katex math {
    font-family: 'STIX Two Math', math;
}

.katex-display {
    display: block;
    margin: 1em 0;
}

.katex-display > .katex {
    display: block;
    white-space: nowrap;
}

/* MathML Core drops `columnalign` and `columnspacing` (Firefox still reads
   them), so `aligned` is restated in CSS for Chromium and WebKit. These also
   match an `array{rl}` and miss a two-pair `aligned` (`right left right left`),
   neither of which any calc emits today. */
.katex mtable[columnalign="right left"] > mtr > mtd {
    padding: 0.125em 0;
}

.katex mtable[columnalign="right left"] > mtr > mtd:nth-child(odd) {
    text-align: right;
}

.katex mtable[columnalign="right left"] > mtr > mtd:nth-child(even) {
    text-align: left;
}

a {
    color: var(--accent);
}

/* One focus indicator for everything that does not declare its own. Blueprint
   blue, because focus is interaction and interaction is the only thing blue
   means here. */
:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

a:hover {
    color: var(--accent-strong);
}

/* The mono capital links stand apart from prose by their face, case and
   blue, so they carry no underline until hovered or focused: a row of them
   underlined read louder than the page around it. */
.site-nav a,
.site-crumbs a {
    text-decoration: none;
}

.site-nav a:hover,
.site-nav a:focus-visible,
.site-crumbs a:hover,
.site-crumbs a:focus-visible,
.jump-derivation:hover,
.jump-derivation:focus-visible,
.jump-results:hover,
.jump-results:focus-visible {
    text-decoration: underline;
}

/* ============================================
   Layout - one centred column of one width (calc pages widen at 1440px)

   Every page is the same column: the index, a calc page, the policy pages and
   the validation page (#105). Header, main and footer fill it, so the header
   rule, the Given block, the tables and the footer share one left edge and one
   right edge. Sidenotes open inline at every width. From the workspace
   breakpoint a calc page alone widens to --workspace-width and lays out in two
   columns; every other page keeps the one width.

   Below the page width plus its padding the column is the viewport less the
   gutter, by `max-width` alone. That state is the majority product, not a
   fallback: plenty of engineers read this at 1280 with an IDE sidebar open.
   ============================================ */
.page {
    max-width: var(--page-width);
    margin-inline: auto;
    padding-inline: var(--space-6);
    padding-block: var(--space-2) var(--space-12);
}

/* Nothing may push the viewport sideways. Wide maths and wide tables scroll
   inside their own block instead; for a derivation step that block is its
   maths (`.calc-step .katex-display` below), not the step. */
.calc-results,
.calc-checks,
/* Positioned so anything placed absolutely inside is clipped with the block
   rather than widening the page past the scroller. */
.calc-table {
    position: relative;
    overflow-x: auto;
}

/* A refused table says so in the same register as a refused figure: quiet,
   and unmistakably not a table with nothing in it. */
.calc-table.is-refused p {
    color: var(--ink-muted);
    font-style: italic;
}

/* ============================================
   Calc title block (#331)

   The title row (heading, the edition and review pills, and at the right the
   save link and the sample sheets), the description, then the actions row
   (the way to the derivation at the left; units and the PDF report at the
   right) and one ink rule before the verdict. The pills are the only rounded
   shapes.
   ============================================ */
.calc-meta {
    padding-bottom: var(--space-6);
    margin-bottom: var(--space-6);
    border-bottom: 1px solid var(--ink);
}

.calc-meta + .calc-verdict {
    margin-top: 0;
}

/* The struct.work family bar (#331): a sunken band over the whole viewport with
   the three sites in muted mono capitals and this one marked. It sits outside
   `.page`, so the band spans the window while the links keep the page's left
   edge. */
.site-family {
    background: var(--surface-sunken);
    border-bottom: 1px solid var(--ink);
}

.site-family-links {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1) 1.5rem;
    max-width: var(--page-width);
    margin-inline: auto;
    padding: var(--space-2) var(--space-6);
    font-family: var(--font-mono);
    font-size: 0.75rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.site-family a {
    color: var(--ink-muted);
    text-decoration: none;
}

.site-family a:hover,
.site-family a:focus-visible {
    color: var(--accent);
}

.site-family a[aria-current] {
    color: var(--ink);
    border-bottom: 1px solid var(--ink);
}

/* The running header (#74, #331): the lockup on the left, the site's pages and
   the session controls on the right, over one ink hairline. The breadcrumb now
   sits under the rule in `.site-crumbs`. The mono register is set on the groups
   and not on the header, so the lockup stays in the prose face.

   Below the width where the groups fit the row wraps rather than truncating
   the lockup; the phone block near the end of the file sets the order. */
.site-header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2) var(--space-4);
    padding-block: var(--space-3);
    margin-bottom: var(--space-4);
    border-bottom: 1px solid var(--ink);
}

.site-crumbs,
.site-nav,
.site-session {
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-muted);
}

/* The breadcrumb trail under the header's rule (#331). A page without one
   leaves the div empty and `:empty` takes it out of the flow. */
.site-crumbs {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-2);
    margin-bottom: var(--space-4);
}

.site-crumbs:empty {
    display: none;
}

/* The lockup (#331): STRUCT. to struct.work, a hairline, then the disc mark and
   the name home. */
.site-lockup {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

.site-wordmark {
    font-family: var(--font-body);
    font-size: 1.25rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    color: var(--ink);
    text-decoration: none;
}

.site-wordmark span {
    color: var(--accent);
}

/* The name reads as a name, not as a label: prose face, normal case. */
.site-brand {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding-left: 0.75rem;
    border-left: 1px solid var(--rule-strong);
    font-family: var(--font-body);
    font-size: 1.25rem;
    color: var(--ink);
    text-decoration: none;
}

.site-brand img {
    width: 1.1em;
    height: 1.1em;
}

/* The edition select (#167): one option per calc of the group, each its own page. `.js` is on `<html>` before the first paint (JS_MARK), so a page without script does not show a control that cannot navigate. */
.calc-edition-select select {
    font: inherit;
    letter-spacing: 0;
    text-transform: none;
    color: var(--accent);
    background: var(--surface);
    border: 1px solid var(--accent);
    padding: 0 var(--space-1);
}

html:not(.js) .calc-edition-select {
    display: none;
}

.calc-title-row,
.calc-actions-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3) var(--space-4);
}

.calc-title-id {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2) var(--space-3);
}

.calc-meta h1 {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin: 0;
    font-size: var(--text-2xl);
}

/* The pills, the only rounded shapes on the page: the edition, the review
   mark and the units. No `white-space: nowrap`, so a long standard wraps
   inside its pill instead of overflowing at 390px. */
.calc-editions,
.calc-verified,
.units-toggle {
    display: inline-flex;
    align-items: center;
    padding: 0.3rem 0.7rem;
    border: 1px solid var(--accent);
    border-radius: 999px;
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    letter-spacing: 0.08em;
    line-height: 1.35;
    text-transform: uppercase;
    color: var(--accent);
    text-decoration: none;
}

/* A calc with no editions shows its standard as a plain span: muted, so it
   does not read as a link (#331). */
span.calc-editions {
    border-color: var(--rule-strong);
    color: var(--ink-muted);
}

a.calc-editions:hover,
a.calc-editions:focus-visible,
.calc-verified:hover,
.calc-verified:focus-visible {
    background: var(--accent-wash);
}

.calc-verified {
    border-color: var(--pass);
    color: var(--pass);
}

.calc-edition-select {
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-muted);
}

/* Save this case and the sample sheets: plain underlined links at the right
   of the title row, the auto margin keeping them right-aligned when they
   wrap under the title on a phone. */
.calc-aux {
    display: flex;
    align-items: center;
    gap: 0 1.5em;
    margin-left: auto;
    font-size: var(--text-sm);
}

.calc-aux p {
    margin: 0;
}

.calc-aux .report-thumb {
    width: 48px;
    height: auto;
}

.calc-description {
    margin: var(--space-2) 0 var(--space-3);
    color: var(--ink);
}

/* `.calc-actions` is also the button row of the subscribe, sign-in and login
   pages, so this one is scoped. */
.calc-actions-row .calc-actions {
    gap: var(--space-3);
    margin-left: auto;
}

/* ============================================
   Sections, tables and derivation steps

   The page should read as one composed document: a section heading is a rule
   with a word on it, the tables are data rather than boxes, and each
   derivation step is a block of its own with room to breathe.
   ============================================ */
/* Not `.calc > section`: the results, checks and derivation sections live
   inside the htmx swap target, so they are grandchildren. */
.calc section {
    margin-top: var(--space-12);
}

.calc h2 {
    margin: 0 0 var(--space-6);
    padding-bottom: var(--space-2);
    border-bottom: 1px solid var(--rule);
    font-size: var(--text-lg);
    font-weight: 600;
}

/* The Questions section. Closed by default: the answers are prose about the
   calc, and expanded they would push the derivation off the first screen. */
.calc-faq summary,
.calc-history summary,
.calc-scope summary {
    padding-block: var(--space-2);
    color: var(--ink);
    font-size: var(--text-base);
    cursor: pointer;
}

/* The Revision history section, shaped like the questions: one closed line per
   change, the moved values in a table when opened. */
.calc-history time {
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    color: var(--ink-muted);
    margin-inline-end: var(--space-2);
}

.calc-history .calc-table {
    margin-block: var(--space-2) var(--space-4);
}

.calc-history details p {
    margin-block: 0 var(--space-4);
    color: var(--ink-muted);
}

.calc-faq details p {
    margin-block: 0 var(--space-4);
    padding-inline-start: var(--space-4);
    color: var(--ink-muted);
}

/* The neighbours and the mail links, after the questions: a plain list, then
   one muted line, so the page ends quietly rather than on a call to action. */
.calc-related ul {
    margin-block: 0 var(--space-4);
    padding-inline-start: var(--space-4);
}

.calc-related p {
    font-size: var(--text-sm);
    color: var(--ink-muted);
}

/* The monthly email form, after the related links: one row of label, field and
   button when there is room, wrapping on a phone, with the consent line in the
   same muted small voice as the lines above it. */
.calc-signup h2 {
    font-size: var(--text-base);
}

.signup-form {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2) var(--space-3);
}

.signup-form input[type="email"] {
    flex: 1 1 14rem;
    min-width: 0;
    padding: var(--space-1) var(--space-3);
}

.signup-consent {
    font-size: var(--text-sm);
    color: var(--ink-muted);
}

/* The usefulness question: small outlined buttons in the muted line's voice,
   so it reads as part of the page's quiet end, not a survey. The question on
   the thank-you page wears the same. */
.calc-feedback button,
.found-us button {
    margin-inline-start: var(--space-2);
    padding: var(--space-1) var(--space-3);
    border: 1px solid var(--rule-strong);
    border-radius: var(--radius-sm);
    background: none;
    font: inherit;
    color: var(--ink);
    cursor: pointer;
}

.calc-feedback button:hover:not(:disabled),
.calc-feedback button:focus-visible,
.found-us button:hover:not(:disabled),
.found-us button:focus-visible {
    border-color: var(--accent);
    color: var(--accent);
}

.calc-feedback button:disabled,
.found-us button:disabled {
    cursor: default;
    opacity: 0.5;
}

.data-table {
    width: 100%;
    border-collapse: collapse;
}

.data-table th {
    padding: var(--space-2) var(--space-4) var(--space-2) 0;
    border-bottom: 1px solid var(--ink);
    font-size: var(--text-xs);
    font-weight: 500;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-muted);
    text-align: left;
}

/* A unit in a heading keeps its case: MPa set in capitals reads as another unit. */
.data-table th .katex {
    text-transform: none;
    letter-spacing: normal;
}

.calc-table.is-sortable th[data-sort] { cursor: pointer; }
.calc-table.is-sortable th[aria-sort="ascending"]::after { content: " \2191"; }
.calc-table.is-sortable th[aria-sort="descending"]::after { content: " \2193"; }

.data-table td {
    padding: var(--space-3) var(--space-4) var(--space-3) 0;
    border-bottom: 1px solid var(--rule);
    vertical-align: baseline;
}

.data-table tr:last-child td {
    border-bottom: none;
}

/* The symbol column is notation and stays narrow; the value column is the
   thing that changes, so it is mono with tabular figures and cannot reflow
   the table when a digit count changes. */
.result-symbol {
    width: 8rem;
}

/* The results table keeps the Given block's rhythm: symbol, what it means,
   the number, its unit. The description is muted for the same reason it is
   in a Given row - it labels the number, it is not the number. It is prose,
   so it takes the prose face and restores the prose size the base `table`
   rule shrinks to 0.875em; the value column stays mono and tabular (#105). */
.result-description {
    font-family: var(--font-body);
    font-size: var(--text-base);
    color: var(--ink-muted);
}

/* The number and its unit each hold a column of their own, so a long
   description cannot squeeze the value onto a second line. */
.result-value {
    width: 7rem;
    white-space: nowrap;
}

.result-unit {
    width: 5rem;
}

/* Half the page from 960px, so the symbol column gives back the width a
   full-width table could spare it. */
.result-table .result-symbol {
    width: 4.5rem;
}

.magnitude {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
}

/* Change-flash. The padding is always there, so the tint appearing cannot
   move anything; tabular figures mean the number itself keeps its width when
   its digits change. */
.result-number {
    padding: 2px 4px;
    margin: -2px -4px;
    border-radius: var(--radius-sm);
}

@keyframes calcsheet-change-flash {
    from {
        background-color: var(--flash-wash);
    }

    to {
        background-color: transparent;
    }
}

.result-number.is-changed {
    animation: calcsheet-change-flash var(--flash-duration) ease-out;
}

@media (prefers-reduced-motion: reduce) {
    .result-number.is-changed {
        animation: none;
    }
}

/* A recompute or a units toggle that has taken longer than 300 ms (#221).
   `calcsheet.js` adds the class; the previous results stay readable under
   it and the first heading says why. On the children, because at the
   workspace width `#results` is `display: contents`. No transition and no
   animation: the state appears and leaves at once, under reduced motion
   too. */
#results.is-updating > * {
    opacity: 0.55;
}

#results.is-updating > :first-child > h2::after {
    content: "Updating";
    margin-left: var(--space-3);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: 400;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-muted);
}

/* Derivation steps read left-aligned, the way they would be written out by
   hand. KaTeX centres display blocks by default, which reads as a slide. */
/* `content-visibility: auto`: a long derivation is most of a page's nodes,
   and the browser lays out and paints only the steps near the screen, on load
   and after every recompute. A skipped step is still in the document: find in
   page, anchors and screen readers reach it, and it renders as it scrolls in.
   `auto` in the intrinsic size keeps a step's real height once it has been
   rendered, so scrolling back up does not move the page. It also scopes CSS
   counters to the step, which matters only if a sidenote ever moves inside
   one: there is none today. Screen only, so print lays out every step.
   The checks and the results the same way: below the inputs on every page,
   so a recompute typed at the top lays out neither until it is scrolled to. */
.calc-step {
    position: relative;
    padding-block: var(--space-2) var(--space-4);
    border-bottom: 1px solid var(--rule);
}

@media screen {
    .calc-step {
        content-visibility: auto;
        contain-intrinsic-size: auto 8rem;
    }

    .calc-checks,
    .calc-results {
        content-visibility: auto;
        contain-intrinsic-size: auto 40rem;
    }
}

/* The step's head line: its number, the record behind it and the clause it
   comes from, flush right, above the maths rather than beside it. Beside would
   cost horizontal room, and horizontal room is what the derivation is short
   of - see the widths measured at U1.4. One line where the three fit, so it
   costs no more height than the number alone did; it wraps at phone width.
   12px mono text needs 4.5:1 on the surface, and `--ink-faint` measures
   3.66:1, so the anchor, its copied note and the range hints read in
   `--ink-muted` (7.49:1); the faint token stays for rules and strokes, where
   the text rule does not apply (#86). */
.step-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0 var(--space-4);
    margin-block-end: var(--space-1);
}

/* A touch target of at least 24px, now that it shares its line with the step's
   name and record (WCAG 2.2 target size). */
.step-anchor {
    display: inline-block;
    min-width: 24px;
    min-height: 24px;
    line-height: 24px;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    color: var(--ink-muted);
    text-decoration: none;
}

/* The step's name, in the text face, so a long derivation can be scanned by
   what each step works out. */
.step-title {
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--ink);
}

.step-anchor:hover,
.step-anchor:focus-visible {
    color: var(--accent);
}

/* Quiet confirmation, in the same mono, in place of nothing moving at all. */
.step-anchor[data-copied]::after {
    content: " link copied";
    color: var(--ink-muted);
}

:target.calc-step {
    background: var(--accent-wash);
}

.calc-step:last-child {
    border-bottom: none;
}

.calc-step .katex-display {
    margin: 0;
    text-align: left;
    /* KaTeX display blocks are width: 100%, and their own content is a couple
       of sub-pixels wider than that. Sizing them to their content instead
       stops a two-pixel scrollbar appearing on a step that fits. */
    width: max-content;
    max-width: 100%;
    /* The maths is the step's scroll box. KaTeX's ink runs two pixels past
       its own box, so the end padding keeps a step that fits from scrolling,
       or from wearing the fade below. A lone `overflow-x: auto` makes the
       vertical axis `auto` too, and the MathML's fraction bars, exponents and
       brackets run a few pixels above and below its box, so every step wore
       a vertical scrollbar (#326). The vertical axis is hidden, and the block
       padding holds that ink inside the box rather than clipping it. */
    overflow-x: auto;
    overflow-y: hidden;
    padding-block: 0.25em;
    padding-inline-end: 0.25em;
}

/* A step whose maths runs past the screen fades at its right edge until it is
   scrolled to the end; calcsheet.js sets and clears `data-overflow`. The mask
   is on the maths, so the clause on the step-number line stays whole. Nothing
   animates, so reduced motion needs no rule of its own. */
.calc-step[data-overflow] .katex-display {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 2.5rem), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - 2.5rem), transparent);
}

/* The block a keyboard can scroll (calcsheet.js gives it tabindex="0") shows the
   site's ring when focused. Drawn inside the box: the fade's mask-image above
   would clip an outline outside it. */
.calc-step .katex-display:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
}

.check-math:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
}

.calc-step .katex-display > .katex {
    text-align: left;
}

/* Which arm of a conditional ran, above the step it produced. Quiet, with
   the test typeset in KaTeX like the steps, and with its own bottom margin
   rather than a collapsing one so it reads as attached to the step below. */
.step-branch {
    margin-block: var(--space-4) 0;
    font-size: var(--text-xs);
    color: var(--ink);
}

/* A note on a step. Mono, because a clause reference is a citation and reads
   as one set that way. Muted rather than faint, and the branch line above
   too: they are text, and the faint token fails 4.5:1 (see `.step-anchor`). */
.step-note {
    margin-block: var(--space-2) 0;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    color: var(--ink);
}

/* The clause a step comes from ends the head line, straight after the record,
   so the two read as one phrase and it is read before the maths, not across
   the width of the page from it. */
.step-note-clause {
    margin: 0;
}

/* An assumption is a sentence, not a citation, so it is set in the text face
   and italicised - a reader weighs the two differently. */
.step-note-assumption {
    font-family: inherit;
    font-style: italic;
}

/* ============================================
   The Given section

   Typeset rows, not a form. Symbol, what it means, the value, its unit and
   the range it is allowed - the way the givens are written at the top of a
   hand calculation. The only chrome is a hairline under each editable value,
   which is the affordance: this number can be changed.
   ============================================ */
/* The symbol track has a floor and no fixed width. A name is an engineering
   symbol most of the time and fits in 4rem, but a `Choice` is named for what it
   selects - `electrode`, `edge_type`, `concrete_type` - and those are set as
   upright words wider than that. Fixed, they ran into the description beside
   them with no gap at all. `auto` lets the track take its widest symbol, and
   the `1fr` description absorbs the difference, so every row on a page stays
   aligned to the same column. */
.given-row {
    position: relative;
    display: grid;
    grid-template-columns: 0.75rem minmax(3rem, auto) minmax(0, 1fr) 6rem 4rem;
    align-items: baseline;
    gap: 0 var(--space-3);
    padding-block: var(--space-2);
    border-bottom: 1px solid var(--rule);
}

.given-row:last-of-type {
    border-bottom: none;
}

.given-description {
    color: var(--ink-muted);
}

.given-value {
    font-family: var(--font-mono);
    font-size: 0.875em;
    font-variant-numeric: tabular-nums;
    color: var(--ink);
    background: transparent;
    border: none;
    border-bottom: 1px solid var(--rule-strong);
    border-radius: 0;
    padding: var(--space-1) var(--space-1) 2px;
    width: 100%;
    text-align: right;
}

/* The browser's number spinner is form chrome, and this section has none.
   Arrow keys still step the value with `appearance: textfield`. */
.given-value {
    appearance: textfield;
}

.given-value::-webkit-outer-spin-button,
.given-value::-webkit-inner-spin-button {
    appearance: none;
    margin: 0;
}

.given-value:hover {
    border-bottom-color: var(--accent);
}

/* The 2px line is the 1px border and a 1px inset shadow, so focus changes no
   size: a row 1px taller re-balances the two Given columns (#172). */
.given-value:focus-visible {
    outline: none;
    border-bottom-style: solid;
    border-bottom-color: var(--accent);
    box-shadow: inset 0 -1px 0 var(--accent);
    background: var(--accent-wash);
}

/* A choice has no unit and no range to show, so it takes their columns. */
.given-choice {
    grid-column: 4 / -1;
    min-width: 0;
    text-align: left;
}

/* The dirty mark. Ink, not blue and not a verdict colour: it reports that a
   value was edited, which is neither an interactive element nor a pass. */
.given-dirty {
    align-self: center;
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: var(--ink);
    visibility: hidden;
}

.given-row.is-dirty .given-dirty {
    visibility: visible;
}

/* A refused value. Red because it is a failure, and prefixed with a glyph so
   it is still a failure without the colour. */
.calc-status {
    margin: var(--space-4) 0 0;
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    color: var(--fail);
}

.calc-status::before {
    content: "\2715  ";
}

/* Empty means there is nothing to say, so there is nothing to see. Lets the
   server clear the message by sending an empty element. */
.calc-status:empty {
    display: none;
}

/* The summary line under the header. The word carries the verdict and the
   colour only follows it. It is a direct child of article.calc, so it sticks
   for the whole calc and not for the header's height; z-index because KaTeX
   output is positioned and would paint over it. */
.calc-verdict {
    position: sticky;
    top: 0;
    z-index: 2;
    margin: var(--space-2) 0 var(--space-6);
    padding-block: var(--space-2);
    background: var(--surface);
    border-bottom: 1px solid var(--rule);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
}

.calc-verdict:empty {
    display: none;
}

html:has(.calc-verdict:not(:empty)) {
    scroll-padding-top: var(--strip-height);
}

/* The verdict word is set as the report stamps it, in a hairline box in its
   own colour, so PASS or FAIL is the first thing the line says. */
.calc-verdict .verdict-word {
    display: inline-block;
    margin-inline-end: var(--space-2);
    padding: 0 0.4em;
    border: 1px solid currentColor;
    border-radius: 2px;
    letter-spacing: 0.06em;
}

/* What the verdict covers, after the governing check, quieter than it. */
.verdict-count {
    margin-inline-start: var(--space-4);
    color: var(--ink);
}

.verdict-count::before {
    content: "\00b7";
    margin-inline-end: var(--space-4);
}

.calc-verdict.is-refused .verdict-word {
    color: var(--fail);
}

.calc-verdict.is-pass .verdict-word {
    color: var(--pass);
}

.calc-verdict.is-fail .verdict-word {
    color: var(--fail);
}

/* The Given row a refusal was about (#108): the fail wash on the row, the
   fail rule under the value where the focus rule sits, and the bound in words
   on a line of its own under the description, in the status line's register.
   Red only here, because it is a failure; the words carry the meaning, so the
   colour is never the only signal. Empty in every other row. */
.given-refused {
    display: none;
}

.given-row.is-refused {
    background: var(--fail-wash);
}

.given-row.is-refused .given-value {
    border-bottom: 2px solid var(--fail);
    box-shadow: none;
}

.given-row.is-refused .given-refused {
    display: block;
    grid-column: 3 / -1;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    color: var(--fail);
}

.given-row.is-refused .given-refused::before {
    content: "\2715  ";
}

/* The project fields for the PDF, asked for in a dialog when the PDF button
   is clicked (#107): a panel on the page's own paper with a hairline, rows in
   the family of `.given-row`, a text field where the number would be. The
   fake-door panel (#315) wears the same. */
dialog.door-panel,
dialog.title-block {
    width: min(28rem, calc(100% - 2 * var(--space-4)));
    padding: var(--space-6);
    border: 1px solid var(--rule-strong);
    border-radius: var(--radius-md);
    background: var(--surface);
    color: var(--ink);
}

/* Translucent ink, written out: `::backdrop` does not inherit the page's
   custom properties in every engine. The value is `--ink` at 45 percent. */
dialog.door-panel::backdrop,
dialog.title-block::backdrop {
    background: rgba(22, 22, 15, 0.45);
}

dialog.door-panel h2,
dialog.title-block h2 {
    margin-top: 0;
}

.title-block-note {
    margin: 0 0 var(--space-2);
    font-size: var(--text-sm);
    color: var(--ink-muted);
}

.title-block-field {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
    align-items: baseline;
    gap: 0 var(--space-3);
    padding-block: var(--space-2);
    border-bottom: 1px solid var(--rule);
}

.title-block-value {
    font-family: var(--font-mono);
    font-size: 0.875em;
    color: var(--ink);
    background: transparent;
    border: none;
    border-bottom: 1px solid var(--rule-strong);
    border-radius: 0;
    padding: var(--space-1) var(--space-1) 2px;
    width: 100%;
}

.title-block-value:hover {
    border-bottom-color: var(--accent);
}

/* Skip in the copy button's outlined register, Create PDF in the PDF
   button's filled one, so creating stays the primary action. */
.door-actions,
.title-block-actions {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-2);
    margin: var(--space-6) 0 0;
}

/* The Given heading row: the heading on the left, Reset on the right, both on
   the section rule. The rule moves from the heading to the row, or Reset would
   sit above the line (#107). */
.given-heading {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-4);
    margin-bottom: var(--space-3);
    padding-bottom: var(--space-2);
    border-bottom: 1px solid var(--rule);
}

.calc .given-heading h2 {
    margin: 0;
    padding: 0;
    border-bottom: none;
}

.given-reset {
    margin: 0;
    padding: 0;
    border: none;
    background: none;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--accent);
    cursor: pointer;
}

.given-reset:not(:disabled):hover,
.given-reset:not(:disabled):focus-visible {
    color: var(--accent-strong);
    text-decoration: underline;
}

/* Nothing to reset yet. Muted, not blue: a disabled control is not
   interactive, and blue means interactive. */
.given-reset:disabled {
    color: var(--ink-muted);
    cursor: default;
    text-decoration: none;
}

/* The first-visit hint and the resume offer: one quiet line each under the
   Given heading (#88). The hint shows on a scripted page until this browser's
   first edit, from the classes JS_MARK puts on `<html>` before the first paint;
   calcsheet.js shows the resume offer. */
.given-hint,
.given-resume {
    font-size: var(--text-sm);
    color: var(--ink);
}

.given-hint {
    display: none;
}

html.js:not(.hint-seen) .given-hint {
    display: block;
}

/* The header's way to the derivation, shown at every width (#264). */
.jump-derivation {
    display: inline-block;
    text-decoration: none;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

/* Under the Given heading on a phone, where the results sit below every
   input row. Hidden at desktop in the 960px block below. */
.jump-results {
    display: block;
    margin-inline-start: auto;
    text-decoration: none;
    text-align: right;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.given-unit {
    white-space: nowrap;
}

/* The allowed range, out of the row's flow so it costs the description no
   width, and shown only while the row is hovered or its value has focus: it
   is what a reader checks while changing the number, not while reading it. */
.given-bounds {
    position: absolute;
    right: 0;
    bottom: calc(-1 * var(--space-2));
    z-index: 1;
    padding: 0 var(--space-1);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-variant-numeric: tabular-nums;
    color: var(--ink-muted);
    background: var(--surface);
    white-space: nowrap;
    /* Faded rather than clipped or resized: the box is the same in every
       state. From 960px the Given groups balance into two columns, and a hint
       that grew on hover re-balanced them, which moved the last row of a column
       out from under the pointer, ended the hover and moved it back, every
       frame (#172). Opacity rather than `visibility: hidden`, so a screen
       reader still reads it through the input's `aria-describedby`; no pointer
       events, so the invisible box cannot take the row below it. */
    opacity: 0;
    pointer-events: none;
}

.given-bounds:not(:empty)::before {
    content: "range ";
}

.given-row:hover .given-bounds,
.given-row:focus-within .given-bounds {
    opacity: 1;
}

/* ============================================
   Subsections

   The Given block, the Checks and the Results each print as the groups their
   calc declares: a mono heading over a full rule, the rows under it. From
   960px the groups flow down the left column and then the right, newspaper
   fashion, and a group never splits across the two.
   ============================================ */
.given-group {
    min-width: 0;
    margin: 0;
    padding: 0;
    border: 0;
}

.group-heading {
    margin: 0 0 var(--space-1);
    padding: 0;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: 500;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink);
}

.group-heading + .given-row,
.group-heading + .data-table {
    border-top: 1px solid var(--ink);
}

.given-group + .given-group,
.block-group + .block-group {
    margin-top: var(--space-6);
}

@media (min-width: 960px) {
    .given-groups,
    .block-groups {
        columns: 2;
        column-gap: var(--space-12);
    }

    .given-group,
    .block-group {
        break-inside: avoid;
    }

    /* A block with no heading - the whole block, for a calc that declares no
       groups - is one long run of rows, so it breaks between rows to fill both
       columns rather than leaving the right one empty. */
    .given-group:not(:has(> .group-heading)),
    .block-group:not(:has(> .group-heading)) {
        break-inside: auto;
    }

    .given-row,
    .check-row,
    .condition-row,
    .result-table tr {
        break-inside: avoid;
    }

    /* In columns the gap above a group is the column's own top, so the margin
       moves below it: a group at the head of the right column then lines up
       with the one at the head of the left. */
    .given-group + .given-group,
    .block-group + .block-group {
        margin-top: 0;
    }

    .given-group,
    .block-group {
        margin-bottom: var(--space-6);
    }
}

/* A check is two lines: its name, D/C, bar and verdict, then the comparison
   under them across the whole column, since the substituted side is the
   long one. The statement cell dissolves into the row's grid, so the wash a
   failing or cautioned statement used to carry moves to the row. */
.check-table,
.check-table tbody,
.condition-table,
.condition-table tbody {
    display: block;
}

.check-row,
.condition-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 3rem 5rem 4.5rem;
    align-items: baseline;
    column-gap: var(--space-3);
    padding: var(--space-2) var(--space-2);
    border-bottom: 1px solid var(--rule);
}

.condition-row {
    grid-template-columns: minmax(0, 1fr);
}

.check-row:last-child,
.condition-row:last-child {
    border-bottom: none;
}

.check-table td,
.condition-table td {
    width: auto;
    padding: 0;
    border: 0;
}

.check-statement,
.condition-statement {
    display: contents;
}

.condition-sentence {
    grid-row: 1;
    grid-column: 1;
    display: block;
}

/* The sentence a condition or an N/A row prints is prose, not mathematics. */
.condition-sentence,
.check-math.check-na {
    font-family: var(--font-body);
}

/* The conditions follow the last check group, spaced as one group follows
   another. */
.block-groups + .group-heading,
.condition-table + .group-heading {
    margin-top: var(--space-6);
}

.check-name {
    grid-row: 1;
    grid-column: 1;
}

.check-math {
    grid-row: 2;
    grid-column: 1 / -1;
    min-width: 0;
    /* Scrolls sideways only; the padding holds the maths' ink inside the box,
       as for a derivation step (#326). More than a step's 0.25em: the maths
       here is set smaller, and the stacked fraction with a primed d in
       bolt-prying's fillet check still overran by a pixel at 0.25em. */
    padding-block: 0.375em;
    overflow-x: auto;
    overflow-y: hidden;
}

.check-table .check-ratio {
    grid-row: 1;
    grid-column: 2;
}

.check-table .check-bar {
    grid-row: 1;
    grid-column: 3;
    align-self: center;
}

.check-table .check-verdict {
    grid-row: 1;
    grid-column: 4;
    white-space: nowrap;
}

.check-clause {
    grid-row: 3;
    grid-column: 1 / -1;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    color: var(--ink-muted);
}

.check-hint {
    grid-row: 4;
    grid-column: 1 / -1;
    font-size: var(--text-sm);
    color: var(--fail);
}

/* On a phone the row folds: symbol and description on one
   line, value, unit and range on the next. */
@media (max-width: 559px) {
    .given-row {
        grid-template-columns: 0.75rem minmax(3rem, auto) minmax(0, 1fr) 4rem 5rem;
    }

    .given-description {
        grid-column: 3 / -1;
    }

    .given-value {
        grid-column: 3;
    }

    .given-choice {
        grid-column: 3 / -1;
    }
}

/* ============================================
   Hover-linked quantities

   Every occurrence of a quantity - its symbol, the number it was substituted
   for, its row in the Given block - lights up together. The tint is the
   neutral flash wash again, not blue: this is not something you can click.
   ============================================ */
[class*="sym-"] {
    border-radius: var(--radius-sm);
}

.is-linked {
    background: var(--flash-wash);
    box-shadow: 0 0 0 2px var(--flash-wash);
}

.given-row.is-linked {
    box-shadow: none;
}


@media (prefers-reduced-motion: no-preference) {
    [class*="sym-"],
    .given-row {
        transition: background-color var(--transition-fast);
    }
}

/* ============================================
   Checks and the governing limit state

   A verdict is never colour alone: the word PASS or FAIL and a glyph carry it,
   and the colour is the third signal, not the only one. Roughly one engineer
   in twelve cannot read the colour.
   ============================================ */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

th.check-ratio,
th.check-verdict {
    text-align: right;
}

.check-ratio {
    width: 4rem;
    text-align: right;
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    color: var(--ink-muted);
}

/* The author's own name for the check, from the assignment that produced it.
   A block label above the statement rather than its own column: a fifth fixed
   column would squeeze the substituted comparison, which is the long one. */
.check-name {
    display: block;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    color: var(--ink-muted);
}

.check-math {
    display: block;
}

/* What the band means, above the table that uses it. The threshold is the
   site's unless the calc declares one, so the page has to state it rather
   than leave the reader to infer one from an amber row. */
.check-caution-note {
    margin: 0 0 var(--space-4);
    font-size: var(--text-sm);
    color: var(--ink-muted);
}

.check-bar {
    width: 8rem;
}

.check-bar-track {
    display: block;
    height: 4px;
    background: var(--surface-sunken);
    border: 1px solid var(--rule);
}

.check-bar-fill {
    display: block;
    height: 100%;
    background: var(--pass);
}

.check-verdict {
    width: 6rem;
    text-align: right;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: 0.08em;
    color: var(--pass);
}

.check-glyph {
    font-size: var(--text-base);
    line-height: 1;
}

.check-row.is-fail .check-bar-fill {
    background: var(--fail);
}

.check-row.is-fail .check-verdict {
    color: var(--fail);
}

.check-row.is-fail {
    background: var(--fail-wash);
}

/* A check a false condition governs. Grey is reinforcement only: the row
   carries the word N/A, never colour alone. */
.check-row.is-na,
.check-row.is-na .check-verdict,
.check-row.is-na .check-ratio {
    color: var(--ink-muted);
}

/* What the calc leaves to the engineer, after the tables and closed like the
   questions: one line until the reader opens it. */
.calc-scope {
    margin-block: var(--space-6);
}

.check-scope {
    margin: var(--space-2) 0 0;
    padding-left: 1.25rem;
    font-size: var(--text-sm);
}

/* A check that passed but sits at or above the band its calc declared. The
   amber is reinforcement only: the row carries a `!` in the verdict column,
   which is DOM text and so survives grey paper and a colour-blind reader. */
.check-row.is-caution .check-bar-fill {
    background: var(--caution);
}

.check-row.is-caution .check-verdict {
    color: var(--caution);
}

.check-row.is-caution {
    background: var(--caution-wash);
}

/* The limit state a min or max actually selected. It is the number an
   engineer looks for first, so it is named in words, not just tinted. */
.result-governs {
    margin-left: var(--space-3);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-muted);
}

tr.is-governing td {
    background: var(--surface-sunken);
}

td.is-current {
    font-weight: 600;
    background: var(--surface-sunken);
}

.table-notes {
    margin: var(--space-2) 0 0;
    padding-left: var(--space-4);
    font-size: var(--text-xs);
    color: var(--ink-muted);
}

.result-link {
    color: inherit;
    text-decoration: none;
}

.result-link:hover,
.result-link:focus-visible {
    color: var(--accent);
    text-decoration: underline;
}

/* ============================================
   Figures

   Drawn in millimetres and scaled by the viewBox, so the geometry on screen
   is the calc's own numbers. Ink for the object, semantic colour for the one
   dimension the figure is about.
   ============================================ */
/* The Given block. The figure above the inputs at every width - textbook
   order, diagram then given. Not sticky: the figure scrolls away with its
   inputs, because it belongs to them.

   The block spans the page width like every other section (#105). At desktop
   the figure used to sit beside the form on a 2:1 split, the F2.2 ruling,
   which was the floor the bolt drawing stayed legible at in 1280 (277px), not
   a preference for small. The row retires it: at 1440 the drawing gets the
   whole page width instead of a fifth of the viewport (#85). At the workspace
   width the figure still leads the Given block, now inside the left column. */
.calc-given.has-figure {
    display: flex;
    flex-direction: column;
}

.calc-given.has-figure .calc-figures {
    order: -1;
    margin-bottom: var(--space-8);
}

.calc-figure {
    margin: var(--space-8) 0 0;
}

/* The margin is there to separate stacked figures, not to indent the block. */
.calc-figures > .calc-figure:first-child {
    margin-top: 0;
}

/* A chart's root <svg> comes out of matplotlib carrying a fixed pt width and
   none of our classes, so it is matched by where it sits. Without this a
   chart keeps its natural 432pt and overflows the column, taking its own
   legend and half its x axis off the side. */
.calc-figure > svg:not(.figure-svg) {
    display: block;
    width: 100%;
    max-width: 34rem;
    height: auto;
}

/* A drawn figure is sized by its subject, not by the window. Labels are 4% of
   the subject's extent (`Sheet.text`), so giving every extent the same
   pixels puts every figure at one scale with one label size, whatever the
   window's width or height. `--fig-w` is the viewBox width in extents,
   written by `Canvas.svg`. Below that width max-width shrinks it like any
   image, all figures together since they stack there. */
.figure-svg {
    --fig-extent: 25rem;
    display: block;
    width: calc(var(--fig-w, 1) * var(--fig-extent));
    max-width: 100%;
    height: auto;
}

/* At desktop the figure row takes the page width. Two figures sit side by
   side rather than stacking two screens tall. A drawn figure's box hugs its
   drawing, min-content so a long caption wraps under it instead of widening
   it; a chart still shares the row and is capped by the viewport. After the
   34rem cap above, which it lifts, so the order is load-bearing. */
@media (min-width: 960px) {
    .jump-results {
        display: none;
    }

    .calc-figures {
        display: flex;
        flex-wrap: wrap;
        gap: var(--space-8);
    }

    .calc-figure {
        flex: 1 1 24rem;
        margin: 0;
    }

    /* A drawn figure's box is as wide as its drawing: the caption is
       contained so it wraps under the drawing instead of widening it. The row
       is centred, as a lone drawing was when its box filled the row. When a
       pair is wider than the row they shrink rather than wrap, and flex takes
       from each in proportion to its width, so both keep the one scale. */
    .calc-figures:has(> .calc-figure > .figure-svg) {
        flex-wrap: nowrap;
        justify-content: center;
    }

    .calc-figure:has(> .figure-svg) {
        flex: 0 1 auto;
        width: max-content;
        min-width: 0;
        display: flex;
        flex-direction: column;
    }

    .calc-figure:has(> .figure-svg) figcaption {
        contain: inline-size;
    }

    /* Figures in one row stretch to the tallest; the auto margins centre a
       shorter drawing in that height and leave its caption at the foot. */
    .figure-svg {
        margin-block: auto;
    }

    .calc-figure > svg:not(.figure-svg) {
        max-width: none;
        max-height: 50vh;
    }
}

.figure-plate rect {
    fill: var(--surface-sunken);
    stroke: var(--ink-muted);
}

.figure-edge {
    stroke: var(--ink);
}

.figure-bolt {
    fill: var(--surface);
    stroke: var(--ink);
}

.figure-centre {
    stroke: var(--ink-faint);
    stroke-dasharray: 3 2 1 2;
}

.figure-minimum line {
    stroke: var(--ink-faint);
    stroke-dasharray: 2 2;
}

.figure-minimum text {
    fill: var(--ink-faint);
    font-family: var(--font-mono);
}

.figure-dimension text {
    font-family: var(--font-mono);
}

.figure-dimension.is-pass line,
.figure-dimension.is-pass path {
    stroke: var(--pass);
    fill: var(--pass);
}

.figure-dimension.is-pass text {
    fill: var(--pass);
}

/* A failed dimension changes in three ways at once: colour, weight and
   pattern. Any one of them read on its own still says something is wrong,
   which is the point - the colour is never carrying the meaning alone. */
.figure-dimension.is-fail line {
    stroke: var(--fail);
    stroke-width: 1.4%;
    stroke-dasharray: 3 1.5;
}

.figure-dimension.is-fail path {
    fill: var(--fail);
    stroke: var(--fail);
}

.figure-dimension.is-fail text {
    fill: var(--fail);
    font-weight: 700;
}

/* A figure that refuses says so where the drawing would have been, in the
   same register as the refusal message under the inputs: mono, small, quiet.
   The slot is reserved rather than collapsed, so a refusal on recompute does
   not move the page under the reader. */
.calc-figure.is-refused p {
    margin: 0;
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    color: var(--ink-muted);
}

/* ============================================
   Figure style layer

   One prescriptive drawing style, so every calc's figure reads as the same
   hand. Nothing here is px-fixed against the viewport: strokes and dashes are
   percentages, which SVG resolves against the viewBox, so they scale with the
   drawing and never with the browser window. Text and geometry are the one
   thing a stylesheet cannot size that way, so a figure computes those from its
   own extent (see `Sheet` in figures.py).

   ISO 128 / ISO 129 / ASME Y14.5 in the parts that matter here: two line
   weights at 2:1, filled arrowheads at about 3:1, dimension text horizontal,
   variables italic and units upright (ISO 80000-2).
   ============================================ */

/* The two weights. Everything that is the object is thick; everything that
   talks about the object - dimension, extension, centre, hidden, hatch - is
   thin, at exactly half. A drawing with three weights reads as three hands. */
.fig-object {
    fill: none;
    stroke: var(--ink);
    stroke-width: 1%;
    stroke-linejoin: round;
}

.fig-thin,
.fig-dimension,
.fig-extension,
.fig-centre,
.fig-hidden,
.fig-leader {
    fill: none;
    stroke: var(--ink-muted);
    stroke-width: 0.5%;
}

/* The object may be filled without becoming a second weight. */
.fig-object.is-solid {
    fill: var(--surface-sunken);
}

.fig-object.is-void {
    fill: var(--surface);
}

/* Reinforcement is an object (object weight, solid) and is told from the
   concrete by colour alone, which is why the token exists. `is-cut` is a bar
   seen end-on. */
.fig-rebar {
    fill: none;
    stroke: var(--rebar);
    stroke-width: 1%;
    stroke-linejoin: round;
}

.fig-rebar.is-cut {
    fill: var(--rebar);
    stroke: none;
}

/* Section hatch. A class rather than a `fill="url(...)"` attribute, because
   `.fig-object`'s own `fill: none` is CSS and would win against it. */
.fig-object.is-hatched,
.fig-rebar.is-hatched {
    fill: url(#fig-hatch-steel);
}

/* Dimension furniture is quieter than the object it measures. */
.fig-extension,
.fig-centre {
    stroke: var(--ink-faint);
}

/* The three dashes, all at the thin weight and all in percent, so the pattern
   is the same fraction of the drawing whatever the drawing is of. The ratios
   are the drafting ones: hidden 4:2, centre 12:3:3:3 (long, gap, short, gap). */
.fig-hidden {
    stroke-dasharray: 2% 1%;
}

.fig-centre {
    stroke-dasharray: 6% 1.5% 1.5% 1.5%;
}

/* Arrowheads. `markerUnits="strokeWidth"` is what keeps them proportional:
   the marker is drawn in multiples of the line's own width, which is itself a
   percentage of the drawing. Nothing to recompute per figure.

   `context-stroke` is what keeps them the right colour. Marker content is
   drawn from the defs, not from the line that references it, so it does not
   inherit the line's colour - a failed dimension would otherwise turn red and
   keep grey arrowheads. The first declaration is the fallback for a browser
   that does not know the keyword. */
.fig-arrow {
    fill: var(--ink-muted);
    fill: context-stroke;
    stroke: none;
}

/* The hatch line is the one width in this layer that is not a percentage. A
   percentage inside a pattern resolves against the pattern's own viewport,
   not the drawing's, which is zero-sized here and rounds the line away. The
   tile is in user units - millimetres, like every viewBox - so the pitch is a
   real 3mm and the line a real 0.4mm on every figure that uses it. */
/* A strut-and-tie node: a filled dot where members meet. Not an object and not
   a dimension, so it takes the annotation colour and no stroke of its own. */
.fig-node {
    fill: var(--ink-muted);
    stroke: none;
}

.fig-hatch {
    stroke: var(--ink-faint);
    stroke-width: 0.4;
}

/* Labels. Symbol in the site's italic, value and unit upright, which is the
   ISO 80000-2 convention and the same family the prose is set in, a sibling of
   the maths face. This is font usage, not layout: subscripts are plain tspans
   and nothing here tries to rebuild the typesetter.

   The halo is what makes a label readable over linework. `paint-order` puts
   the canvas-coloured stroke behind the glyph rather than over it. */
.fig-label {
    fill: var(--ink);
    font-family: 'STIX Two Text', 'Times New Roman', serif;
    paint-order: stroke fill;
    stroke: var(--surface);
    stroke-width: 0.6%;
    stroke-linejoin: round;
}

.fig-label .fig-sym {
    font-style: italic;
}

.fig-label .fig-sub {
    font-size: 0.72em;
}

/* A note is a label that is not a measurement: a minimum, a reference, an
   aside. Quiet enough to read second. */
.fig-note {
    fill: var(--ink-faint);
}

/* The verdict. Colour is never alone: a failed dimension also changes weight,
   changes to a dashed line, and its label says so in words and a glyph. Any
   one of those read on its own still says something is wrong, which is what
   makes the drawing legible in greyscale and to a reader who cannot separate
   red from green. */
.fig-verdict.is-pass .fig-dimension,
.fig-verdict.is-pass .fig-extension {
    stroke: var(--pass);
}

.fig-verdict.is-pass .fig-label {
    fill: var(--pass);
}

.fig-verdict.is-fail .fig-dimension {
    stroke: var(--fail);
    stroke-width: 0.75%;
    stroke-dasharray: 1.5% 0.75%;
}

.fig-verdict.is-fail .fig-extension {
    stroke: var(--fail);
}

.fig-verdict.is-fail .fig-label {
    fill: var(--fail);
    font-weight: 700;
}

/* A hidden line inside a verdict is a section a check is taken at. It keeps
   its dash, so a failed one says so by colour and weight. */
.fig-verdict.is-pass .fig-hidden {
    stroke: var(--pass);
}

.fig-verdict.is-fail .fig-hidden {
    stroke: var(--fail);
    stroke-width: 0.75%;
}

/* The same link, drawn the only way SVG can draw it. An SVG element has no
   background and no box-shadow, so the rules above paint nothing at all on a
   figure - the link would look wired up and do nothing. Accent is the one
   colour reserved for this, and it is transient: the verdict is still carried
   by the dash and by the label's own wording while a dimension is lit. */
.figure-svg .is-linked {
    background: none;
    box-shadow: none;
}

.figure-svg .is-linked .fig-object {
    stroke: var(--accent);
}

.figure-svg .is-linked .fig-rebar {
    stroke: var(--accent);
}

.figure-svg .is-linked .fig-rebar.is-cut {
    fill: var(--accent);
    stroke: none;
}

.figure-svg .is-linked .fig-dimension,
.figure-svg .is-linked .fig-extension,
.figure-svg .is-linked .fig-hidden,
.figure-svg .is-linked .fig-centre {
    stroke: var(--accent);
    stroke-width: 0.75%;
}

.figure-svg .is-linked .fig-label {
    fill: var(--accent);
}

/* The defs block carries the arrow marker and the hatch pattern. It draws
   nothing itself and must not take part in layout. */
.figure-defs {
    position: absolute;
    width: 0;
    height: 0;
    overflow: hidden;
}

.calc-figure figcaption {
    margin-top: var(--space-3);
    font-size: var(--text-sm);
    color: var(--ink);
}

/* ============================================
   The index

   The running header carries the brand and the search box on one row; under
   it the recent row, the h1 at display size, a sub-line, two actions, the
   animated hero stage, the trust line, then the groups (issues 106 and 306).
   ============================================ */

/* The page's `h1`, set at display size since #306 (the owner's approved hero
   copy). It stays the page's only `h1`: the brand is the header's, and every
   `<h2>` below is a category. */
.index-lede {
    max-width: 56.25rem;
    margin: var(--space-12) 0 0;
    font-size: 3rem;
    font-weight: 400;
    line-height: 1.1;
    letter-spacing: -0.012em;
    text-wrap: balance;
}

.index-sub {
    max-width: 45rem;
    margin: var(--space-6) 0 0;
    font-size: var(--text-lg);
    line-height: 1.5;
    color: var(--ink-muted);
}

.index-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-4) var(--space-6);
    margin: var(--space-8) 0 0;
}

/* Two classes, because `.calc-export` is declared later in the sheet. */
.index-actions .calc-export {
    padding: 0.8125rem 1.375rem;
    letter-spacing: 0.14em;
}

.index-trust {
    margin: var(--space-6) 0 var(--space-8);
    font-size: var(--text-sm);
    color: var(--ink);
}

/* The search box, boxed in a light rule with a glass inside it: a bare
   underline read as a caption, not a control (#82). The glass
   is a file beside this one, not a data URI, because the CSP's `default-src
   'self'` blocks `data:` images; as a background it cannot say `currentColor`,
   so it carries --ink-muted as a literal. No `display` here: `html:not(.js) .calc-search` keeps the box off a page
   without scripts (#62). Focus moves the box to the interaction blue. */
.calc-search {
    width: 16rem;
    padding: var(--space-1) var(--space-2) var(--space-1) 1.75rem;
    border: 1px solid var(--rule-strong);
    border-radius: 2px;
    background: transparent url('search.svg') no-repeat 0.5rem 50% / 1rem 1rem;
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    color: var(--ink);
    appearance: none;
}

.calc-search::placeholder {
    color: var(--ink-muted);
}

.calc-search:focus {
    border-color: var(--accent);
}

/* Keyboard focus thickens the blue box rather than drawing the global ring
   around it: a shadow, not a wider border, so the text does not move (#62). */
.calc-search:focus-visible {
    outline: none;
    box-shadow: 0 0 0 1px var(--accent);
}

/* Said once, where the groups were, when the filter leaves nothing. */
.search-empty {
    margin: 0 0 var(--space-8);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    color: var(--ink-muted);
}

/* The about paragraph closes the contents now, so it needs the same gap that
   separates one group from the next. */
.calc-about {
    margin-top: var(--space-8);
}

/* One block per category, so the second heading needs air above it. The base
   h2 rule sets no margin, and without this the next group's heading sits on
   the last row of the group before it. */
.contents + .contents {
    margin-top: var(--space-8);
}

/* The heading opens its category with a rule in full ink. 14px mono in muted
   ink is lighter than the serif titles under it, so without a mark the label
   did not read as a section start; the rule is what makes it scannable (#52). Sentence case, not tracked caps: the rule already
   does the separating, and caps here plus caps on every meta line was the one
   habit on the page that read as template output (#53). */
.contents h2 {
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    font-weight: 500;
    color: var(--ink-muted);
    border-top: 1px solid var(--ink);
    border-bottom: none;
    padding-top: var(--space-2);
    margin-bottom: var(--space-4);
}

.calc-index {
    margin: 0;
    padding: 0;
    list-style: none;
}

/* One grid over three items: the icon in the first column, the title over
   its description in the second. Two lines, not three: the mono code line
   went because the description already names the code in prose, and its
   room went to a larger icon and title (#82). The row gap is zero because
   the rows carry their own margins; the column gap is the air beside the icon.

   The grid is on the anchor, so every part of the row is clickable. That makes
   it an `a` the global rules would colour and underline whole, hence the two
   resets (#53). */
.calc-entry {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: 0 var(--space-3);
    align-items: baseline;
    padding-block: var(--space-4);
    border-top: 1px solid var(--rule);
    color: inherit;
    text-decoration: none;
}

/* The mark takes the first column for the whole row; the text stacks in the
   second. Drawn in `currentColor`, so it is the row's ink at every state. */
.calc-entry-icon {
    grid-row: 1 / span 2;
    align-self: start;
    width: 2.5rem;
    height: 2.5rem;
    margin-top: 0.15em;
}

.calc-entry-title,
.calc-entry-description {
    grid-column: 2;
}

.calc-icon {
    width: 1em;
    height: 1em;
    vertical-align: -0.12em;
}

/* The rule sits above each entry, and the first row has the heading's rule
   over it instead. A grid's first row is exact at any column count and its
   last row is not, hence top rather than bottom. On the `li`, not on
   `.calc-entry`: the anchor is its item's only child, so the bare
   `:first-child` would match every row and strip all the rules (#106). */
.calc-index > li:first-child > .calc-entry {
    border-top: none;
}

/* Heading leading, so a title that wraps in a two-column row reads as one. */
.calc-entry-title {
    font-size: var(--text-xl);
    line-height: var(--leading-tight);
    color: var(--ink);
}

/* The row is the link but only the title answers to the pointer: a background
   on the whole row is louder than the page anywhere else. The focus
   ring is the global `:focus-visible`, on the anchor itself. */
.calc-entry:hover .calc-entry-title,
.calc-entry:focus-visible .calc-entry-title {
    color: var(--accent);
    text-decoration: underline;
}

.calc-entry-description {
    margin: var(--space-1) 0 0;
    color: var(--ink);
}

/* From this width each group lays its entries two to a row and the page is
   half as long to scroll; the third column comes at 1200px. Grid rows share a
   height, so the rules over a row line up. */
@media (min-width: 960px) {
    .calc-index {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        column-gap: var(--space-8);
    }

    /* Stretched to its neighbour's height, the grid keeps its two lines at the
       top rather than spreading the spare height between them. */
    .calc-entry {
        height: 100%;
        box-sizing: border-box;
        align-content: start;
    }

    /* The second entry is on the first row too, under the heading's rule. */
    .calc-index > li:nth-child(2) > .calc-entry {
        border-top: none;
    }
}

/* Three to a row at the page width (#106). A column is about 330px, and about
   280px of it is left for the title beside the icon: at --text-xl five of the
   published titles wrapped, so the title steps down one size here only. */
@media (min-width: 1200px) {
    .calc-index {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }

    .calc-index > li:nth-child(3) > .calc-entry {
        border-top: none;
    }

    .calc-entry-title {
        font-size: var(--text-lg);
    }
}

/* ============================================
   The index hero (#306)

   A picture of the bolt-capacity page, the FAIL poster rendered by the server
   and moved by hero.js. Every length is `calc(N * var(--u))` with N the
   approved mockup's px, and `--u` is the column over the design width (1100
   desktop, 358 compact) through container units, so the stage scales with the
   column from the first paint and without script. Motion is `transform`,
   `opacity`, `clip-path` and colour only, so the loop shifts no layout. The
   stage reaches into the page padding at each side only so the window's and
   the card's shadows are not cut.
   ============================================ */

.hero {
    margin-top: var(--space-6);
}

.hero-stage {
    position: relative;
    container-type: inline-size;
    margin-inline: calc(-1 * var(--space-6));
    padding-inline: var(--space-6);
    /* The pointer is placed in px from the last layout; a resize while paused must not push it past the page. */
    overflow-x: clip;
    -webkit-mask-image: linear-gradient(to bottom, #000 0, #000 78.5%, transparent 100%);
    mask-image: linear-gradient(to bottom, #000 0, #000 78.5%, transparent 100%);
}

.hero-frame {
    --u: calc(100cqi / 1100);
    position: relative;
    height: calc(700 * var(--u));
}

.hero-win {
    position: absolute;
    left: 0;
    top: calc(24 * var(--u));
    bottom: 0;
    width: calc(960 * var(--u));
    overflow: hidden;
    background: var(--surface);
    border-radius: calc(8 * var(--u));
    box-shadow:
        0 0 0 1px color-mix(in srgb, var(--ink) 9%, transparent),
        0 calc(26 * var(--u)) calc(52 * var(--u)) calc(-22 * var(--u)) color-mix(in srgb, var(--ink) 28%, transparent),
        0 calc(10 * var(--u)) calc(22 * var(--u)) calc(-12 * var(--u)) color-mix(in srgb, var(--ink) 16%, transparent);
}

.hero-chrome {
    display: flex;
    align-items: center;
    gap: calc(6 * var(--u));
    height: calc(37 * var(--u));
    padding-inline: calc(14 * var(--u));
    background: var(--surface-sunken);
    border-bottom: 1px solid var(--rule);
}

.hero-dot {
    width: calc(9 * var(--u));
    height: calc(9 * var(--u));
    border-radius: 50%;
    background: var(--rule-strong);
}

.hero-url {
    flex: 1;
    margin-left: calc(14 * var(--u));
    height: calc(22 * var(--u));
    display: flex;
    align-items: center;
    padding-inline: calc(10 * var(--u));
    background: var(--surface);
    box-shadow: inset 0 0 0 1px var(--rule);
    border-radius: calc(6 * var(--u));
    color: var(--ink-muted);
    font-family: var(--font-body);
    font-size: calc(12 * var(--u));
}

.hero-body {
    padding: 0 calc(44 * var(--u));
}

.hero-crumbs {
    display: flex;
    align-items: center;
    gap: calc(9 * var(--u));
    height: calc(46 * var(--u));
    border-bottom: 1px solid var(--rule);
}

.hero-mark {
    flex: none;
    width: calc(16 * var(--u));
    height: calc(16 * var(--u));
    margin-right: calc(13 * var(--u));
}

.hero-crumb {
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: calc(11 * var(--u));
    line-height: 1;
    letter-spacing: 0.16em;
    text-transform: uppercase;
}

.hero-crumb--acc {
    color: var(--accent);
}

.hero-crumb--link {
    color: var(--accent);
    font-family: var(--font-body);
    font-size: calc(13 * var(--u));
    letter-spacing: 0;
    text-decoration: underline;
    text-underline-offset: calc(3 * var(--u));
    text-transform: none;
}

.hero-strip {
    display: flex;
    align-items: center;
    height: calc(38 * var(--u));
    margin-top: calc(14 * var(--u));
    border-top: 1px solid var(--ink);
    border-bottom: 1px solid var(--rule);
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: calc(11 * var(--u));
    line-height: 1;
    letter-spacing: 0.16em;
}

.hero-titlebar {
    display: flex;
    align-items: center;
    gap: calc(10 * var(--u));
    height: calc(66 * var(--u));
}

.hero-icon {
    flex: none;
    width: calc(28 * var(--u));
    height: calc(28 * var(--u));
}

.hero-title {
    font-family: var(--font-body);
    font-size: calc(30 * var(--u));
    font-weight: 600;
    line-height: 1;
    letter-spacing: -0.005em;
}

/* The verdict changes width without moving anything: the word sits absolutely
   placed and the sentence after it shifts by transform. A mono face advances
   every glyph by 1ch and the word adds 0.06em of spacing per character, so 4
   and 20 characters give exact widths. */
.hero-verdict {
    --pass-w: calc(4ch + 0.24em + 16 * var(--u) + 2px);
    --fail-w: calc(20ch + 1.2em + 16 * var(--u) + 2px);
    position: relative;
    display: flex;
    align-items: center;
    min-height: calc(48 * var(--u));
    padding-left: calc(var(--pass-w) + 14 * var(--u));
    border-bottom: 1px solid var(--rule);
    color: var(--ink);
    font-family: var(--font-mono);
    font-size: calc(12.5 * var(--u));
    line-height: 1.45;
}

.hero-word {
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
    display: grid;
    width: var(--pass-w);
    overflow: hidden;
    white-space: nowrap;
    padding: calc(2 * var(--u)) calc(8 * var(--u));
    border: 1px solid currentColor;
    border-radius: calc(2 * var(--u));
    color: var(--pass);
    font-weight: 500;
    letter-spacing: 0.06em;
}

.hero-word > span {
    grid-area: 1 / 1;
}

.hero-word-fail {
    opacity: 0;
}

.hero-verdict.is-fail .hero-word {
    width: var(--fail-w);
    color: var(--fail);
}

.hero-verdict.is-fail .hero-word-pass {
    opacity: 0;
}

.hero-verdict.is-fail .hero-word-fail {
    opacity: 1;
}

@media (min-width: 560px) {
    .hero-verdict.is-fail .hero-gov {
        transform: translateX(calc(var(--fail-w) - var(--pass-w)));
    }
}

.hero-cols {
    display: grid;
    grid-template-columns: calc(440 * var(--u)) 1fr;
    gap: calc(44 * var(--u));
    margin-top: calc(22 * var(--u));
}

.hero-given {
    border-top: 1px solid var(--ink);
}

.hero-row {
    display: grid;
    grid-template-columns: calc(30 * var(--u)) minmax(0, 1fr) calc(112 * var(--u)) calc(34 * var(--u));
    align-items: center;
    height: calc(44 * var(--u));
    padding: 0 calc(8 * var(--u));
    border-bottom: 1px solid var(--rule);
}

.hero-sym {
    justify-self: start;
    font-family: var(--font-body);
    font-size: calc(17 * var(--u));
    line-height: 1;
}

.hero-label {
    font-family: var(--font-body);
    font-size: calc(16 * var(--u));
}

.hero-val {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    height: calc(30 * var(--u));
    padding: 0 calc(4 * var(--u));
    border-bottom: 1px solid var(--rule-strong);
}

.hero-unit {
    padding-left: calc(8 * var(--u));
    color: var(--ink-muted);
    font-family: var(--font-body);
    font-size: calc(16 * var(--u));
}

.hero-bar {
    display: block;
    width: calc(var(--w) * var(--u));
    height: calc(7 * var(--u));
    border-radius: calc(4 * var(--u));
    background: color-mix(in srgb, var(--ink) 7%, transparent);
}

.hero-bar--dot {
    --w: 9;
    height: calc(9 * var(--u));
    border-radius: 50%;
}

.hero-bar--dot,
.hero-val .hero-bar {
    background: color-mix(in srgb, var(--ink) 9%, transparent);
}

.hero-bar--unit {
    height: calc(6 * var(--u));
    margin-left: calc(12 * var(--u));
}

.hero-figbar {
    --w: 250;
    margin-top: calc(22 * var(--u));
}

.hero-field {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    height: calc(30 * var(--u));
    padding: 0 calc(4 * var(--u));
    box-shadow: inset 0 -1px 0 var(--rule-strong);
    font-family: var(--font-mono);
    font-size: calc(14 * var(--u));
    font-variant-numeric: tabular-nums;
}

.hero-row--e.is-focus {
    background: var(--flash-wash);
}

/* An inset shadow, not a thicker border, so the digits do not move. */
.hero-row--e.is-focus .hero-field {
    background: var(--accent-wash);
    box-shadow: inset 0 calc(-2 * var(--u)) 0 var(--accent);
}

.hero-field.is-sel .hero-digits {
    background: color-mix(in srgb, var(--accent) 28%, transparent);
}

/* The caret takes no room, so the digits never step aside when it arrives. */
.hero-caret {
    width: calc(1.5 * var(--u));
    height: calc(16 * var(--u));
    margin-right: calc(-1.5 * var(--u));
    background: var(--accent);
    opacity: 0;
}

.hero-row--e.is-focus .hero-caret {
    opacity: 1;
}

.hero-roll {
    display: inline-grid;
    clip-path: inset(0);
    font-variant-numeric: tabular-nums;
}

.hero-roll > span {
    grid-area: 1 / 1;
}

.hero-roll > .is-below {
    transform: translateY(100%);
    opacity: 0;
}

.hero-roll > .is-above {
    transform: translateY(-100%);
    opacity: 0;
}

.hero-figcol {
    padding-top: calc(6 * var(--u));
}

.hero-drawing {
    display: block;
    width: calc(400 * var(--u));
    height: auto;
    overflow: visible;
}

.hero-plate {
    fill: var(--surface-sunken);
    stroke: var(--ink);
    stroke-width: 2.5;
}

.hero-bolt {
    fill: var(--surface);
    stroke: var(--ink);
    stroke-width: 2.5;
}

.hero-cl {
    stroke: var(--ink-faint);
    stroke-width: 1.2;
    stroke-dasharray: 11 3 2 3;
}

.hero-shaft {
    fill: none;
    stroke-width: 1.6;
    vector-effect: non-scaling-stroke;
}

.hero-shaft--ok {
    stroke: var(--pass);
}

.hero-shaft--bad {
    stroke: var(--fail);
    stroke-dasharray: 6 4;
    opacity: 0;
}

.hero-fig.is-fail .hero-shaft--ok {
    opacity: 0;
}

.hero-fig.is-fail .hero-shaft--bad {
    opacity: 1;
}

.hero-arrow {
    fill: var(--pass);
}

.hero-tick {
    stroke: var(--pass);
    stroke-width: 1.6;
}

.hero-fig.is-fail .hero-arrow {
    fill: var(--fail);
}

.hero-fig.is-fail .hero-tick {
    stroke: var(--fail);
}

.hero-fig-label {
    position: relative;
    display: flex;
    align-items: center;
    margin-top: calc(8 * var(--u));
    color: var(--pass);
    font-family: var(--font-body);
    font-size: calc(18 * var(--u));
    line-height: 1.3;
}

.hero-fig.is-fail .hero-fig-label {
    color: var(--fail);
    font-weight: 600;
}

.hero-x {
    position: absolute;
    left: 0;
    top: 50%;
    width: calc(11 * var(--u));
    height: calc(11 * var(--u));
    margin-top: calc(-5.5 * var(--u));
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    opacity: 0;
    transform: scale(0.6);
}

.hero-fig.is-fail .hero-x {
    opacity: 1;
    transform: none;
}

.hero-fig-text {
    display: flex;
    align-items: center;
    white-space: pre;
}

.hero-fig.is-fail .hero-fig-text {
    transform: translateX(calc(18 * var(--u)));
}

.hero-under {
    opacity: 0;
    clip-path: inset(0 100% 0 0);
}

.hero-fig.is-fail .hero-under {
    opacity: 1;
    clip-path: inset(0);
}

/* The derivation step as the site prints it, straddling the window's right
   edge over the header margin and never over the figure. */
.hero-card {
    position: absolute;
    left: calc(820 * var(--u));
    top: calc(118 * var(--u));
    width: calc(280 * var(--u));
    display: flex;
    flex-direction: column;
    gap: calc(10 * var(--u));
    padding: calc(16 * var(--u)) calc(17 * var(--u));
    background: var(--surface-raised);
    border-radius: calc(4 * var(--u));
    box-shadow:
        0 0 0 1px color-mix(in srgb, var(--ink) 10%, transparent),
        0 calc(16 * var(--u)) calc(30 * var(--u)) calc(-12 * var(--u)) color-mix(in srgb, var(--ink) 30%, transparent),
        0 calc(4 * var(--u)) calc(10 * var(--u)) calc(-4 * var(--u)) color-mix(in srgb, var(--ink) 12%, transparent);
}

.hero-card-head {
    display: flex;
    align-items: baseline;
    gap: calc(10 * var(--u));
}

.hero-card-n {
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: calc(11.5 * var(--u));
    line-height: 1;
    letter-spacing: 0.06em;
}

.hero-card-title {
    color: var(--ink);
    font-family: var(--font-body);
    font-size: calc(15 * var(--u));
    font-weight: 600;
}

.hero-card-cl {
    margin-top: calc(-4 * var(--u));
    color: var(--accent);
    font-family: var(--font-mono);
    font-size: calc(11 * var(--u));
    line-height: 1;
    letter-spacing: 0.08em;
}

.hero-math {
    display: grid;
    grid-template-columns: auto auto 1fr;
    column-gap: calc(8 * var(--u));
    row-gap: calc(4 * var(--u));
    font-family: var(--font-body);
    font-size: calc(19 * var(--u));
    line-height: 1.35;
}

.hero-math sub {
    font-size: 0.66em;
    line-height: 0;
}

.hero-lhs {
    text-align: right;
}

.hero-math > :nth-child(3n + 2) {
    text-align: center;
}

/* The lit trace, the site's `.is-linked` look: every place the edge distance
   appears lights in the neutral flash wash (not blue: it is not a control). */
.hero-stage.is-lit .hero-row--e {
    background: var(--flash-wash);
}

.hero-ln {
    border-radius: calc(2 * var(--u));
}

.hero-stage.is-lit .hero-ln {
    background: var(--flash-wash);
    box-shadow: 0 0 0 calc(3 * var(--u)) var(--flash-wash);
}

.hero-pointer {
    position: absolute;
    top: 0;
    left: 0;
    width: calc(14 * var(--u));
    height: auto;
    fill: var(--ink);
    stroke: var(--surface);
    stroke-width: 1.2;
    stroke-linejoin: round;
    opacity: 0;
    pointer-events: none;
}

.hero-pointer.is-on {
    opacity: 1;
}

/* Two classes, because `.link-button` sets `font: inherit` later in the sheet. */
.hero .hero-pause {
    display: block;
    min-height: 1.5rem;
    margin: var(--space-2) 0 0 auto;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

html:not(.js) .hero-pause {
    display: none;
}

/* Only with scripts: the loop is the one thing that moves, and a transition from a late font swap would move the poster of a page without them. */
@media (prefers-reduced-motion: no-preference) {
    html.js .hero-row {
        transition: background-color 220ms ease;
    }

    html.js .hero-field {
        transition: background-color 200ms ease, box-shadow 200ms ease;
    }

    html.js .hero-hole,
    html.js .hero-end {
        transition: transform 0.8s cubic-bezier(0.45, 0, 0.1, 1);
    }

    html.js .hero-shaft {
        transition: transform 0.8s cubic-bezier(0.45, 0, 0.1, 1), opacity 0.5s ease;
    }

    html.js .hero-arrow {
        transition: fill 0.25s;
    }

    html.js .hero-tick {
        transition: stroke 0.25s;
    }

    html.js .hero-fig-label {
        transition: color 0.25s;
    }

    html.js .hero-x {
        transition: opacity 0.3s, transform 0.3s;
    }

    html.js .hero-fig-text {
        transition: transform 0.5s cubic-bezier(0.45, 0, 0.1, 1);
    }

    /* Faint until the opening edge has passed the words. */
    html.js .hero-under {
        transition: clip-path 0.5s cubic-bezier(0.45, 0, 0.1, 1), opacity 0.3s ease 0.25s;
    }

    html.js .hero-word {
        transition: width 0.5s cubic-bezier(0.45, 0, 0.1, 1), color 0.22s;
    }

    html.js .hero-word > span {
        transition: opacity 0.25s;
    }

    html.js .hero-gov {
        transition: transform 0.5s cubic-bezier(0.45, 0, 0.1, 1);
    }

    html.js .hero-ln {
        transition: background-color 0.2s, box-shadow 0.2s;
    }

    html.js .hero-roll > span {
        transition: transform 400ms cubic-bezier(0.45, 0, 0.1, 1), opacity 400ms ease;
    }

    html.js .hero-roll > .is-below {
        transition: none;
    }

    html.js .hero-pointer {
        transition-property: transform, opacity;
        transition-duration: 0ms, 200ms;
        transition-timing-function: cubic-bezier(0.45, 0, 0.1, 1), ease;
    }
}

/* The script never starts here, so a button that stops nothing is gone. */
@media (prefers-reduced-motion: reduce) {
    .hero .hero-pause {
        display: none;
    }
}

/* Compact: a redesign for the phone column. The verdict, the edge distance and
   the figure, in a stage no taller than it is wide (358 by 358 at 390). */
@media (max-width: 559px) {
    .index-lede {
        margin-top: var(--space-8);
        font-size: 1.875rem;
    }

    .index-sub {
        margin-top: var(--space-4);
        font-size: var(--text-base);
    }

    .index-actions {
        margin-top: var(--space-6);
    }

    .hero-stage {
        margin-inline: 0;
        padding-inline: 0;
        -webkit-mask-image: linear-gradient(to bottom, #000 0, #000 95%, transparent 100%);
        mask-image: linear-gradient(to bottom, #000 0, #000 95%, transparent 100%);
    }

    .hero-frame {
        --u: calc(100cqi / 358);
        height: calc(358 * var(--u));
    }

    .hero-win {
        top: calc(4 * var(--u));
        width: 100%;
    }

    .hero-body {
        padding: 0 calc(16 * var(--u));
    }

    .hero-crumbs,
    .hero-strip,
    .hero-titlebar,
    .hero-card,
    .hero-row:not(.hero-row--e) {
        display: none;
    }

    .hero-verdict {
        display: block;
        min-height: calc(70 * var(--u));
        padding: calc(36 * var(--u)) 0 0;
        border-bottom: 0;
        font-size: calc(11.5 * var(--u));
    }

    .hero-word {
        top: calc(6 * var(--u));
        transform: none;
    }

    .hero-cols {
        display: block;
        margin-top: 0;
    }

    .hero-row {
        grid-template-columns: calc(22 * var(--u)) minmax(0, 1fr) calc(96 * var(--u)) calc(30 * var(--u));
    }

    .hero-fig {
        margin-top: calc(12 * var(--u));
    }

    .hero-drawing {
        width: calc(260 * var(--u));
    }

    .hero-figbar {
        --w: 210;
        margin-top: calc(12 * var(--u));
    }
}

/* Between the 960px breakpoint and the workspace, the figures sit beside the
   Given rows rather than above them, so the first inputs are on the first
   screen. The split is even, which leaves a drawing more room than its 25rem
   extent asks for: #85 retired a 2:1 split because the drawing got a fifth of
   the window there, and this gives it half. Two or more figures stack in that
   column. One figure sticks under the verdict strip while its rows scroll
   past; a stack is too tall to stick. From 1440px the workspace takes over. */
@media screen and (min-width: 960px) and (max-width: 1439px) {
    .calc-given.has-figure {
        display: grid;
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
        column-gap: var(--space-8);
        align-items: start;
    }

    .calc-given.has-figure .calc-form {
        grid-column: 1;
        grid-row: 1;
    }

    .calc-given.has-figure .calc-figures {
        grid-column: 2;
        grid-row: 1;
        min-width: 0;
        margin-bottom: 0;
        flex-direction: column;
        align-items: center;
    }

    .calc-given.has-figure .calc-figure:has(> .figure-svg) {
        width: auto;
        max-width: 100%;
    }

    .calc-given.has-figure .calc-figures:has(> .calc-figure > .figure-svg) {
        flex-wrap: wrap;
    }

    .calc-given.has-figure .calc-figures:not(:has(.calc-figure + .calc-figure)) {
        position: sticky;
        top: calc(var(--strip-height) + var(--space-4));
    }

    .calc-given.has-figure .given-groups {
        columns: 1;
    }
}

/* The workspace: from this width a calc page is two columns, the inputs on the
   left and the answer on the right, so changing a value and reading the verdict
   needs no scrolling. `span 4` reserves the right column's four possible rows
   (verdict, checks, results, tables) so the left column's grid area is as tall
   as the right column and a sticky item has room to stick; a missing section
   leaves an empty zero-height row. The derivation and everything after it
   auto-place below both: `.calc > *` spans both columns for the direct
   children, and the derivation, which is a child of `#results` and only a grid
   item because `#results` is dissolved, needs its own `#results >
   .calc-derivation` rule (locked or not, both carry the class); the rows the
   left column occupies are skipped. `dense` because the
   verdict, placed first in column 2, would otherwise move the cursor past
   column 1 and drop the inputs a row below it. `display: contents`
   dissolves `#results` so its sections are grid items, and `[hidden]` is
   restored because gated mode sets it. One column of groups, because half a
   page is narrower than the 960px breakpoint's two columns were designed for.
   `:has` scopes the wider page to calc pages, keeping #105's one width on the
   index and the policy pages.

   The breakpoint is 1440px, picked from screenshots of rc-shear-torsion at
   1280, 1440, 1536, 1920 and 2560: 1280 stays one column; at 1440 and 1536 the
   section drawing is as large as it was in the one-column 1920 page (capped by
   50vh, not by the column), no Given row takes a third line, no check scrolls
   sideways and nothing overflows. 1440 is the smaller of the two that pass,
   and a 1536 logical screen gets the workspace. */
@media screen and (min-width: 1440px) {
    :root {
        --workspace-width: 1800px;
    }

    .page:has(> main > .calc) {
        max-width: var(--workspace-width);
    }

    /* The family bar lines up with the widened calc page (#331). */
    body:has(> .page > main > .calc) .site-family-links {
        max-width: var(--workspace-width);
    }

    .calc {
        display: grid;
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
        column-gap: var(--space-12);
        grid-auto-flow: row dense;
        align-items: start;
    }

    .calc > *,
    #results > .calc-derivation {
        grid-column: 1 / -1;
    }

    .calc-inputs-column {
        grid-column: 1;
        grid-row: span 4;
        align-self: stretch;
    }

    .calc-inputs {
        position: sticky;
        top: var(--space-4);
        max-height: calc(100vh - 2 * var(--space-4));
        overflow-y: auto;
    }

    .calc-verdict,
    .calc-checks,
    .calc-results,
    .calc-table {
        grid-column: 2;
    }

    .calc-verdict {
        position: static;
        border-bottom: 0;
        padding-block: 0;
    }

    html:has(.calc-verdict:not(:empty)) {
        scroll-padding-top: 0;
    }

    #results {
        display: contents;
    }

    #results[hidden] {
        display: none;
    }

    .given-groups,
    .block-groups {
        columns: 1;
    }
}

.site-footer {
    margin-top: var(--space-12);
    padding-top: var(--space-4);
    border-top: 1px solid var(--rule);
    color: var(--ink-muted);
}

/* The size goes on the paragraph, not the footer, so the footer's own box
   keeps the page's font and only its text is set small. */
.site-footer p {
    margin: 0;
    font-size: var(--text-sm);
}

/* The footer holds two paragraphs now, the disclaimer and the links. Without
   this the second sits on the last line of the first. */
.site-footer p + p {
    margin-top: var(--space-2);
}

/* The footer's links take the footer's muted ink (#331). */
.site-footer a {
    color: inherit;
}

/* The struct.work family row closes the footer over an ink hairline (#331). */
.site-family-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem 1.25rem;
    margin-top: var(--space-4);
    padding-top: var(--space-3);
    border-top: 1px solid var(--ink);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.site-family-row a {
    text-decoration: none;
}

.site-family-row a:hover,
.site-family-row a:focus-visible {
    color: var(--accent);
    text-decoration: underline;
}

/* The sign-in line, at the header's right (#74). A flex row wraps an item at
   its full width, so on a narrow screen the line moves to its own row whole,
   and breaks inside only if even that row is too narrow for it. The form is
   inline so the button sits on the sentence it finishes. */
.site-session {
    text-align: right;
}

/* The anonymous sign-in is the session line's only child, drawn as a bordered
   button, so its link line stays as it was (#331). */
.site-session > a:only-child {
    display: inline-block;
    padding: 0.3rem 0.7rem;
    border: 1px solid var(--ink);
    color: var(--ink);
    text-decoration: none;
}

.site-session > a:only-child:hover,
.site-session > a:only-child:focus-visible {
    color: var(--accent);
    border-color: var(--accent);
}

/* The site's own pages, at the header's right ahead of the session line. The
   auto margin is here rather than on `.site-session`, so the two sit together
   instead of `space-between` parking the link in the middle (#84). */
.site-nav {
    display: flex;
    gap: 1.4rem;
    margin-left: auto;
}

/* Muted mono capitals, blue on hover or focus (#331). */
.site-nav a {
    color: var(--ink-muted);
}

.site-nav a:hover,
.site-nav a:focus-visible {
    color: var(--accent);
}

/* On the index the search box sits between the brand and the nav; it takes the
   auto margin so it and About share the right edge, rather than the two
   splitting the free space and parking the box mid-row (#106). */
.site-header .calc-search {
    margin-left: auto;
}

.page-index .site-nav {
    margin-left: 0;
}

/* A page without scripts has no search box, since a box that filters nothing
   is worse than none (#62), and the nav takes the right edge back as on every
   other page. `.js` is set on `<html>` before the first paint (JS_MARK), so a
   scripted page lays the box out from the start and nothing moves (#273). */
html:not(.js) .calc-search {
    display: none;
}

html:not(.js) .page-index .site-nav {
    margin-left: auto;
}

.site-session form {
    display: inline;
}

/* A button that reads as the link beside it would. It is a button because
   signing out is a POST; it is blue because it is interactive. */
.link-button {
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    letter-spacing: inherit;
    text-transform: inherit;
    color: var(--accent);
    text-decoration: underline;
    cursor: pointer;
}

.link-button:hover {
    color: var(--accent-strong);
}

/* Draft, Pro and New marks. Neutral ink on a rule, because a draft, a tier or
   a recent addition is a state of the document rather than a verdict or
   something to click: green, amber and red mean pass, caution and fail, and
   blue means interactive. Set like the sheet number, mono caps, since it is a
   label on the sheet. */
.draft-badge,
.pro-badge,
.new-badge {
    display: inline-block;
    padding: 0 var(--space-1);
    border: 1px solid var(--rule-strong);
    border-radius: var(--radius-sm);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-muted);
    vertical-align: middle;
}

.draft-banner {
    margin: 0 0 var(--space-6);
    padding: var(--space-3) var(--space-4);
    border: 1px dashed var(--rule-strong);
    background: var(--surface-sunken);
    font-size: var(--text-sm);
    color: var(--ink-muted);
}

.draft-banner .draft-badge {
    margin-right: var(--space-2);
}

/* A draft's readiness checklist, closed under the banner and in the same
   register: a review aid for the owner, so neutral ink, and never the verdict
   colours even where a row reads `no`. */
.draft-checklist {
    margin: 0 0 var(--space-6);
    padding: var(--space-3) var(--space-4);
    border: 1px dashed var(--rule-strong);
    background: var(--surface-sunken);
    font-size: var(--text-sm);
    color: var(--ink-muted);
}

.draft-checklist summary {
    cursor: pointer;
}

.draft-checklist h3,
.draft-checklist h4 {
    margin: var(--space-4) 0 var(--space-2);
    font-size: var(--text-base);
}

.draft-checklist .calc-table {
    margin-block: var(--space-2) var(--space-4);
}

/* The one action on the page that leaves it, in the title block level with
   the heading. Filled blueprint blue: it is interactive, which is the only
   thing blue means here, and it is the loudest shape that rule allows. The
   global focus ring sits 2px outside the fill, so it stays visible. */
.calc-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: var(--space-2);
    flex-shrink: 0;
    margin: 0;
}

.calc-export,
.plan-button,
.title-block-create {
    display: inline-block;
    padding: var(--space-2) var(--space-4);
    border: 1px solid var(--accent);
    border-radius: var(--radius-sm);
    background: var(--accent);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--surface);
    text-decoration: none;
    cursor: pointer;
}

.calc-export:hover,
.calc-export:focus-visible,
.plan-button:hover,
.plan-button:focus-visible,
.title-block-create:hover,
.title-block-create:focus-visible {
    background: var(--accent-strong);
    border-color: var(--accent-strong);
}

.plan-button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    pointer-events: none;
}

/* Outlined, not filled: it still goes somewhere, to the page that shows the
   report and says what Pro is, so it reads as a live control in the blue the
   site keeps for interactive things, one step quieter than the free calc's
   filled button. The badge inside it is the same one the index uses. */
.calc-export.is-locked {
    background: none;
    color: var(--accent);
    border-color: var(--accent);
}

.calc-export.is-locked:hover,
.calc-export.is-locked:focus-visible {
    color: var(--accent-strong);
    background: var(--accent-wash);
    border-color: var(--accent-strong);
}

.calc-export .pro-badge {
    margin-left: var(--space-1);
}

/* Outlined beside the filled PDF button, so the report stays the primary
   action, with the step anchor's quiet copied state. The dialog's Skip button
   shares the outline and keeps the mono caps; the copy button is an icon. */
.calc-copy,
.door-close,
.title-block-skip {
    display: inline-block;
    padding: var(--space-2) var(--space-4);
    border: 1px solid var(--accent);
    border-radius: var(--radius-sm);
    background: none;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--accent);
    cursor: pointer;
}

/* The two links in one pill, the system on screen in ink; links, so the
   switch works without script. The pill's own shape and mono register come
   from the shared pill rule beside `.calc-editions`. */
.units-toggle {
    align-self: center;
    gap: 0.6rem;
}

.units-toggle a {
    color: var(--accent);
    text-decoration: none;
}

.units-toggle a + a {
    padding-left: 0.6rem;
    border-left: 1px solid var(--rule-strong);
}

.units-toggle a[aria-current] {
    color: var(--ink);
    font-weight: 600;
}

.units-toggle a:not([aria-current]):hover,
.units-toggle a:not([aria-current]):focus-visible {
    text-decoration: underline;
}

.derivation-units {
    font-size: var(--text-sm);
    color: var(--ink-muted);
    margin: 0 0 1rem;
}

/* A square the height of the PDF button, which `.calc-actions` stretches it
   to; the glyph is inline so it can say `currentColor` under `default-src
   'self'` (#111). */
.calc-copy {
    display: inline-flex;
    align-items: center;
    padding: 0 var(--space-2);
}

.calc-copy svg {
    width: 1rem;
    height: 1rem;
}

.calc-copy[hidden] {
    display: none;
}

.calc-copy:hover,
.calc-copy:focus-visible,
.door-close:hover,
.door-close:focus-visible,
.title-block-skip:hover,
.title-block-skip:focus-visible {
    color: var(--accent-strong);
    border-color: var(--accent-strong);
}

.calc-copy[data-copied] {
    color: var(--ink-muted);
    border-color: var(--rule-strong);
}

/* The index's recent row, in the meta row's mono caps register for its label
   and an inline list of links after it. It opens the page, above the h1
   (#306), so it carries no top margin of its own. */
.calc-recent {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-2) var(--space-4);
    margin-block: 0;
}

.calc-recent[hidden] {
    display: none;
}

.calc-recent-label {
    margin: 0;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-muted);
}

.calc-recent ul {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-4);
    margin: 0;
    padding: 0;
    list-style: none;
}

/* Phone width (#331): sign-in beside the lockup, the nav on its own line, the
   index search last at the column's width. 640 because the desktop header row
   needs about 620px. */
@media (max-width: 640px) {
    .calc-search {
        width: 100%;
    }

    .site-lockup {
        gap: 0.5rem;
    }

    .site-wordmark,
    .site-brand {
        font-size: 1.0625rem;
    }

    .site-brand {
        padding-left: 0.5rem;
    }

    .site-session {
        order: 2;
        margin-left: auto;
    }

    .site-session > a:only-child {
        padding: 0.25rem 0.55rem;
        font-size: 0.6875rem;
        letter-spacing: 0.04em;
    }

    .site-nav,
    .page-index .site-nav {
        order: 3;
        flex-basis: 100%;
        justify-content: space-between;
        gap: var(--space-2);
        margin-left: 0;
        padding-top: var(--space-2);
        border-top: 1px solid var(--rule);
        font-size: 0.75rem;
        letter-spacing: 0.04em;
    }

    .site-header .calc-search {
        order: 4;
        width: 100%;
        margin-left: 0;
    }

    .calc-meta h1 {
        font-size: var(--text-xl);
    }

    /* Units and the PDF button start the actions row's second line at the
       left, under the derivation link. */
    .calc-actions-row .calc-actions {
        margin-left: 0;
    }

    .site-family-links {
        gap: var(--space-1) 1.1rem;
        font-size: 0.6875rem;
        letter-spacing: 0.04em;
    }
}

/* ============================================
   Sidenotes

   Mechanics cribbed from Tufte CSS: a numbered marker, a hidden checkbox and
   the note. The marker is a toggle at every width, closed by default, and the
   note opens inline under its paragraph, never floated beside the column.
   Not floated (#66): an open floated note pushed the Given table and its
   figure down by the note's full height.
   ============================================ */
body {
    counter-reset: sidenote-counter;
}

.sidenote {
    display: none;
    font-size: var(--text-sm);
    line-height: 1.45;
    color: var(--ink-muted);
    text-indent: 0;
    vertical-align: baseline;
}

.sidenote-number {
    counter-increment: sidenote-counter;
}

.sidenote-number::after,
.sidenote::before {
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    line-height: 0;
    vertical-align: super;
}

.sidenote-number::after {
    content: counter(sidenote-counter);
    color: var(--accent);
    cursor: pointer;
}

.sidenote::before {
    content: counter(sidenote-counter) " ";
    color: var(--ink-muted);
}

label.sidenote-number {
    display: inline;
}

/* The numbered marker is itself the toggle, so there is no second label to
   show or hide. The checkbox is clipped rather than `display: none`, which
   would take it out of the tab order and leave the note openable only with a
   pointer. */
input.margin-toggle {
    display: inline;
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
}

label.sidenote-number:has(+ input.margin-toggle:focus-visible) {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.margin-toggle:checked + .sidenote {
    display: block;
    float: none;
    clear: both;
    width: 100%;
    margin: var(--space-3) 0;
    padding-left: var(--space-4);
    border-left: 2px solid var(--rule);
}

@media (max-width: 480px) {
    .page {
        padding-inline: var(--space-4);
    }

    .site-family-links {
        padding-inline: var(--space-4);
    }
}

/* ============================================
   Print styles

   A printed or saved-as-PDF copy carries the title block, the Given block
   with its figure, the results, the checks and any tables, then the footer.
   Only the derivation stays off paper, along with the FAQ, the header and
   the buttons: hiding the rest protected nothing a screenshot does not
   already take (docs/decisions/paid-exports.md). `.print-only` carries the
   notice that says the derivation is on the PDF report. Selectors are
   semantic classes owned by calcsheet templates,
   not framework internals, so no specificity overrides are needed.
   ============================================ */
.print-only {
    display: none;
}

/* The sample report page the pricing pages show (#201): the sheet at the
   column's width, with a hairline so the white page reads as one. */
.report-sample {
    display: block;
    width: 100%;
    max-width: 36rem;
    height: auto;
    margin: var(--space-4) 0;
    border: 1px solid var(--rule);
}

/* The record behind a step, on the step number's line after the number, in
   the step's citation register: what the step's row on the validation page
   says, linked to that row. The kind is set apart so "Compared with" and
   "Accepted by the author" read at a glance down a long derivation. */
.step-check {
    margin: 0;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    color: var(--ink);
}

.step-check a {
    color: inherit;
    text-decoration-color: var(--rule-strong);
    text-underline-offset: 0.2em;
}

.step-check a:hover,
.step-check a:focus-visible {
    color: var(--accent);
    text-decoration-color: currentColor;
}

.step-check-kind {
    color: var(--ink);
}

/* A resistance factor in a citation, phi_b set as the code sets it. The phi
   is drawn in STIX Two Math, the one face with Greek in it; the subscript
   stays in the citation's own face. */
.phi {
    font-family: 'STIX Two Math', var(--font-body);
    font-size: 1.1em;
    line-height: 1;
}

.step-note sub,
.step-check sub,
.check-clause sub,
.calc-verdict sub,
.validation-table sub {
    font-size: 0.8em;
    line-height: 0;
}

/* The way down to the report's sheets, at the right of the title row: the
   small sheet is the report itself, drawn from its PDF, and its words are a
   plain underlined link. */
.calc-report-line a {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
}

/* What the report costs this reader, one muted line right-aligned under the
   actions row (#312); a locked reader's price is a link in the site's blue. */
.calc-price,
.calc-download,
.calc-pro-line {
    margin: var(--space-2) 0 0;
    font-size: var(--text-sm);
    color: var(--ink-muted);
    text-align: right;
}

/* The fake doors (#315): a quiet underlined text button in the site's blue,
   since it is interactive, in the voice of the line it sits in. Only script
   opens the panel, so without script neither shows. */
.calc-door {
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: var(--accent);
    text-decoration: underline;
    cursor: pointer;
}

.calc-door:hover,
.calc-door:focus-visible {
    color: var(--accent-strong);
}

.calc-verdict .calc-door {
    margin-inline-start: var(--space-4);
}

html:not(.js) .calc-door,
html:not(.js) .calc-save {
    display: none;
}

/* On a phone the Given rows come before the figure, so the first screen has
   something to type into; the drawing follows the rows it is drawn from. */
@media (max-width: 559px) {
    .calc-given.has-figure .calc-figures {
        order: 0;
        margin-block: var(--space-8) 0;
    }
}

/* On a phone the verdict's count takes its own line. */
@media (max-width: 559px) {
    /* The count and the scope link take their own line under the verdict. */
    .verdict-count {
        margin-inline-start: 0;
    }

    .verdict-count::before {
        content: "\A";
        white-space: pre;
        margin: 0;
    }
}

.report-thumb {
    display: block;
    flex: none;
}

/* The sheets at reading size: two pages side by side at every width, each a
   white page on the paper surface with a hairline, as a printed sheet. */
.report-sheets {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-4);
    margin-block: var(--space-3) var(--space-6);
}

.report-sheet {
    margin: 0;
}

.report-sheet img {
    display: block;
    width: 100%;
    height: auto;
    background: #fff;
    border: 1px solid var(--rule-strong);
}

.report-sheet figcaption {
    margin-top: var(--space-2);
    font-size: var(--text-sm);
    color: var(--ink-muted);
}

/* On a phone one sheet to a row, at the column's width, so its type can be read. */
@media (max-width: 559px) {
    .report-sheets {
        grid-template-columns: 1fr;
    }
}

.report-sheets-open > summary,
.pro-calcs-more > summary {
    cursor: pointer;
    color: var(--accent);
}

.calc-report-note,
.report-sheets-note {
    color: var(--ink-muted);
}

/* The upsell page shows the sheets at the column's full width; the calc page
   keeps them to the reading measure, since they are a preview there. */
.calc-report .report-sheets {
    grid-template-columns: 1fr;
    max-width: 52rem;
}

/* A validation row reached from its step. */
.validation-table tr:target {
    background: var(--accent-wash);
}

/* On a phone each validation row is a record: its cells stacked, each under
   its column's name, so the clause and what it was compared with are read
   whole instead of in a squeezed column. */
@media (max-width: 559px) {
    .validation-table thead {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
    }

    .validation-table,
    .validation-table tbody,
    .validation-table tr,
    .validation-table td {
        display: block;
    }

    .validation-table tr {
        padding-block: var(--space-3);
        border-bottom: 1px solid var(--rule);
    }

    .validation-table td {
        padding: var(--space-1) 0;
        border: none;
    }

    .validation-table td::before {
        content: attr(data-label);
        display: block;
        font-family: var(--font-mono);
        font-size: var(--text-xs);
        letter-spacing: 0.06em;
        text-transform: uppercase;
        color: var(--ink-muted);
    }
}

/* The sample sheet on the sign-in step: a small page, ruled like a sheet.
   Above the print block, which `test_static.py` reads to the end of the sheet. */
.sign-in-sheet img {
    display: block;
    width: 100%;
    max-width: 16rem;
    height: auto;
    border: 1px solid var(--rule-strong);
}

/* The 402 page's plans, one card each (#335). `auto-fit` stacks them on a
   phone without a breakpoint; the button sits at the foot of every card.
   Above the print block, which `test_static.py` reads to the end of the sheet. */
.plan-cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
    gap: var(--space-4);
    margin: var(--space-6) 0 var(--space-4);
    padding: 0;
    list-style: none;
}

.plan-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin: 0;
    padding: var(--space-4);
    border: 1px solid var(--rule-strong);
    border-radius: var(--radius-sm);
    background: var(--surface-raised);
}

.policy .plan-card .plan-name {
    margin: 0;
    font-size: var(--text-lg);
}

.plan-card .plan-price {
    margin: 0;
    font-size: var(--text-xl);
}

.plan-card ul {
    flex-grow: 1;
    margin: 0;
    padding-left: var(--space-4);
    font-size: var(--text-sm);
}

.plan-card .calc-export {
    align-self: flex-start;
}

@media print {
    /* Boxed in caution amber so the missing derivation is not read past as
       body text. Browsers drop backgrounds on paper by default; the border
       carries the notice alone if the wash is dropped anyway. */
    .print-only {
        display: block;
        margin: var(--space-6) 0;
        padding: var(--space-3) var(--space-4);
        border: 2px solid var(--caution);
        border-radius: var(--radius-md);
        background: var(--caution-wash);
        print-color-adjust: exact;
        -webkit-print-color-adjust: exact;
        break-inside: avoid;
    }

    /* The scope list prints open even when the reader left it closed. The
       print handler in `calcsheet.js` opens it too, for engines without
       `::details-content`. */
    .calc-scope::details-content {
        content-visibility: visible;
    }

    .calc-scope summary {
        list-style: none;
    }

    .calc-scope summary::-webkit-details-marker {
        display: none;
    }

    /* Paper cannot be hovered, and the allowed range is part of the record. */
    .given-bounds {
        opacity: 1;
    }

    /* Paper keeps the summary line and loses the pinned strip. */
    .calc-verdict {
        position: static;
        padding-block: 0;
        border-bottom: 0;
    }

    /* The derivation: on screen and on the PDF report, not on paper. */
    .calc-derivation {
        display: none;
    }

    :root {
        --surface: white;
        --surface-sunken: white;
    }

    /* No body inset: the paper margin is the browser's `@page` to set, and a
       40px inset here only narrows the page box. */
    body {
        font-size: 11pt;
    }

    /* Hide non-essential elements. The footer is not one of them: the
       disclaimer prints with the page regardless. */
    .no-print,
    .site-header,
    .calc-form button {
        display: none;
    }

    /* Paper sets its own width: the screen's page width is not protecting a
       reading line here, it would only leave part of the sheet blank beside
       the Given block. */
    .page,
    .page > header,
    .page > main,
    .page > footer {
        max-width: none;
    }

    /* Page breaks */
    h1, h2 {
        page-break-after: avoid;
    }
}

/* The policy pages: prose at the page width, sectioned like a calc sheet. */
/* Prose on the policy-frame pages keeps a reading measure; tables, the
   report sheets and the sample keep the column's width. */
.policy > p,
.policy > ul {
    max-width: 46rem;
}

.policy h2 {
    margin-top: var(--space-8);
}

.policy-meta {
    color: var(--ink-muted);
    font-size: var(--text-sm);
}

/* The 402 page's case: the project fields the reader filled, label in the
   mono register of `.site-nav`, value beside it (#114). */
.case-block {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: var(--space-1) var(--space-4);
    margin: var(--space-4) 0;
    padding-block: var(--space-2);
    border-top: 1px solid var(--ink);
    border-bottom: 1px solid var(--rule);
}

.case-block dt {
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-muted);
    align-self: baseline;
}

.case-block dd {
    margin: 0;
}

/* The checkout link and its price on one row. `.calc-actions` right-aligns
   for the calc page's title row; in prose it starts at the left margin. */
.policy .calc-actions,
.subscribe .calc-actions {
    justify-content: flex-start;
    align-items: center;
    margin: var(--space-4) 0;
}

/* The subscribe page: its own article rather than a policy, sectioned the
   same way, with the Free and Pro columns centred under their heads. */
.page-subscribe .subscribe h2 {
    margin-top: var(--space-8);
}

.plan-table th[scope="row"] {
    font-size: inherit;
    font-weight: normal;
    letter-spacing: normal;
    text-transform: none;
    color: var(--ink);
    border-bottom: 1px solid var(--rule);
}

.plan-table th:nth-child(n+2),
.plan-table td {
    text-align: center;
}

.plan-table tr:last-child th {
    border-bottom: none;
}

.plan-note {
    display: block;
    font-size: var(--text-sm);
    color: var(--ink-muted);
}

/* A yes or no as a tick or a cross, from `_mark.html`. The glyph carries it;
   the colour only helps. */
.mark {
    font-size: 1.25em;
    font-weight: bold;
    line-height: 1;
}

.mark-yes {
    color: var(--pass);
}

.mark-no {
    color: var(--fail);
}

/* The section properties page (/sections): one form, then the picked section,
   then the list. Labels in the mono register of `.site-nav`; controls
   underlined like the Given values, so the page reads as the same sheet. */
.sections-form {
    display: grid;
    gap: var(--space-3);
    margin: var(--space-6) 0;
}

.sections-filter {
    display: grid;
    gap: var(--space-3);
    margin: 0;
    padding: var(--space-3) 0 0;
    border: none;
    border-top: 1px solid var(--rule);
}

.sections-filter legend {
    padding: 0;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-muted);
}

.sections-note {
    text-transform: none;
    letter-spacing: 0;
}

.sections-field {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-2) var(--space-3);
}

.sections-field label {
    flex: 0 0 8rem;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-muted);
}

.sections-field select,
.sections-criterion input {
    min-width: 0;
    max-width: 100%;
    padding: var(--space-1) 0;
    border: none;
    border-bottom: 1px solid var(--rule-strong);
    border-radius: 0;
    background: transparent;
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    color: var(--ink);
}

.sections-field select:hover,
.sections-criterion input:hover {
    border-bottom-color: var(--accent);
}

.sections-field select:focus-visible,
.sections-criterion input:focus-visible {
    outline: none;
    border-bottom: 2px solid var(--accent);
}

/* A select is as wide as its longest option unless told otherwise, and the
   property titles are long enough to push a phone sideways. */
.sections-field select:first-of-type {
    flex: 1 1 12rem;
    width: 0;
}

@media (max-width: 560px) {
    .sections-field label {
        flex-basis: 100%;
    }
}

.sections-criterion input {
    width: 7rem;
    font-variant-numeric: tabular-nums;
}

.sections-form .sections-search {
    width: 20rem;
}

.section-detail {
    margin-bottom: var(--space-8);
    scroll-margin-top: var(--space-4);
}

.section-imperial {
    margin-left: var(--space-2);
    font-size: var(--text-base);
    font-weight: normal;
    color: var(--ink-muted);
}

/* The drawing beside the property table from 960px; stacked below that. */
.section-detail-body {
    display: grid;
    gap: var(--space-6);
}

@media (min-width: 960px) {
    .section-detail-body {
        grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
        align-items: start;
    }
}

.section-detail .data-table,
.sections-table {
    font-size: var(--text-sm);
}

.section-detail .data-table td,
.sections-table td,
.sections-table th[scope="row"] {
    padding-block: var(--space-1);
}

.section-code {
    font-family: var(--font-mono);
}

/* Descriptions are prose; the base table rule would set them in mono. */
.section-properties td:nth-child(2) {
    font-family: var(--font-body);
}

.section-properties td:last-child {
    white-space: nowrap;
}

.sections-page .num {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    text-align: right;
}

/* A wide family scrolls sideways inside its own box, and the designation
   stays put so a row never loses its name. */
.sections-scroll {
    overflow-x: auto;
}

.sections-table th[scope="row"] {
    border-bottom: 1px solid var(--rule);
    letter-spacing: 0;
    text-transform: none;
    position: sticky;
    left: 0;
    background: var(--surface);
    font-family: var(--font-mono);
    font-weight: normal;
    text-align: left;
    white-space: nowrap;
}

.sections-table th[scope="col"] {
    white-space: nowrap;
    text-transform: none;
    letter-spacing: 0;
}

.sections-table tr.is-picked th,
.sections-table tr.is-picked td {
    background: var(--accent-wash);
}

.sections-empty {
    color: var(--ink-muted);
}

/* The list folds behind the disclosure after the table. A `<details>` cannot wrap `<tr>`, so it sits beside the table and `:has()` reaches back; a browser without `:has()` shows every row, which is the page as it was. */
.sections-results:has(> .sections-more:not([open])) .sections-table tr.sections-more-row {
    display: none;
}

.sections-more summary {
    padding-block: var(--space-2);
    color: var(--ink);
    font-size: var(--text-base);
    cursor: pointer;
}

.sections-more[open] .sections-more-closed,
.sections-more:not([open]) .sections-more-open {
    display: none;
}

/* `/editions`: an edition name or NBC year breaks at its hyphen otherwise. */
.editions-table td:nth-child(n + 2):nth-child(-n + 4) {
    white-space: nowrap;
}
