/* ===========================================================================
   Hawthorne Household Care — design system.

   Derived from Concept One, the route the client's email confirms.

   FOUR RULES, in the order they matter:

   1. EVERY RULE IN THIS FILE IS INSIDE A CASCADE LAYER. An unlayered
      declaration beats every layered one regardless of specificity, so one
      stray rule outside a layer would silently outrank the whole system. The
      layer order is declared once, below, and it must stay the first thing in
      the first stylesheet the browser loads — which is why main.css is the
      first <link> in base.html and must not be reordered.

   2. Every brand value is a custom property in the :root block below. Nothing
      further down this file, and nothing in any template, contains a brand
      hex. Rebranding is that block.

   3. Gold (--gold) is legal ONLY on a dark ground. In every brand image gold
      appears on navy or dark green — apron, van, navy card, gift box — and on
      cream everything is navy ink. This is enforced structurally, not by
      comment: components never name --gold. They name --accent, which resolves
      to ink on a light ground and is remapped to gold in exactly two places,
      both of them a dark ground — the .on-dark scope, and the dark-mode :root
      where the whole page is the dark ground. Grep for "--gold": its
      definition, its hover variant, and those two remaps. Nothing else.

   4. Two dialects, because one identity cannot do both jobs. `.public` is
      airy, editorial and ornamental, laid out on a left spine with centring
      rationed to two or three moments a page. `.portal` is left-aligned,
      dense, high contrast, >=44px tap targets — it is used on a phone, in
      gloves, in variable lighting. Both work in light and dark.

   Three stylesheets, three owners:
       static/css/main.css               this file — the design system
       static/css/portal.css             @layer portal    — the three portals
       static/marketing/css/marketing.css @layer marketing — the public site
   At most two of them load on any one page.
   =========================================================================== */

@layer reset, tokens, base, layout, components, portal, marketing, utilities;

/* ===========================================================================
   TOKENS
   =========================================================================== */

@layer tokens {

    /* --- Self-hosted type -----------------------------------------------
       Variable fonts, latin subset, served from this origin. No CDN link: a
       third-party request on every page load is a GDPR question we don't
       need. See static/fonts/README.md.

       @font-face sits inside a layer deliberately. It declares no cascading
       properties, so it cannot win or lose a cascade fight; keeping it here
       means the "nothing is unlayered" rule above can be checked by eye
       rather than by exception.
       -------------------------------------------------------------------- */

    @font-face {
        font-family: 'Cormorant Garamond';
        src: url("../fonts/cormorant-garamond-latin.ec4225ec161b.woff2") format('woff2');
        font-weight: 300 700;   /* one variable file covers the whole range */
        font-style: normal;
        font-display: swap;
    }

    @font-face {
        font-family: 'Jost';
        src: url("../fonts/jost-latin.41a72a3a0ae3.woff2") format('woff2');
        font-weight: 200 700;
        font-style: normal;
        font-display: swap;
    }

    /* --- Metric-matched fallbacks ---------------------------------------
       size-adjust ONLY. The usual recipe adds ascent-override,
       descent-override and line-gap-override, but Safari has never
       implemented those three and does support size-adjust — so a fallback
       tuned for Chrome would apply the scaling in Safari and skip the
       correction, which can be worse than doing nothing. Safari is the floor
       for the field PWA and @supports cannot test @font-face descriptors, so
       there is no way to branch around it.

       The vertical half of the mismatch is neutralised instead by setting an
       explicit unitless line-height on body and on each heading tier (see the
       base layer). That is the substitute for ascent-override and it works
       everywhere.

       HOW THESE NUMBERS WERE DERIVED — do not copy a value from a blog post.
           size-adjust = avgCharWidth(webfont) / avgCharWidth(fallback)
       Measured in the browser with canvas measureText() over a 130-character
       English sample at 100px, on 2026-07-31:
           Cormorant Garamond 46.336   Georgia 51.896   Iowan Old Style 52.618
           Jost               49.314   Arial   51.889   Helvetica       51.889
       46.336 / 51.896 = 0.893 -> 89%.  49.314 / 51.889 = 0.950 -> 95%.
       Georgia and Iowan Old Style are 1.4% apart, so one value serves both.
       Times New Roman is ~6% narrower and drifts; it is third in the list and
       only reached where neither of the other two exists.
       -------------------------------------------------------------------- */

    @font-face {
        font-family: 'Cormorant Fallback';
        src: local('Iowan Old Style'), local('Georgia'), local('Times New Roman');
        size-adjust: 89%;
    }

    @font-face {
        font-family: 'Jost Fallback';
        src: local('Helvetica Neue'), local('Arial');
        size-adjust: 95%;
    }

    :root {
        /* Tells the UA that both schemes are supported, so native controls —
           <select>, date inputs, checkboxes, scrollbars, autofill — render
           dark in dark mode instead of light-on-navy. One line; it was the
           cause of a defect on every portal screen. Paired with the
           <meta name="color-scheme"> in base.html. */
        color-scheme: light dark;

        /* --- Brand palette: change these ------------------------------- */
        --navy:              #2B3547;
        --navy-soft:         #3B4655;
        --cream:             #EFEAE1;
        --sage:              #8E9C8E;
        --sage-deep:         #6E7C6C;
        --taupe:             #B7AB98;
        --greige:            #DCD7CB;
        --gold:              #C6B183;   /* DARK GROUNDS ONLY — see rule 3 */
        --gold-lift:         #D9C69C;   /* gold, one step brighter, for hover */

        /* --- Surfaces: three, and only three ---------------------------
           Measured CIE L*: paper 92.9, raised 98.0, ink 22.0. Two whispers
           (dL* 5.1) and one chasm (dL* 70.9), with nothing in the middle —
           the shape every luxury reference site has. The retired greige is
           L* 86.1, dL* 6.8 from cream, which is inside the "deliberate" band
           on lightness alone but drops saturation from 30% to 22%: it is not
           cream-made-darker, it is cream-made-greyer, and that is what read as
           muddy. Hold hue and chroma; move only lightness. Greige survives as
           --edge — a tint or a divider, never a full band. */
        --surface-paper:     var(--cream);
        --surface-raised:    #FBF9F5;
        --surface-ink:       var(--navy);
        --edge:              var(--greige);

        /* Aliases. Kept so templates and the two satellite stylesheets keep
           working through the migration. --bg-soft used to be --greige; that
           one line is what retired the cream/greige band oscillation. */
        --bg:                var(--surface-paper);
        --bg-raised:         var(--surface-raised);
        --bg-soft:           var(--surface-raised);

        --ink:               var(--navy);
        --ink-soft:          var(--navy-soft);
        /* Was #6B7382, which measures 3.98:1 on cream and fails SC 1.4.3 for
           normal text — and this token colours card copy, trust details, form
           labels and table sub-lines. Darkened on lightness alone (HSL hue
           219, saturation 9.7% both before and after): 4.60:1 on cream,
           5.25:1 on the raised surface. */
        --ink-muted:         #626977;

        /* Hairlines at 1px, navy at 18% and 38%. Decorative: a rule, a
           divider, the underline beneath a table head. */
        --rule:              rgba(43, 53, 71, 0.18);
        --rule-strong:       rgba(43, 53, 71, 0.38);
        /* The boundary of anything a person operates — an input, a secondary
           button, a checkbox, a chip. WCAG 2.2 SC 1.4.11 needs 3:1 for those
           and --rule-strong measures 2.05:1 on cream, so a hairline cannot do
           both jobs. 0.55 alpha composites to #83868C on cream: 3.05:1 on
           cream, 3.17:1 on the raised surface. Do not use it for decoration —
           at that weight it stops reading as a hairline. */
        --border-control:    rgba(43, 53, 71, 0.55);

        /* The accent on a LIGHT ground is ink. Gold does not appear here. */
        --accent:            var(--navy);
        --accent-hover:      var(--navy-soft);
        --accent-contrast:   var(--cream);

        /* Status. Muted to sit beside the palette rather than shout over it.
           Each is dark enough to pass 4.5:1 as TEXT on both light surfaces —
           the cream ground and the raised surface. Measured, not estimated:
             ok      5.12 on cream, 5.83 on raised
             warn    4.84 on cream, 5.51 on raised
             danger  5.71 on cream, 6.51 on raised
           The first two were originally lighter (#4C7A4C, #9A6B20) and failed
           on cream at 4.18 and 3.90. If you lighten any of these to taste,
           re-measure against BOTH surfaces — passing on the raised surface
           alone is the trap, because most status text sits on the cream
           ground. */
        --ok:                #436B43;
        --warn:              #8A5C18;
        --danger:            #9B3B33;

        /* --warn measures 4.44:1 against the raised surface — a hair under the
           4.5:1 WCAG needs for text this size, and over-quote hours are
           exactly the numbers a director reads closely. Mixing a little ink in
           takes it to 5.32:1 light / 7.35:1 dark without inventing a colour:
           the hue is still the brand's. The plain --warn above the mix is the
           fallback for a browser without color-mix(). Hoisted out of
           backoffice.css: it is a semantic token, and semantic tokens do not
           live in satellite files. */
        --warn-text:         var(--warn);
        --warn-text:         color-mix(in srgb, var(--warn) 82%, var(--ink));

        /* Charts. One fill per chart, taken from the brand accent: the accent
           is ink on a light ground and gold on a dark one, and both clear 3:1
           against their chart surface. Deliberately NOT a categorical palette
           — in dark mode the brand's accent, warn and danger collapse into one
           another under simulated protanopia, so no chart encodes a category
           in colour. Hoisted out of backoffice.css for the same reason as
           --warn-text. */
        --chart-series:      var(--accent);
        --chart-surface:     var(--surface-raised);
        --chart-grid:        var(--rule);
        --chart-axis:        var(--rule-strong);
        /* The quoted-hours marker. Same value as --chart-series on a light
           ground, which is exactly why the marker is drawn as wings flanking
           the column rather than as a line across it — see
           apps/backoffice/charts.py. */
        --chart-reference:   var(--ink);
        --chart-ink:         var(--ink-muted);

        /* --- Type ------------------------------------------------------- */
        --font-display: 'Cormorant Garamond', 'Cormorant Fallback',
                        'Iowan Old Style', Georgia, 'Times New Roman', serif;
        --font-sans:    'Jost', 'Jost Fallback', ui-sans-serif, system-ui,
                        -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue',
                        Arial, sans-serif;
        --font-mono:    ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas,
                        'Liberation Mono', monospace;

        /* Tracking is a function of size, INVERSELY. --track-display was
           0.15em, which is label tracking applied to display type: measured
           across eight luxury reference sites, large display serif runs
           0.00–0.03em and 0.15–0.25em is reserved for small all-caps sans
           labels. Wide-tracked Cormorant caps is the single most reliable tell
           of a template luxury site, and 0.15em is also what made a
           22-character trust label unable to fit its column. */
        --track-display: 0.01em;   /* display + h2 + h3 serif */
        --track-h1:      0.02em;
        --track-tight:   0em;      /* editorial numerals; §7.3 names it */
        --track-micro:   0.20em;   /* 10–20px all-caps sans only */
        --track-btn:     0.14em;

        /* Explicit and unitless. Half of what gets blamed on "font CLS" is
           line-height changing between the fallback and the web font because
           their internal metrics differ; pinning it removes that. */
        --leading-display: 1.08;   /* all-caps display, no descenders */
        --leading-heading: 1.2;    /* display serif, mixed case */
        --leading-body:    1.6;    /* never below 1.45 */
        --leading-tight:   1.35;

        /* --- Type scale --------------------------------------------------
           Compressed through the working range, one violent jump at the top.
           clamp() is for display tiers ONLY: body, lede, labels, form text,
           table text and every portal size are fixed rem, because browser
           page zoom does not change the CSS-pixel value of vw and a clamp
           whose preferred value is pure viewport units fails WCAG 1.4.4
           outright. Every step below anchors its preferred value in rem plus
           a vw term, and every text step has max / min <= 2 — Safari caps page
           zoom at 300% and Safari is the floor for the field PWA.
           --text-figure exceeds 2x and is exempt only because it is a
           decorative numeral, never a word a visitor must read. */
        --text-2xs:     0.6875rem;  /* 11px. Fixed: 0.20em tracking collapses below this */
        --text-xs:      0.8125rem;  /* 13px */
        --text-sm:      0.9375rem;  /* 15px */
        --text-base:    1rem;       /* 16px. Never smaller, anywhere, either dialect */
        --text-lede:    1.125rem;   /* 18px */
        --text-h3:      clamp(1.1875rem, 1.125rem + 0.3vw, 1.375rem);  /* 19 → 22 */
        --text-h2:      clamp(1.5rem,    1.3rem  + 0.9vw, 2.125rem);   /* 24 → 34 */
        --text-h1:      clamp(2rem,      1.6rem  + 1.9vw, 3.25rem);    /* 32 → 52 */
        --text-display: clamp(2.5rem,    1.8rem  + 3.4vw, 4.5rem);     /* 40 → 72 */
        --text-figure:  clamp(3.5rem,    1.5rem  + 9vw,   9rem);       /* 56 → 144 */

        /* --- Measures ----------------------------------------------------
           38ch, not the canonical 66ch. Measured on live paragraphs at the
           reference sites: Hoare's 34–39ch, Heckfield 35ch, Aman 41ch. A
           35–45ch column reads as a magazine column rather than a web
           paragraph, and you cannot write filler at 38ch — the copy
           discipline the brief demands is enforced by the measure. */
        --measure:      38ch;   /* public body copy */
        --measure-lede: 46ch;   /* the .center default; §5.7 names it */
        --measure-wide: 52ch;   /* legal prose and the portal only */
        --measure-head: 24ch;   /* headings, with text-wrap: balance */

        /* --- Metrics ------------------------------------------------------ */
        --shell-wide:   1400px;   /* back-office tables only */
        --shell-max:    1120px;
        --shell-narrow: 620px;

        /* Fluid, which replaces the old @media (max-width: 640px) override.
           One token, no breakpoint. */
        --gutter:    clamp(1.25rem, 4vw, 2.5rem);
        --header-h:  64px;
        --tap:       44px;        /* minimum tap target */
        --radius:    2px;         /* the identity does not do rounded corners */
        --radius-sm: 2px;
        --shadow-sm: none;
        --shadow-md: none;

        --spine:     minmax(0, 15rem);   /* the margin column of .spread */

        /* --- Spacing ------------------------------------------------------
           Every margin, padding and gap comes from this scale or from the
           section scale. Ad-hoc values are why the vertical rhythm was
           uneven; an exception needs a comment saying why. */
        --space-3xs: 0.25rem;   /*   4px */
        --space-2xs: 0.5rem;    /*   8px */
        --space-xs:  0.75rem;   /*  12px */
        --space-s:   1rem;      /*  16px */
        --space-m:   1.5rem;    /*  24px */
        --space-l:   2rem;      /*  32px */
        --space-xl:  3rem;      /*  48px */
        --space-2xl: 4rem;      /*  64px */
        --space-3xl: 6rem;      /*  96px */
        --space-4xl: 8rem;      /* 128px */

        --section-tight: clamp(2.5rem, 5vw,  3.5rem);
        --section:       clamp(3.5rem, 7vw,  6rem);
        --section-loose: clamp(5rem,  10vw,  8rem);
    }

    /* Dark mode: the whole page becomes a dark ground, so gold becomes the
       accent everywhere. That is the same rule, not an exception to it.

       Only the source tokens are restated. --bg, --bg-soft and --bg-raised are
       declared on :root as var(--surface-*), and a custom property resolves
       against the element it is declared on — which here is :root in both
       blocks — so the aliases follow automatically. */
    @media (prefers-color-scheme: dark) {
        :root {
            --surface-paper:   #1B2231;
            --surface-raised:  #253045;
            --edge:            var(--surface-raised);

            --ink:             #EFEAE1;
            --ink-soft:        #D8D2C6;
            --ink-muted:       #9AA2B0;   /* 6.19:1 on paper, 5.15:1 on raised */

            --rule:            rgba(239, 234, 225, 0.16);
            --rule-strong:     rgba(239, 234, 225, 0.34);
            --border-control:  rgba(239, 234, 225, 0.45);  /* 3.81:1 / 3.52:1 */

            --accent:          var(--gold);
            --accent-hover:    var(--gold-lift);
            --accent-contrast: #1B2231;

            --ok:              #8FBF8F;
            --warn:            #D9B36B;
            --danger:          #E08C84;
        }
    }
}

/* ===========================================================================
   RESET
   =========================================================================== */

@layer reset {

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

    /* Zero-specificity so a component never has to fight the reset. */
    :where(body, h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd) {
        margin: 0;
    }

    :where(img, svg, video, canvas) { max-inline-size: 100%; }
    :where(img, video) { block-size: auto; }

    :where(button, input, select, textarea) { font: inherit; color: inherit; }

    html {
        -webkit-text-size-adjust: 100%;
        text-size-adjust: 100%;
    }
}

/* ===========================================================================
   BASE — bare elements only. No component lives here.
   =========================================================================== */

@layer base {

    body {
        min-block-size: 100svh;
        display: flex;
        flex-direction: column;
        background: var(--bg);
        color: var(--ink-soft);
        font-family: var(--font-sans);
        font-size: var(--text-base);
        font-weight: 400;
        line-height: var(--leading-body);
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
    }

    main { flex: 1 0 auto; }

    a { color: var(--accent); text-decoration: none; }
    a:hover { color: var(--accent-hover); text-decoration: underline; }

    h1, h2, h3, h4 {
        margin-block-end: 0.5em;
        color: var(--ink);
        font-family: var(--font-display);
        font-weight: 500;
        line-height: var(--leading-heading);
        letter-spacing: var(--track-display);
    }
    h1 { font-size: var(--text-h1); letter-spacing: var(--track-h1); }
    h2 { font-size: var(--text-h2); }
    h3 { font-size: var(--text-h3); }
    h4 { font-size: var(--text-base); }

    p { margin-block-end: 1em; max-inline-size: var(--measure-wide); }
    p:last-child { margin-block-end: 0; }

    code, pre { font-family: var(--font-mono); font-size: 0.875em; }

    /* Wrapping. balance on headings only — browsers bail out of balancing past
       a line-count threshold (Chromium: 6), so on body copy you get no effect
       and pay the layout cost on every paragraph. pretty is a free
       enhancement; Firefox has not implemented it, which is fine because
       nothing may depend on it. Never both on one element. */
    :where(h1, h2, h3, .lede, .trust-label) { text-wrap: balance; }
    :where(p, li, dd) { text-wrap: pretty; }

    /* The header is sticky, so an anchor jump would land under it. Also WCAG
       2.2 SC 2.4.11: a focused element must not be obscured by author
       content, and an in-page link is the usual way that happens. */
    :where([id]) { scroll-margin-block-start: calc(var(--header-h) + var(--space-m)); }

    /* SC 2.4.7 Focus Visible is Level AA. The UA default outline is close to
       invisible on navy, and there was no replacement anywhere in the
       codebase.

       .on-dark deliberately asks for --accent-hover rather than naming a gold
       token: inside .on-dark, --accent IS gold, and a focus ring the same
       colour as the thing it surrounds is not a focus ring. --accent-hover
       resolves to --gold-lift there (7.34:1 on navy, well over the 3:1 SC
       1.4.11 needs) and it keeps the gold grep at its four sanctioned
       places. */
    :focus-visible {
        outline: 2px solid var(--accent);
        outline-offset: 2px;
        border-radius: var(--radius-sm);
    }
    .on-dark :focus-visible { outline-color: var(--accent-hover); }
}

/* ===========================================================================
   LAYOUT PRIMITIVES — seven, each with one job.
   =========================================================================== */

@layer layout {

    /* --- .shell — the container -----------------------------------------
       Full-bleed bands are top-level <section> elements with a .shell inside
       them. Never margin-inline: calc(50% - 50vw): 100vw includes the classic
       scrollbar on desktop Windows and Linux, which is a reliable way to
       produce horizontal overflow. There is no .bleed utility and there must
       not be one. */
    .shell {
        inline-size: 100%;
        max-inline-size: var(--shell-max);
        margin-inline: auto;
        padding-inline: var(--gutter);
    }
    .shell-narrow { max-inline-size: var(--shell-narrow); }
    .shell-wide   { max-inline-size: var(--shell-wide); }

    /* --- .stack — vertical rhythm, owned in one place ------------------ */
    .stack > * + * { margin-block-start: var(--stack-space, var(--space-m)); }

    /* --- .cluster — wrapping inline groups ----------------------------- */
    .cluster {
        display: flex;
        flex-wrap: wrap;
        gap: var(--cluster-space, var(--space-xs))
             var(--cluster-space-x, var(--space-m));
        align-items: center;
    }

    /* --- .grid — equal-weight cards -------------------------------------
       min(100%, X) is not optional: minmax(17rem, 1fr) overflows any viewport
       narrower than 17rem plus gutters, and that is the single most common
       cause of mobile horizontal overflow in grid layouts. */
    .grid {
        display: grid;
        gap: var(--grid-gap-y, var(--space-xl)) var(--grid-gap-x, var(--space-l));
        grid-template-columns:
            repeat(auto-fit, minmax(min(100%, var(--grid-min, 17rem)), 1fr));
    }

    /* --- .spread — the spine --------------------------------------------
       An asymmetric two-column grid: a narrow margin column for eyebrows,
       numerals and running labels, a wide column for content, both starting
       from one left rule that holds down the whole page. This is what the
       site was missing; everything was centred on a single axis with no other
       compositional idea in play.

       Below 60rem it collapses to one column and the margin content sits
       above its content, which is the mobile layout the site already had. */
    .spread { display: grid; gap: var(--space-s); }

    @media (min-width: 60rem) {
        .spread {
            grid-template-columns: var(--spine) minmax(0, 1fr);
            column-gap: var(--space-xl);
            align-items: start;
        }
        /* Sticky marginalia, for sections taller than about 2x the viewport.
           Opt in — on a short section it does nothing but cost a layer. */
        .spread-aside.is-sticky {
            position: sticky;
            inset-block-start: calc(var(--header-h) + var(--space-m));
        }
    }

    /* --- .sidebar — content plus a rail -------------------------------- */
    .sidebar { display: grid; gap: var(--space-xl); }

    @media (min-width: 60rem) {
        .sidebar {
            grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
            align-items: start;
        }
        .sidebar > :last-child {
            position: sticky;
            inset-block-start: calc(var(--header-h) + var(--space-m));
        }
    }

    /* --- .center — opt-in centring --------------------------------------
       `.public { text-align: center }` is deleted. Centring is now a rationed
       gesture: the hero, one mid-page statement, the closing call to action.
       With no photography to sit against, centred type has nothing anchoring
       it and collapses into a wedding invitation — which is exactly what was
       reported. */
    .center {
        text-align: center;
        margin-inline: auto;
        max-inline-size: var(--center-measure, var(--measure-lede));
    }
    .center > * { margin-inline: auto; }

    /* --- Sections and bands ---------------------------------------------
       A <section> never carries a margin. The space between two sections is
       the sum of their padding and nothing else — that is the rule that
       removes the "dead gap" symptom, where a gap was section padding plus a
       trailing <p> margin plus an ornament margin plus a page-head padding,
       four sources nobody was holding in their head at once.

       `.section + .section { border-top }` is REMOVED. A hairline between
       every pair of sections is what made the page read as a list of boxes.
       Rules are used deliberately now — above a trust item, under a page
       head, between FAQ rows — and never automatically. */
    .section        { padding-block: var(--section-tight); }
    .section-tight  { padding-block: var(--section-tight); }
    .section-loose  { padding-block: var(--section-loose); }

    /* The last child of a section has no trailing margin. Enforced once. */
    .section > .shell > :last-child { margin-block-end: 0; }

    /* Three surfaces and no more. Greige is never a section background. */
    .band-raised,
    .soft { background: var(--surface-raised); }

    /* The hairline under a page head is one of the deliberate rules §6.2
       keeps — it separates the head from the first section without a band
       change. `.public .page-head { border-bottom: 0; padding-bottom: 0 }` is
       deleted: it made a page head indistinguishable from the section after
       it, and was one of the sources of the uneven rhythm. */
    .page-head {
        padding-block: var(--section) var(--space-l);
        border-block-end: 1px solid var(--rule);
    }
    .page-head .shell { display: grid; gap: var(--space-2xs); }
    .page-head h1 { margin-block-end: 0; }
    .page-head p  { margin: 0; color: var(--ink-muted); }
}

/* ===========================================================================
   COMPONENTS
   =========================================================================== */

@layer components {

    /* --- The dark-ground scope --------------------------------------------
       The ONE place gold is switched on. Any block that puts navy behind its
       content opts in by taking .on-dark; everything inside then inherits an
       inverted set of tokens, and .btn / .card-go / links pick up gold without
       knowing anything about it. Putting gold on a cream card is therefore not
       something you can do by accident — you would have to name --gold
       yourself.

       The three --bg* aliases are restated here rather than inherited: a
       custom property whose value is a var() is resolved on the element that
       declares it, so :root's `--bg: var(--surface-paper)` computes to cream
       once and stays cream no matter what a descendant does to
       --surface-paper.

       It sits at the top of the components layer so that a later, more
       specific band rule can still override it deliberately.
       ---------------------------------------------------------------------- */

    .on-dark {
        --surface-paper:   var(--navy);
        --surface-raised:  var(--navy-soft);
        --edge:            var(--navy-soft);
        --bg:              var(--surface-paper);
        --bg-soft:         var(--surface-raised);
        --bg-raised:       var(--surface-raised);
        --ink:             var(--cream);
        --ink-soft:        rgba(239, 234, 225, 0.86);   /* 8.09:1 on navy */
        --ink-muted:       rgba(239, 234, 225, 0.62);   /* 5.01:1 on navy */
        --rule:            rgba(198, 177, 131, 0.28);
        --rule-strong:     rgba(198, 177, 131, 0.55);
        --border-control:  rgba(198, 177, 131, 0.62);   /* 3.24:1 on navy */
        --accent:          var(--gold);
        --accent-hover:    var(--gold-lift);
        --accent-contrast: var(--navy);
        background: var(--navy);
        color: var(--ink-soft);
    }

    /* --- Skip link ---------------------------------------------------------
       WCAG 2.4.1, technique G1, and the first focusable element in <body>.
       Moved out of view with a transform rather than display:none or
       visibility:hidden, both of which would take it out of the tab order and
       defeat the point.
       ---------------------------------------------------------------------- */

    .skip-link {
        position: absolute;
        inset-block-start: 0;
        inset-inline-start: var(--space-s);
        z-index: 100;
        padding: var(--space-2xs) var(--space-s);
        background: var(--surface-raised);
        color: var(--ink);
        border: 1px solid var(--border-control);
        border-block-start: 0;
        font-size: var(--text-sm);
        translate: 0 -110%;
    }
    .skip-link:focus { translate: 0 0; text-decoration: none; }

    /* --- Dialect: .public --------------------------------------------------
       Airy, editorial, ornamental, low density. Marketing pages only.
       ---------------------------------------------------------------------- */

    .public { text-align: start; }
    .public .section { padding-block: var(--section); }
    .public .section-tight { padding-block: var(--section-tight); }
    .public .section-loose { padding-block: var(--section-loose); }
    .public h1 {
        text-transform: uppercase;
        letter-spacing: var(--track-h1);
        font-weight: 400;
    }
    .public h2 {
        text-transform: uppercase;
        letter-spacing: var(--track-display);
        font-weight: 400;
    }
    /* Sentence case below h2. All-caps is a display device and it costs
       legibility; on a three-word card title it is over-signalled. */
    .public h3 { text-transform: none; }
    .public p { max-inline-size: var(--measure); }
    /* The public card is unfilled: one hairline, top only, so it reads as a
       rule in a printed catalogue rather than as a container. Its anatomy is
       .card-service below. */
    .public .card {
        border: 0;
        border-block-start: 1px solid var(--rule);
        border-radius: 0;
        box-shadow: none;
        background: none;
        padding: var(--space-l) 0 0;
    }

    /* --- Dialect: .portal --------------------------------------------------
       Left-aligned, tight, dense, high contrast. Navy is UI chrome here, not
       display type. Everything touchable is at least 44px, and nothing is
       ornamental.
       ---------------------------------------------------------------------- */

    .portal { text-align: start; }
    .portal .section { padding-block: var(--space-m); }
    .portal h1 {
        font-family: var(--font-sans);
        font-size: 1.35rem;
        font-weight: 500;
        letter-spacing: 0;
        text-transform: none;
    }
    .portal h2 {
        font-family: var(--font-sans);
        font-size: 1.05rem;
        font-weight: 500;
        letter-spacing: 0;
        text-transform: none;
    }
    .portal h3 {
        font-family: var(--font-sans);
        font-size: var(--text-base);
        letter-spacing: 0;
        text-transform: none;
    }
    .portal p { max-inline-size: var(--measure-wide); }
    .portal .btn,
    .portal button,
    .portal .tap {
        min-block-size: var(--tap);
        min-inline-size: var(--tap);
    }
    .portal .card { padding: var(--space-s); gap: var(--space-3xs); }
    .portal .page-head { padding-block: var(--space-m) var(--space-s); }

    /* --- Labels and display type ------------------------------------------ */

    /* Micro-label: light sans, all caps, wide tracking. Eyebrows, taglines,
       table headers. 0.20em is the measured band for 10–20px all-caps sans and
       it is confirmed by the research, unlike the same value on display serif.
       `.eyebrow` is the skeleton's name for the same thing, kept so existing
       templates keep working. */
    .micro-label,
    .eyebrow {
        display: block;
        font-family: var(--font-sans);
        font-size: var(--text-2xs);
        font-weight: 300;
        text-transform: uppercase;
        letter-spacing: var(--track-micro);
        line-height: var(--leading-tight);
        color: var(--ink-muted);
    }

    .display {
        font-family: var(--font-display);
        font-size: var(--text-display);
        line-height: var(--leading-display);
        letter-spacing: var(--track-display);
        text-transform: uppercase;
        font-weight: 500;
        color: var(--ink);
    }

    .lede {
        font-size: var(--text-lede);
        line-height: var(--leading-body);
        color: var(--ink-soft);
        max-inline-size: var(--measure-lede);
    }

    /* An editorial numeral in the margin column, doing the job a photograph
       would do — giving the eye an entry point. Never a word a visitor must
       read, which is what exempts it from the max/min <= 2 rule. */
    .figure {
        font-family: var(--font-display);
        font-size: var(--text-figure);
        line-height: 1;
        letter-spacing: var(--track-tight);
        color: var(--ink-muted);
    }

    /* --- Ornament ----------------------------------------------------------
       The diamond flanked by hairline rules. Markup:

           <span class="ornament" aria-hidden="true"><i class="diamond"></i></span>

       The two rules are ::before and ::after, which leaves no pseudo-element
       for the diamond — hence the inner <i>, drawn as a rotated square so it
       takes currentColor and needs no glyph. Drop the <i> and you get a plain
       hairline, which is a reasonable degradation rather than a broken one.

       THE RESTRAINT RULE, which is part of the component:

       Budget: at most TWO per public page, ZERO per portal page. Permitted
       once inside brand/_lockup_stacked.html, and once per page as the closing
       mark in the final .on-dark band or immediately above the footer — not
       both, because those are the same gesture twice in 200px.

       Forbidden: on or adjacent to any heading; inside any repeated element
       (trust items, proof points, cards, steps, FAQ rows); more than once in
       one <section>; anywhere in .portal; and as a divider floating centred
       between two sections, which is the specific pattern that reads as an
       off-the-shelf theme. Sections are divided by their surface and their
       layout.

       `margin: 1.5rem auto` was the old value, and with nine of these on the
       home page it injected 3rem of vertical space at nine positions nobody
       chose — most of the uneven-rhythm defect. It is now margin-block only;
       .center supplies the horizontal centring where centring is wanted.
       ---------------------------------------------------------------------- */

    .ornament {
        display: flex;
        align-items: center;
        gap: var(--space-2xs);
        margin-block: var(--space-l);
        max-inline-size: 18rem;
        color: var(--ink-muted);
    }
    .center .ornament,
    .lockup-stacked .ornament { margin-inline: auto; }
    .ornament::before,
    .ornament::after {
        content: "";
        flex: 1 1 auto;
        block-size: 1px;
        background: var(--rule-strong);
    }
    .ornament .diamond {
        flex: none;
        inline-size: 7px;
        block-size: 7px;
        background: currentColor;
        rotate: 45deg;
    }

    /* --- Brand lockups ---------------------------------------------------- */

    .crest { inline-size: 28px; block-size: auto; color: currentColor; flex: none; }
    .crest-sm { inline-size: 26px; }
    .crest-lg { inline-size: 84px; }

    .lockup { display: inline-flex; align-items: center; color: var(--ink); }
    .lockup-h { gap: var(--space-2xs); }
    .lockup-stacked {
        flex-direction: column;
        gap: var(--space-2xs);
        text-align: center;
    }
    .lockup-word {
        font-family: var(--font-display);
        text-transform: uppercase;
        letter-spacing: var(--track-micro);
        font-size: var(--text-base);
        font-weight: 500;
        line-height: var(--leading-display);
    }
    /* The wordmark is the one place all-caps display serif carries tracking,
       because at 16px it is a label rather than display type. The stacked form
       is larger and steps the tracking back down accordingly. */
    .lockup-stacked .lockup-word {
        font-size: var(--text-h3);
        letter-spacing: var(--track-display);
    }
    .lockup-stacked .ornament { margin-block: var(--space-3xs); max-inline-size: 12rem; }

    /* --- Header ------------------------------------------------------------ */

    .site-header {
        position: sticky;
        inset-block-start: 0;
        z-index: 50;
        background: var(--bg);
        border-block-end: 1px solid var(--rule);
    }
    .site-header-inner {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: var(--space-s);
        block-size: var(--header-h);
    }

    .site-brand { display: inline-flex; align-items: center; color: var(--ink); }
    .site-brand:hover { color: var(--ink); text-decoration: none; }

    .site-nav {
        display: flex;
        align-items: center;
        gap: var(--space-m);
        font-size: var(--text-xs);
        text-transform: uppercase;
        letter-spacing: var(--track-btn);
        font-weight: 400;
    }
    /* :not(.btn) matters. `.site-nav a` is (0,1,1) and `.btn` is (0,1,0), so
       without the exclusion an <a class="btn"> in the header loses its own
       colour to this rule and renders as plain nav text. The marketing app was
       patching that from a downstream stylesheet; fixed here instead, at the
       cause. */
    .site-nav a:not(.btn) { color: var(--ink-soft); }
    .site-nav a:not(.btn):hover { color: var(--accent); text-decoration: none; }
    .site-nav .who { color: var(--ink-muted); text-transform: none; letter-spacing: 0; }

    .logout-form { display: inline; margin: 0; }
    .logout-form button {
        background: none;
        border: 0;
        padding: 0;
        font: inherit;
        color: var(--ink-soft);
        cursor: pointer;
        text-transform: inherit;
        letter-spacing: inherit;
    }
    .logout-form button:hover { color: var(--accent); }

    /* --- Footer ------------------------------------------------------------
       Was: centred, 13px, four lines of company detail crammed into one
       paragraph inside a 2.5rem band. Now a grid, left-aligned, with each
       legal fact on its own line below a hairline that spans the full width.

       The [pending registration] placeholders stay visible. That is deliberate
       (ASSUMPTIONS A9) and it is not a defect to fix here.
       ---------------------------------------------------------------------- */

    .site-footer {
        flex: none;
        padding-block: var(--section-tight);
        font-size: var(--text-sm);
        line-height: 1.7;
    }
    .site-footer a { color: var(--accent); }

    .footer-grid {
        display: grid;
        gap: var(--space-xl);
        grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
        text-align: start;
    }
    .footer-brand .lockup-stacked { align-items: flex-start; text-align: start; }
    .footer-brand .ornament { margin-inline: 0; }
    .footer-contact p { margin: 0; }
    .footer-contact .micro-label { margin-block-end: var(--space-2xs); }

    .footer-legal {
        grid-column: 1 / -1;
        padding-block-start: var(--space-l);
        border-block-start: 1px solid var(--rule);
        font-size: var(--text-xs);
        line-height: 1.7;
        color: var(--ink-muted);
        max-inline-size: 80ch;
    }
    .footer-legal p { margin: 0; max-inline-size: none; }
    .footer-legal p + p { margin-block-start: var(--space-3xs); }

    /* --- Buttons ------------------------------------------------------------
       One filled .btn per screen — the primary action. Everything else is
       .btn-secondary or .btn-ghost.
       ---------------------------------------------------------------------- */

    .btn {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        gap: var(--space-2xs);
        /* SC 2.5.8 asks for 24x24 as AA; 44 is the client's own mobile-first
           requirement and there is no reason the public site should have
           smaller targets than the portal. Moved here from .portal. */
        min-block-size: var(--tap);
        padding-block: 0.7rem;
        padding-inline: var(--space-l);
        border-radius: var(--radius-sm);
        border: 1px solid var(--accent);
        background: var(--accent);
        color: var(--accent-contrast);
        font-family: var(--font-sans);
        font-size: 0.75rem;
        font-weight: 400;
        text-transform: uppercase;
        letter-spacing: var(--track-btn);
        line-height: 1.2;
        cursor: pointer;
        transition: background-color 120ms ease, border-color 120ms ease,
                    color 120ms ease;
    }
    .btn:hover {
        background: var(--accent-hover);
        border-color: var(--accent-hover);
        color: var(--accent-contrast);
        text-decoration: none;
    }
    .btn:disabled { opacity: 0.5; cursor: default; }

    .btn-secondary {
        background: transparent;
        color: var(--ink);
        border-color: var(--border-control);
    }
    .btn-secondary:hover {
        background: transparent;
        color: var(--accent);
        border-color: var(--accent);
    }

    /* On .on-dark, .btn-secondary's border resolves to a faint gold hairline
       that all but disappears against navy. .btn-ghost takes the accent
       itself, which on a dark ground is gold at 5.88:1. */
    .btn-ghost {
        background: transparent;
        color: var(--accent);
        border-color: var(--accent);
    }
    .btn-ghost:hover {
        background: transparent;
        color: var(--accent-hover);
        border-color: var(--accent-hover);
    }

    .btn-block { inline-size: 100%; }

    /* --- Cards -------------------------------------------------------------
       Two dialects, deliberately opposite. The portal card stays filled,
       because a dense screen needs its containers to be visible. The public
       card is stripped to a hairline and given an anatomy instead — an index
       numeral, an accent tab that extends on hover and on focus, asymmetric
       internal padding. None of it is a shadow, a gradient or a radius,
       because the identity does not do those.

       Let the public cards rag. Do not force equal heights: none of the
       reference sites stretches cards to match, and a stretched grid with a
       bottom-pinned link is exactly what makes a page read as CMS output.
       ---------------------------------------------------------------------- */

    .card {
        display: flex;
        flex-direction: column;
        gap: var(--space-2xs);
        block-size: 100%;
        padding: var(--space-m);
        background: var(--surface-raised);
        border: 1px solid var(--rule);
        border-radius: var(--radius);
    }
    .card h3 { margin: 0; }
    .card p {
        margin: 0;
        font-size: var(--text-sm);
        color: var(--ink-muted);
        flex: 1 1 auto;
        max-inline-size: none;
    }
    .card.is-restricted { background: transparent; }

    a.card-link { color: inherit; display: block; block-size: 100%; }
    a.card-link:hover { text-decoration: none; }
    a.card-link:hover .card { border-color: var(--rule-strong); }
    a.card-link:hover .card h3 { color: var(--accent); }

    /* Legacy grid names. All four are the same grid and all four become .grid
       with a --grid-min; the templates that use them belong to Phase 2, so the
       names survive one release. */
    .card-grid {
        display: grid;
        gap: var(--space-xl) var(--space-l);
        grid-template-columns:
            repeat(auto-fit, minmax(min(100%, var(--grid-min, 17rem)), 1fr));
        margin-block-start: var(--space-m);
        text-align: start;
    }

    /* The service card's anatomy. */
    .card-service {
        display: grid;
        gap: var(--space-2xs);
        align-content: start;          /* NOT stretch — let them rag */
        padding-block: var(--space-m) var(--space-xl);   /* asymmetric, print convention */
        border-block-start: 1px solid var(--rule);
        position: relative;
        background: none;
        border-inline: 0;
        border-block-end: 0;
        border-radius: 0;
        block-size: auto;
    }
    /* The accent tab: a 2rem segment of a heavier rule sitting on the
       hairline, extending to the full width of the card on hover and on
       focus. Hover is a rule changing, never a lift-and-shadow. */
    .card-service::before {
        content: "";
        position: absolute;
        inset-block-start: -1px;
        inset-inline-start: 0;
        block-size: 2px;
        inline-size: 2rem;
        background: var(--accent);
        transition: inline-size 200ms ease;
    }
    .card-link:hover .card-service::before,
    .card-link:focus-visible .card-service::before { inline-size: 100%; }

    .card-index {
        font-family: var(--font-display);
        font-size: var(--text-h2);
        line-height: 1;
        letter-spacing: var(--track-tight);
        color: var(--ink-muted);
    }
    .card-title { font-size: var(--text-h3); text-transform: none; margin: 0; }
    .card-body  { font-size: var(--text-sm); color: var(--ink-muted); margin: 0; }
    /* Sentence case, hairline underline, no arrow-plus-caps-plus-tracking.
       `.go` is the older name for the same thing. */
    .card-go,
    .card .go {
        font-size: var(--text-2xs);
        text-transform: uppercase;
        letter-spacing: var(--track-micro);
        color: var(--accent);
        padding-block-start: var(--space-xs);
    }
    .card .go::after { content: " →"; letter-spacing: 0; }

    .empty {
        margin-block-start: var(--space-s);
        padding: var(--space-l);
        border: 1px solid var(--rule);
        color: var(--ink-muted);
        font-size: var(--text-sm);
    }

    /* --- Data table --------------------------------------------------------
       The primitive the ornamental dialect would fight. Portal dialect only —
       a table on a public page is a design error, with the legal pages'
       retention schedule as the one sanctioned exception.
       ---------------------------------------------------------------------- */

    .data-table {
        inline-size: 100%;
        border-collapse: collapse;
        font-size: var(--text-sm);
    }
    .data-table th {
        text-align: start;
        font-family: var(--font-sans);
        font-size: var(--text-2xs);
        font-weight: 400;
        text-transform: uppercase;
        letter-spacing: var(--track-micro);
        color: var(--ink-muted);
        padding: var(--space-2xs) var(--space-xs);
        border-block-end: 1px solid var(--rule-strong);
        white-space: nowrap;
    }
    .data-table td {
        padding: 0.7rem var(--space-xs);
        border-block-end: 1px solid var(--rule);
        color: var(--ink);
    }
    .data-table tr:last-child td { border-block-end: 0; }
    .data-table td .sub {
        display: block;
        font-size: var(--text-xs);
        color: var(--ink-muted);
    }
    /* A horizontally scrolling table must be reachable by keyboard (SC
       2.1.1), which is why every .table-scroll carries tabindex="0" and
       role="region" with an aria-label naming the table. */
    .table-scroll { overflow-x: auto; }

    /* --- Notice ------------------------------------------------------------
       One .notice. It was defined twice, differently, in marketing.css (left
       border variants, no ground) and fieldwork.css (a filled block with a
       <strong> lead). They never collided at runtime because they never
       loaded together, which is exactly what made it dangerous — the conflict
       was latent and would have landed the first time someone reused a
       template.

       .notice-error and .notice-bad are the old names for .notice-danger and
       are kept as aliases for one release.
       ---------------------------------------------------------------------- */

    .notice {
        margin-block: 0 var(--space-m);
        padding: var(--space-xs) var(--space-s);
        border: 1px solid var(--rule-strong);
        border-inline-start-width: 3px;
        background: var(--surface-raised);
        font-size: var(--text-sm);
        text-align: start;
    }
    .notice strong { display: block; color: var(--ink); margin-block-end: var(--space-3xs); }
    .notice p { margin-block-end: var(--space-2xs); max-inline-size: none; }
    .notice p:last-child { margin-block-end: 0; }
    .notice ul {
        margin-block: 0 var(--space-2xs);
        padding-inline-start: var(--space-s);
        font-size: var(--text-sm);
    }
    .notice-ok     { border-inline-start-color: var(--ok); }
    .notice-warn   { border-inline-start-color: var(--warn); }
    .notice-draft  { border-inline-start-color: var(--warn); }
    .notice-danger,
    .notice-error,
    .notice-bad    { border-inline-start-color: var(--danger); }

    .fine-print {
        margin-block-start: var(--space-s);
        font-size: var(--text-xs);
        line-height: 1.6;
        color: var(--ink-muted);
    }

    /* --- Pill --------------------------------------------------------------
       Also defined twice, differently, in fieldwork.css and backoffice.css.
       One definition, four state modifiers. `.pill-live` is fieldwork's old
       name for `.is-live`.
       ---------------------------------------------------------------------- */

    .pill {
        display: inline-block;
        padding: 0.15rem var(--space-2xs);
        border: 1px solid var(--rule-strong);
        border-radius: var(--radius-sm);
        font-size: var(--text-2xs);
        text-transform: uppercase;
        letter-spacing: var(--track-micro);
        line-height: var(--leading-tight);
        color: var(--ink);
        white-space: nowrap;
    }
    .pill.is-good  { border-color: var(--ok);     color: var(--ok); }
    .pill.is-warn  { border-color: var(--warn);   color: var(--warn-text); }
    .pill.is-alert { border-color: var(--danger); color: var(--danger); }
    .pill.is-live,
    .pill-live     { border-color: var(--ok);     color: var(--ok); }

    /* --- Forms -------------------------------------------------------------
       One set. .field, .field-check and .field-radio were defined in three
       files; error text had four names. Per DESIGN-SYSTEM.md §7.4 and §10.4.
       ---------------------------------------------------------------------- */

    .field { margin-block-end: var(--space-m); }

    .field > label,
    .field > legend,
    .auth-field label {
        display: block;
        margin-block-end: var(--space-3xs);
        padding: 0;
        font-size: var(--text-2xs);
        text-transform: uppercase;
        letter-spacing: var(--track-micro);
        color: var(--ink-muted);
    }
    .field .req { color: var(--accent); }

    .auth-field input,
    .field input,
    .field select,
    .field textarea {
        inline-size: 100%;
        min-block-size: var(--tap);
        padding-block: 0.6rem;
        padding-inline: var(--space-xs);
        font: inherit;
        /* 16px exactly. Below this iOS Safari zooms the viewport on focus,
           which on the primary conversion form is a real cost. */
        font-size: var(--text-base);
        color: var(--ink);
        background: var(--surface-raised);
        border: 1px solid var(--border-control);
        border-radius: var(--radius-sm);
    }
    .field :is(input, select, textarea):focus-visible,
    .auth-field input:focus-visible {
        outline: 2px solid var(--accent);
        outline-offset: 1px;
        border-color: var(--accent);
    }
    /* :user-invalid, not :invalid — :invalid paints an empty required field
       red before the visitor has typed anything. */
    .field :is(input, textarea, select):user-invalid { border-color: var(--danger); }

    .field-help,
    .field .help,
    .field .helptext,
    .auth-help-text,
    .privacy-note,
    .form-foot {
        display: block;
        margin-block-start: var(--space-3xs);
        font-size: var(--text-xs);
        color: var(--ink-muted);
    }

    /* One error style. ul.errorlist is Django's own and is mapped onto it.
       Error text carries a marker as well as the colour — never colour alone
       (SC 1.4.1). */
    .field-error,
    .auth-field-error,
    .field .errors,
    ul.errorlist {
        display: block;
        margin-block: var(--space-3xs) 0;
        padding: 0;
        list-style: none;
        font-size: var(--text-xs);
        color: var(--danger);
    }
    .field.has-error input,
    .field.has-error select,
    .field.has-error textarea { border-color: var(--danger); }

    /* Django 5 renders RadioSelect as nested <div>s — it stopped using
       <ul>/<li> in 4.0 — so `.field-radio ul` and `.field-radio li` match
       nothing. Targeting the real markup: `.field-radio > div` is the widget's
       container and each child div is one option. Get this wrong and the
       option labels silently inherit the 11px uppercase micro-label style from
       `.field > label` above, with no tap target, on the required radio group
       of the primary conversion form. */
    .field-radio { border: 0; margin-block: 0 var(--space-m); padding: 0; min-inline-size: 0; }
    .field-radio > div {
        display: flex;
        flex-wrap: wrap;
        gap: var(--space-3xs) var(--space-m);
    }
    .field-radio > div > div { flex: none; }
    .field-radio label,
    .field-check label {
        display: flex;
        align-items: center;
        gap: var(--space-2xs);
        min-block-size: var(--tap);
        margin: 0;
        text-transform: none;
        letter-spacing: 0;
        font-size: var(--text-sm);
        color: var(--ink-soft);
    }
    .field-check { display: flex; align-items: flex-start; gap: var(--space-2xs); }
    .field-radio input[type="radio"],
    .field-check input[type="checkbox"] {
        inline-size: 1.15rem;
        block-size: 1.15rem;
        min-block-size: 0;
        /* Needs color-scheme set or the tick renders wrong in dark mode. */
        accent-color: var(--accent);
        flex: none;
    }
    .field-check label { align-items: flex-start; }

    /* field-sizing is the newest thing in this file and the least supported.
       Behind @supports, and always with a min/max block size, or the field
       collapses to nothing when empty. */
    @supports (field-sizing: content) {
        .field textarea {
            field-sizing: content;
            min-block-size: 6rem;
            max-block-size: 22rem;
        }
    }

    /* The honeypot. display:none, not off-screen. The off-screen version is
       sometimes recommended on the theory that bots skip hidden fields, but an
       off-screen input is still an input that browser autofill will populate —
       and a honeypot filled in by the visitor's own password manager refuses a
       real enquiry, repeatably. The field's name does the work of not looking
       fillable; see the docstring in apps/marketing/forms.py. */
    .hp-wrap { display: none; }

    /* --- Auth pages --------------------------------------------------------
       Neither dialect. The navigator and the auth pages are a lobby and a
       door: no data, no ornament, and forcing either dialect on them makes
       them look like something they are not.
       ---------------------------------------------------------------------- */

    .auth-page {
        display: flex;
        align-items: center;
        justify-content: center;
        padding: var(--space-2xl) var(--gutter);
    }

    .auth-card {
        inline-size: 100%;
        max-inline-size: 420px;
        padding: var(--space-l);
        background: var(--surface-raised);
        border: 1px solid var(--rule);
        border-radius: var(--radius);
        text-align: start;
    }
    .auth-card h1 {
        font-size: var(--text-h3);
        letter-spacing: var(--track-display);
        margin-block-end: var(--space-3xs);
    }
    .auth-card-head { margin-block-end: var(--space-m); }
    .auth-card-head p { color: var(--ink-muted); font-size: var(--text-sm); margin: 0; }

    .auth-field { margin-block-end: var(--space-s); }

    .auth-error {
        margin-block: 0 var(--space-s);
        padding: var(--space-2xs) var(--space-xs);
        border: 1px solid var(--danger);
        border-radius: var(--radius-sm);
        font-size: var(--text-sm);
        color: var(--danger);
    }
    .auth-error p { margin: 0; max-inline-size: none; }

    .auth-submit { margin-block-start: var(--space-xs); }
    .auth-foot {
        margin-block-start: var(--space-m);
        font-size: var(--text-xs);
        color: var(--ink-muted);
    }

    /* --- PWA chrome --------------------------------------------------------
       The offline banner and the iOS install prompt. Both start hidden and are
       revealed by static/js/pwa.js; an assistant has to be able to tell that a
       tick has not reached the server yet.
       ---------------------------------------------------------------------- */

    .offline-banner,
    .install-prompt {
        display: none;
        padding: var(--space-2xs) var(--gutter);
        font-size: var(--text-xs);
    }
    .offline-banner[data-visible="true"],
    .install-prompt[data-visible="true"] { display: block; }

    /* Navy on the warn amber measures 6.4:1 — this is the one banner a person
       has to read in sunlight, and it must not be quiet. */
    .offline-banner {
        background: var(--warn);
        color: var(--navy);
    }
    .offline-banner .queued { font-weight: 500; }

    .install-prompt {
        border-block-end: 1px solid var(--rule);
        color: var(--ink-soft);
    }
    .install-prompt button {
        font: inherit;
        background: none;
        border: 0;
        color: var(--accent);
        cursor: pointer;
        text-decoration: underline;
        min-block-size: var(--tap);
    }

    /* --- Small screens -----------------------------------------------------
       The --gutter override that used to live here is gone: --gutter is fluid
       now, one token and no breakpoint. What is left is the header, which
       genuinely has a different shape on a phone.
       ---------------------------------------------------------------------- */

    @media (max-width: 40rem) {
        .site-nav { gap: var(--space-2xs); font-size: 0.75rem; }
        .site-nav .who { display: none; }
    }
}

/* ===========================================================================
   UTILITIES — last, so a utility wins without !important.
   =========================================================================== */

@layer utilities {

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

    /* Tabular figures belong in a column, not in running text: at body size
       equal-width digits make a number like 121 read loose. */
    .num,
    .u-num { font-variant-numeric: tabular-nums; }

    .u-measure { max-inline-size: var(--measure); }
    .u-measure-wide { max-inline-size: var(--measure-wide); }

    .visually-hidden {
        position: absolute;
        inline-size: 1px;
        block-size: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
        border: 0;
    }

    /* --- Paper grain -------------------------------------------------------
       A near-invisible grain is what separates flat colour from stock. It is
       an SVG MASK rather than a coloured background image, so the colour still
       comes from a token and no hex leaves the :root block.

       Hard limits, and they are limits rather than suggestions: opacity <=
       0.04 (if it reads as texture rather than as depth it is wrong), at most
       two grained elements per page, pointer-events none, contain paint.
       Behind @supports — without mask support there is simply no texture,
       which is a perfectly good page. QA has explicit authority to delete this
       outright if it costs paint time or moves INP; it is the least important
       idea in the specification and the first thing to cut.
       ---------------------------------------------------------------------- */

    @supports (mask-image: url("../img/grain.2c35b7a16716.svg")) {
        .grain { position: relative; }
        .grain > * { position: relative; z-index: 1; }
        .grain::after {
            content: "";
            position: absolute;
            inset: 0;
            z-index: 0;
            pointer-events: none;
            contain: paint;
            background: var(--ink);
            mask-image: url("../img/grain.2c35b7a16716.svg");
            mask-size: 180px 180px;
            mask-repeat: repeat;
            opacity: 0.03;
        }
    }
}

/* ===========================================================================
   Reduced motion.

   Reopened in the FIRST layer on purpose. !important reverses layer order — an
   important declaration in the earliest layer beats an important declaration in
   every later one — so putting this block in `reset` makes it the most powerful
   thing on the page, which is exactly what it needs to be. It is also the only
   !important anywhere in the three stylesheets, and it stays that way.
   =========================================================================== */

@layer reset {
    @media (prefers-reduced-motion: reduce) {
        *, *::before, *::after {
            transition: none !important;
            animation: none !important;
            scroll-behavior: auto !important;
        }
    }
}
