/* ============================================================================
   AUTH SCREENS — sign-in, forgot password, change password, unlock (docs/16 §6).
   Token-based; the CI guard (build/check-no-raw-hex.ps1) forbids a raw hex here.

   Split screen: product panel on the LEFT, form on the RIGHT, divided by a wave
   rather than a straight seam. This is §6's own orientation ("left 40% brand
   panel, right 60% form"); the inversion this file used to carry, and the note
   admitting it disagreed with §6, are both gone.

   All four screens share it. The layout is one component
   (Components/Account/AuthShell.razor) and everything below is one `auth-*`
   vocabulary, so a fix lands on all four at once.

   Here rather than in scoped .razor.css files because docs/16 §10.3 records
   what scoped CSS cost this project once already — `.empty` lived in
   Dashboard.razor.css while two other screens used the class and silently
   rendered unstyled. Every rule is prefixed `auth-`, `form-`, `field` or
   `policy-`, so nothing else picks them up, but they sit where the hex guard
   and the next reader can both see them. It also keeps styling off
   `<InputText>`'s own markup, which a scoped sheet cannot reach without
   `::deep`.

   THE PANEL IS THE ONLY DECORATED SURFACE. The form side stays plain: no card
   border, no panel, no rule, no texture. Everything box-shaped on the right-hand
   column is a field or the commit button, and nothing there is box-shaped purely
   to look like something. The visual weight of the screen is all in one place on
   purpose, which is what makes the form read as the thing to do.

   ---------------------------------------------------------------------------
   NOTE ON COLOUR — READ BEFORE RE-THEMING THE PANEL.

   The right-hand column is ordinary themed chrome: --c-text and --c-text-muted
   on --c-bg, which the theme contrast gate already holds at 4.5:1 and 3:1
   against every surface on every preset.

   The panel is not, and cannot be. It carries light text over a photograph
   nobody has seen yet, so its contrast cannot come from the palette — it comes
   from `.auth-hero-scrim`, a FIXED dark gradient, with --c-hero-ink on top.
   Both halves of that pair are deliberately theme-independent (tokens.css says
   so at the token, in the same terms as --c-chart-ink).

   This is the second attempt at this screen. The first painted the panel
   --c-primary and its text --c-primary-ink, which measured 3.47:1 in Night
   Shift and 4.21:1 in Ledger dark — both under §8's 4.5:1 floor for body text,
   so it failed WCAG AA on two of the four shipped presets, and it was removed.
   What makes the photograph safe where the flat brand colour was not is exactly
   that the scrim does not move: the ratio is a property of this stylesheet
   rather than of whichever theme is loaded.

   The theme reaches the panel through `.auth-hero-tint` — --c-primary multiplied
   over the photograph at full strength. That layer sits UNDER the scrim and only
   ever darkens, so it can tint the picture without being able to cost the text a
   ratio. Keep that order.

   WHY --c-primary AND NOT --c-rail-bg. The rail colour was tried here first, on
   the reasoning that the sign-in panel and the navigation rail are the same
   left-hand column and should not change colour as a user crosses the login form.
   That reasoning is sound and it is a pity to lose, but the TOKEN cannot carry it:
   --c-rail-bg is audited against --c-text at 4.5:1, because the rail carries nav
   labels. A saturated brand colour there fails the gate and cannot be published at
   all on a light theme — a mid-blue rail measures 2.59:1 against Ledger's text.

   --c-primary is audited on entirely different terms (ContrastChecker.Audit):
   --c-primary-ink ON it at 4.5:1, and it against --c-bg at 3:1. Nothing requires
   --c-text to be legible on it, because nothing sets --c-text on it. That is
   exactly the shape of surface this panel is, which is why a site can set its
   brand colour here and publish.

   The consequence to know: --c-primary also inks the field focus ring and the
   reset-flow step markers, so setting it repaints those too. That is the price of
   using the one token whose contract fits this panel.
   ============================================================================ */

/* ---- Shell ----------------------------------------------------------------
   Two columns, one row, and a real seam between them for the first time — the
   panel is a photograph, so the "one unbroken --c-bg surface" this file used to
   insist on is no longer available or wanted.

   The seam is a curve, and it belongs to the PANEL: `.auth-hero` is cut to the
   designer's shape with `clip-path`, and what shows beside the cut is simply the
   page. Nothing is painted over the edge, so there is no second curve to keep in
   agreement with the first. The shape itself lives in AuthShell.razor. */

.auth-shell {
    display: grid;
    /* Fractions rather than percentages so the columns divide the padding cleanly.

       THE PANEL IS THE WIDER HALF, which inverts §6's original 40/60 rather than merely nudging it.
       Two reasons, and the second is the one that decides it. The wave bites up to 130px out of the
       panel's own right edge, so its NARROWEST point is a good deal less than its column; splitting
       evenly left the picture reading as the smaller half even though the numbers said otherwise.
       And the form does not want the space — it is a 440px column of two fields, and every pixel
       past that is margin. Giving the surplus to the photograph is the only place it does any work. */
    grid-template-columns: 55fr 45fr;
    grid-template-rows: 1fr;
    /* min-height, not height: §2 exempts the auth screens from the fixed viewport on purpose —
       they are short, and on a laptop with a 600px viewport this must still scroll normally. */
    min-height: 100vh;
    background: var(--c-bg);
    color: var(--c-text);
    font-family: var(--font-body);
}

/* ---- Form column ----------------------------------------------------------
   SECOND ON SCREEN, FIRST IN THE DOM. `grid-column: 2` is what moves it right;
   the markup order is unchanged and deliberate. Nothing in the panel is
   focusable, so the two orders cannot disagree about where the keyboard goes —
   the username field is the first stop either way. What the DOM order still
   decides is what a screen reader meets first, and on a sign-in screen that
   should be the form rather than the marketing. See AuthShell.razor. */

/* CENTRED, so a taller form grows into BOTH margins at once. This column was top-anchored at a fixed
   inset for a while, to stop /change-password's heading riding up the page relative to /login's.
   That held the top edge still, but it spent the whole margin on one side: the tallest screen ran
   out of viewport at the bottom and put a scrollbar on the page. Centred, /change-password's extra
   ~200px is split into ~100px each way and the same screen fits without scrolling.

   `margin: auto 0` on the child rather than `align-items: center` here. They centre identically
   while there is free space, but auto margins never resolve negative — so when a form IS taller than
   the column (a long error plus the forced-change note on a short laptop), it lands at the top edge
   and stays scrollable. `align-items: center` would centre the overflow too and put the top of the
   form above the scroll origin, where it cannot be reached at all. */
.auth-side {
    grid-column: 2;
    grid-row: 1;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding: var(--sp-8) var(--sp-6);
}

/* The column: form, then the copyright line beneath it.

   ONE WIDTH FOR ALL FOUR SCREENS. There used to be a `.auth-side-wide` modifier here, set only by
   /change-password, because its policy checklist wraps mid-sentence in a narrower column. That made
   the form visibly change size as you moved between screens that are otherwise the same panel.

   440 IS NOT A REGRESSION FROM THE 480 THIS USED TO BE. The floor is set by that checklist, and the
   checklist got wider, not narrower: the bordered card around it is gone, so its 48px of side
   padding came back to the content. 440 − 24 of checklist padding − 8 of column gap leaves ~204 per
   column against the ~194 the longest rule ("A special character ($@!%*?&)") measures at 0.75rem —
   marginally more room than the old 480 left. Anyone narrowing this FURTHER should expect that rule
   to wrap, and should take `.policy-list` to one column in the same commit. */
.auth-side-inner {
    display: flex;
    flex-direction: column;
    width: 100%;
    max-width: 440px;
    margin: auto 0;   /* vertical centring — see `.auth-side` for why it is a margin and not align-items */
}

/* NOT A CARD ANY MORE, and the name is kept only because four screens and their forms address it.
   No border, no background, no shadow: the form sits directly on the page.

   That is what lets the fields read as raised without a second trick — `.field input` is
   --c-surface-raised against the page's --c-bg, one step forward, with nothing in between competing.
   The old bordered panel had to be --c-surface for exactly that reason, and spent 48px of width on
   padding to hold a box the screen did not need. */
.auth-card {
    display: flex;
    flex-direction: column;
}

.auth-card form {
    display: flex;
    flex-direction: column;
    gap: var(--sp-4);
}

/* Below the form, on the page itself — a footer for the screen, not a line of the form. */
.auth-copyright {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-1);
    margin: var(--sp-8) 0 0;
    font-size: 0.6875rem;
    color: var(--c-text-muted);
}

.auth-copyright .icon { flex: none; }

/* ---- Product panel --------------------------------------------------------
   FIRST ON SCREEN, SECOND IN THE DOM (see the form column above). Four stacked
   layers, and the order is load-bearing:

     1. `.auth-hero-photo`  the site's own photograph, or
        `.auth-hero-drawn`  the recorder illustration when none is uploaded
     2. `.auth-hero-tint`   the rail colour multiplied over it — the theme's one
                            reach into this panel, and it can only darken
     3. `.auth-hero-scrim`  the fixed dark gradient that holds the contrast
     4. `.auth-hero-inner`  the mark and the headline, in --c-hero-ink

   Nothing in here is focusable. */

.auth-hero {
    grid-column: 1;
    grid-row: 1;
    position: relative;
    /* The photo is sized to cover; it must not bleed past the curve into the form column. */
    overflow: hidden;
    /* THE CURVE. The panel is cut to the designer's own shape rather than having a wave painted over
       its edge — see AuthShell.razor for the shape and for why the overlay it replaces could not be
       made correct. Undone below the stacking breakpoint, where the panel is a band and this edge is
       no longer the one between the columns. */
    clip-path: url(#auth-panel-shape);
    display: flex;
    /* Contains `.auth-hero-tint`'s multiply so it blends with the photograph beneath it and not with
       the page behind the panel. Without this the blend group is the root stacking context and the
       tint darkens the form column's background too, on the browsers that get it right. */
    isolation: isolate;
    /* The ground beneath everything: what shows while the photograph is still arriving, and what
       the illustration branch is drawn on. See the tint below for why this is --c-primary and not
       --c-rail-bg, which is where it briefly was. */
    background: var(--c-primary);
}

/* `cover`, so the photograph fills a column of any proportion without distorting; `center` so what
   gets cropped is taken evenly off both sides rather than off one. Absolute rather than in flow so
   the three layers above it stack in the same box without any of them needing a height. */
.auth-hero-photo {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center;
    /* DESATURATED ON PURPOSE, and it is half of the duotone. The tint below multiplies one hue over
       this; leaving the photograph's own colour in would fight that hue wherever the picture is warm,
       and a stainless chamber under sodium light is very warm. Greyscale first means the panel is one
       colour with the photograph's LUMINANCE in it, which is what the design is.
       The contrast lift is small and only sharpens the mid-tones the tint then colours. */
    filter: grayscale(1) contrast(1.06);
}

/* The other half of the duotone: the site's own primary at FULL strength, multiplied over the
   greyscale photograph. Together they map the picture's luminance onto one hue, which is the flat
   bright panel the design asks for rather than a photograph with a wash over it.

   ---------------------------------------------------------------------------------------------
   WHY --c-primary AND NOT --c-rail-bg. This was the rail colour for one revision, on the reasoning
   that the sign-in panel and the navigation rail are the same left-hand column and should not change
   colour as you cross the login form. That reasoning still holds and it is a pity to lose, but the
   token cannot carry it: --c-rail-bg is audited against --c-text at 4.5:1, because the rail carries
   nav labels, so a saturated brand colour there fails the gate and cannot be published at all on a
   light theme. A mid-blue rail measures 2.59:1 against Ledger's text.

   --c-primary is audited on different terms entirely (ContrastChecker.Audit): --c-primary-ink ON it
   at 4.5:1, and it against --c-bg at 3:1. Nothing requires --c-text to be readable on it, because
   nothing sets --c-text on it. That is exactly the shape of surface this panel is, so a site can set
   its brand blue here and publish.
   ---------------------------------------------------------------------------------------------

   FULL OPACITY IS A CONTRAST GUARANTEE, not a style preference. Multiply cannot lighten, so at
   opacity 1 the lightest pixel this panel can produce is --c-primary itself: photo x primary peaks
   exactly at primary, where the photograph is white. That collapses "how bright might an unseen
   photograph be under the headline" into "what is --c-primary", which is a value the theme system
   already knows. The scrim below is then sized against the presets' primaries rather than against an
   unbounded image, which is why it can be as light as it now is. Do not reduce this below 1 without
   putting the scrim back up. */
.auth-hero-tint {
    position: absolute;
    inset: 0;
    background: var(--c-primary);
    mix-blend-mode: multiply;
    opacity: 1;
    pointer-events: none;
}

/* High Contrast maps --c-primary to near-black, and multiplying that over a photograph erases it.
   The preset exists to strip decoration rather than to black it out, so the panel drops the picture
   and stands as flat colour with the mark and headline on it. */
:root[data-theme="high-contrast"] .auth-hero-photo,
:root[data-theme="high-contrast"] .auth-hero-drawn { display: none; }

/* THE CONTRAST FLOOR — and it is now a light one, which is the whole point of the duotone above.

   It used to run to 0.90 black at the foot, because it had to defend the headline against an
   arbitrary photograph. It no longer does: full-opacity multiply caps the panel at --c-primary, so
   this only has to close the gap between the PALEST preset primary and 4.5:1 — a pale primary is the
   one that leaves white ink the least room, so it is the one that binds. Measured against every
   shipped preset with both inks, Ledger dark binds at alpha 0.22; Night Shift needs 0.10, a mid
   brand blue 0.11, and the two light presets need none at all. The 0.30 stop at the
   foot clears the worst of them with margin.

   Weighted to the bottom because that is where the words are — the mark at the top rides its own
   plate and takes nothing from this. The near-zero stops across the upper half are what let the
   picture read as a bright flat panel rather than something with a shadow pulled over it.

   Two things to keep in step if you touch either: this gradient and the tint's opacity. Lower the
   tint below 1 and the panel is no longer capped at --c-primary, and these stops stop being enough. */
.auth-hero-scrim {
    position: absolute;
    inset: 0;
    background: linear-gradient(
        to bottom,
        rgb(0 0 0 / 0.04) 0%,
        rgb(0 0 0 / 0.00) 30%,
        rgb(0 0 0 / 0.16) 66%,
        rgb(0 0 0 / 0.30) 100%);
    pointer-events: none;
}

/* ---- The panel with no photograph uploaded --------------------------------
   A themed wash and the recorder trace, so a fresh install has a finished screen
   before an administrator has been anywhere near Masters → Company. Sits in the
   photograph's own layer, under the same tint and the same scrim, so the two
   branches are lit identically and the text keeps its ratio either way. */

.auth-hero-drawn {
    position: absolute;
    inset: 0;
    overflow: hidden;
}

/* Three soft radial washes under a heavy blur. Drawn from the chart-trace tokens rather than one
   brand shade, so the blob carries the product's own palette and re-inks for every theme with no
   image asset to ship. */
.auth-hero-glow {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 150%;
    aspect-ratio: 1.25;
    transform: translate(-50%, -50%);
    background-image:
        radial-gradient(closest-side, var(--c-chart-trace-3), transparent),
        radial-gradient(closest-side, var(--c-chart-trace-4), transparent),
        radial-gradient(closest-side, var(--c-chart-trace-5), transparent);
    background-position: 20% 28%, 82% 40%, 54% 88%;
    background-size: 62% 68%, 58% 64%, 66% 60%;
    background-repeat: no-repeat;
    filter: blur(64px);
    opacity: 0.75;
    pointer-events: none;
}

/* The trace itself, scaled to fill the panel. `slice` rather than the default `meet`: this is a
   backdrop now rather than a framed picture, so it should crop at the edges like the photograph it
   stands in for instead of letterboxing inside the column. */
.auth-hero-trace {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0.85;
}

/* The acceptance band edges. --c-chart-grid rather than --s-normal / --s-alarm: Layer-3 status
   colours mean "a reading is inside or outside its band", which is a claim a drawing must not
   make. */
.auth-trace-limit {
    stroke: var(--c-chart-grid);
    stroke-width: 1.5;
    stroke-dasharray: 6 6;
    opacity: 0.55;
}

.auth-trace-pen {
    fill: none;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.auth-trace-pen-a { stroke: var(--c-chart-trace-1); stroke-width: 3; }
.auth-trace-pen-b { stroke: var(--c-chart-trace-2); stroke-width: 2; opacity: 0.6; }

/* The blurred copy of the pen, painted underneath it — what makes the trace glow rather than sit
   flat on the wash. */
.auth-trace-halo { stroke: var(--c-chart-trace-1); stroke-width: 7; opacity: 0.35; }

/* The pen tip, parked where the paper is still advancing, inside a soft ring. */
.auth-trace-pen-dot  { fill: var(--c-chart-trace-1); }
.auth-trace-pen-halo { fill: var(--c-chart-trace-1); opacity: 0.18; }

/* The pens draw themselves once on load, at the §7 page tempo. Purely ornamental, so it is behind a
   reduced-motion query and the illustration is complete without it. The dash length just needs to
   exceed the path; 900 comfortably covers a 640-unit box of wander. */
@media (prefers-reduced-motion: no-preference) {
    .auth-trace-pen {
        stroke-dasharray: 900;
        stroke-dashoffset: 900;
        animation: auth-trace-draw 1600ms var(--motion-ease) forwards;
    }

    .auth-trace-pen-b { animation-delay: 140ms; }

    .auth-trace-pen-dot,
    .auth-trace-pen-halo {
        opacity: 0;
        animation: auth-trace-settle var(--motion-page) var(--motion-ease) 1450ms forwards;
    }

    /* The one that does not settle to opacity 1 needs its own landing value. */
    .auth-trace-pen-halo { animation-name: auth-trace-settle-halo; }
}

@keyframes auth-trace-draw        { to { stroke-dashoffset: 0; } }
@keyframes auth-trace-settle      { to { opacity: 1; } }
@keyframes auth-trace-settle-halo { to { opacity: 0.18; } }

/* ---- The shape definition -------------------------------------------------
   Paints nothing; it carries the clipPath `.auth-hero` is cut to. Taken out of
   flow entirely so it can never occupy a grid cell or add a scrollbar. */
.auth-defs {
    position: absolute;
    width: 0;
    height: 0;
    overflow: hidden;
}

/* ---- Panel contents -------------------------------------------------------
   Mark at the top, headline at the foot, the slack between them. */

.auth-hero-inner {
    position: relative;
    z-index: 1;
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: var(--sp-8);
    padding: clamp(28px, 4vw, 52px);
    /* Clears the wave, which is up to 104px of curve biting into this edge. */
    /* The curve's narrowest point is 81.9% of the panel width, at the very foot. A percentage
       rather than a clamp so this tracks the shape instead of the viewport: the copy stays clear
       of the cut at every width, which a pixel value cannot promise on a shape that scales. */
    padding-right: 24%;
}

/* The mark, top-left. */
.auth-hero-mark {
    display: flex;
    align-items: flex-start;
}

/* THE PLATE IS NOT DECORATION — see AuthShell.razor. An uploaded logo is an unknown image over a
   photograph, and most are dark ink drawn for a white page; on the scrim they would disappear.
   This puts a light frosted chip behind whatever arrives, so a logo reads whichever way round it
   was drawn. The vendor lockup below needs none, because its ink is ours.

   `backdrop-filter` is progressive: where it is unsupported the chip still renders at its own
   translucency, which is already enough to carry a dark logo. */
.auth-mark-plate {
    display: inline-flex;
    padding: var(--sp-3) var(--sp-4);
    border-radius: calc(var(--radius-card) * 2);
    background: rgb(255 255 255 / 0.90);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    box-shadow: var(--elev-raised);
}

/* Sized to carry the mark alone — it is the mark, not a bullet beside a name. Both dimensions are
   capped and `object-fit: contain` keeps the aspect ratio, so neither a tall square emblem nor a
   long horizontal wordmark arrives stretched: a wide mark hits `max-width` first and stops. */
.auth-mark-logo {
    display: block;
    max-width: min(240px, 100%);
    max-height: 52px;
    object-fit: contain;
    object-position: center;
}

/* ---- The vendor lockup (no logo uploaded) ---------------------------------
   Emblem plus name, in --c-hero-ink. No plate: this ink is ours and the scrim
   already holds it at contrast. */

.auth-mark-lockup {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-3);
}

.auth-mark-emblem {
    flex: none;
    width: 38px;
    height: 38px;
}

/* The faceplate the emblem is drawn on. Light rather than --c-primary: it sits on the scrim, not on
   a page, so it needs to read as a lit badge against a dark ground. */
.auth-mark-emblem-plate { fill: rgb(255 255 255 / 0.16); stroke: rgb(255 255 255 / 0.34); }

.auth-mark-limit {
    stroke: var(--c-hero-ink);
    stroke-width: 1.4;
    stroke-dasharray: 3 3;
    opacity: 0.45;
}

.auth-mark-trace {
    fill: none;
    stroke: var(--c-hero-ink);
    stroke-width: 2.4;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.auth-mark-pen { fill: var(--c-hero-ink); }

.auth-mark-name {
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 1.0625rem;
    line-height: 1.2;
    letter-spacing: -0.01em;
    color: var(--c-hero-ink);
}

/* ---- The claim, bottom-left ----------------------------------------------- */

.auth-hero-copy {
    display: flex;
    flex-direction: column;
    gap: var(--sp-4);
}

/* The engraved product claim (§3): display face, weight stops at 700 which §3 sets as the ceiling.

   The text-shadow is a second belt on top of the scrim's braces. The scrim guarantees the ratio
   against a worst-case flat photograph; a shadow is what keeps the letterforms crisp over a BUSY
   one, where a bright highlight can land inside a counter without moving the average at all. It is
   soft and offsetless on purpose — a glow that lifts the type off the picture, not a drop shadow. */
.auth-hero-title {
    margin: 0;
    /* Full width of the copy column, like the sentence beneath it. The 18ch cap is gone: it was what
       forced the vendor name onto its own line, and with both blocks now running to the same padding
       the two read as one column of copy rather than a narrow heading over a wide paragraph. */
    max-width: 100%;
    font-family: var(--font-display);
    /* 400, with only the vendor's name at 600 — the design sets the claim as running text with one
       emphasised phrase, not as a display heading. It was 700 throughout, which read as a poster. */
    font-weight: 400;
    font-size: clamp(1.5rem, 2.4vw, 2.1875rem);
    line-height: 1.45;
    letter-spacing: 0;
    color: var(--c-hero-ink);
    text-shadow: 0 1px 24px rgb(0 0 0 / 0.55);
}

/* The vendor's name inside the sentence. NOT --c-primary, which is what this was when the headline
   sat on --c-bg: that token is chosen to contrast with a PAGE, and on this panel a primary-coloured
   word would be the panel's own colour on itself. It is the same ink as the rest of the line, and it
   takes its own line and nothing else — no underline, no second colour. The sentence is already set
   in the display face at 700; a rule under the last line was one distinction too many. */
/* INLINE, not a block of its own. It lands on its own line in the design because the line
   before it fills, not because it was forced there — forcing it breaks the sentence at a fixed
   point and strands a short last line as soon as the column or the vendor name changes width. */
.auth-hero-brand { font-weight: 600; color: var(--c-hero-ink); }

.auth-hero-sub {
    margin: 0;
    /* Full width of the copy column, rather than the 46ch it was capped at. The cap was there to
       hold this sentence near a comfortable measure; at 100% it runs to the padding instead, which
       on a wide screen is a long line to scan. Asked for deliberately — the constraint that still
       applies is `.auth-hero-inner`'s 24% right padding, which keeps it clear of the curve. */
    max-width: 100%;
    font-size: 0.9375rem;
    line-height: 1.65;
    color: var(--c-hero-ink-muted);
    text-shadow: 0 1px 16px rgb(0 0 0 / 0.55);
}

/* ---- Headings and copy ---------------------------------------------------- */

/* A flex item's margins do not collapse, so a UA top margin here would open a gap the parent's
   `gap` has already accounted for. */
.form-title {
    margin: 0;
    font-family: var(--font-display);
    /* No font-size — the h1 takes the browser's own heading size (2em of the inherited 1rem, so
       32px) instead of the 1.5rem this used to pin it to. Asked for deliberately. Nothing else in
       fonts/tokens/auth/shell.css sizes a bare h1, so this rule is the only thing that was, and
       removing it hands the decision to the UA sheet. */
    font-weight: 600;
    line-height: 1.2;
    letter-spacing: -0.01em;
}

/* Sits directly under the title, so it cancels the parent gap and owns the spacing itself —
   otherwise the sentence floats as far from its heading as the first field does. */
.form-subtitle {
    margin: calc(var(--sp-4) * -1 + var(--sp-2)) 0 0;
    /* Full width of the form column, rather than the 42ch it was capped at — the same change, and
       for the same reason, as `.auth-hero-sub` above. The cap held the sentence near a comfortable
       measure; at 100% it runs to the card's padding instead, so the subtitle breaks on the same
       edge the fields below it do rather than turning over early against nothing. Asked for
       deliberately — the measure is now the card's width. */
    max-width: 100%;
    font-size: 0.875rem;
    line-height: 1.55;
    color: var(--c-text-muted);
}

.hint {
    font-size: 0.75rem;
    line-height: 1.5;
    color: var(--c-text-muted);
}

/* A neutral notice — "what happens next", not "something went wrong". --s-info is Layer-3's
   neutral-notice colour and this is exactly that; it is never used to grade a reading. */
.form-note {
    display: flex;
    gap: var(--sp-2);
    margin: 0;
    padding: var(--sp-3);
    border: 1px solid color-mix(in srgb, var(--s-info) 40%, transparent);
    border-radius: var(--radius-control);
    background: color-mix(in srgb, var(--s-info) 7%, transparent);
    font-size: 0.8125rem;
    line-height: 1.5;
    color: var(--c-text);
}

.form-note .icon { flex: none; margin-top: 2px; color: var(--s-info); }

.form-error {
    display: flex;
    gap: var(--sp-2);
    margin: 0;
    padding: var(--sp-3);
    border: 1px solid var(--s-alarm);
    border-radius: var(--radius-control);
    background: color-mix(in srgb, var(--s-alarm) 8%, transparent);
    font-size: 0.8125rem;
    line-height: 1.5;
    color: var(--c-text);
}

/* The box is bordered and backed in --s-alarm and led by an alert glyph, so the message itself is
   left in --c-text: the alarm red is a 3.6:1 foreground on some presets and this is body copy.
   Colour is not carrying the meaning alone either way (§8, WCAG 1.4.1). */
.form-error .icon { flex: none; margin-top: 2px; color: var(--s-alarm); }

/* ---- Fields --------------------------------------------------------------- */

.field {
    display: flex;
    flex-direction: column;
    gap: var(--sp-1);
    font-size: 0.8125rem;
}

/* Labels sit above their input and are real text, not a placeholder standing in for one (§6) — a
   placeholder disappears exactly when the user needs it. Sentence case, not uppercase: these are
   field labels, not engraved panel tags. Display face at 600 per §3. */
.field > span {
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 0.8125rem;
    color: var(--c-text);
}

/* A HINT INSIDE THE LABEL IS NOT A LABEL. The rule above dresses every direct span of a field as
   one — display face, 600, full-strength ink — and `.hint` alone cannot undo that: it carries the
   same specificity and is declared earlier, so it loses on every property the two share. /license is
   the only screen that puts its hint inside the <label> rather than after it, and until this rule
   existed the machine-id sentence rendered as a second bold heading under the field it explains. */
.field > span.hint {
    font-family: var(--font-body);
    font-weight: 400;
    font-size: 0.75rem;
    line-height: 1.5;
    color: var(--c-text-muted);
}

.field input {
    /* §8: touch targets ≥40px for tablet use on the plant floor. */
    min-height: 42px;
    width: 100%;
    padding: var(--sp-2) var(--sp-3);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-control);
    /* The raised surface, not --c-surface: the page is one --c-bg now, and --c-surface sits close
       enough to it under the light presets that the fields would read as painted-on rather than as
       something to type in. */
    background: var(--c-surface-raised);
    color: var(--c-text);
    font-family: var(--font-body);
    font-size: 0.875rem;
    transition: border-color var(--motion-fast) var(--motion-ease);
}

.field input::placeholder { color: var(--c-text-muted); opacity: 0.7; }

.field input:hover { border-color: color-mix(in srgb, var(--c-border) 50%, var(--c-text-muted)); }

.field input:focus {
    border-color: var(--c-primary);
    outline: var(--focus-width) solid var(--c-focus);
    outline-offset: var(--focus-offset);
}

/* Blazor marks a field that failed server validation; the ring must not depend on colour alone, so
   the message underneath is what actually states the problem. */
.field input.invalid:not(:focus) { border-color: var(--s-alarm); }

.field .validation-message {
    font-size: 0.75rem;
    line-height: 1.4;
    color: var(--s-alarm);
}

/* The positioning context for the reveal button below, and nothing else now.

   A LEADING GLYPH USED TO SIT INSIDE EVERY CONTROL — a user, a lock, an envelope, indented behind
   the caret. It has been removed from all four screens. It never said anything the label above the
   box did not already say in words, it cost every input 28px of its left inset, and on a form whose
   whole job is two boxes it read as clutter rather than as help. The reveal button stays, because
   that one is a control and does something. */
.field-control { position: relative; display: block; }

/* ---- Show / hide password -------------------------------------------------
   Mirror image of `.field-icon`: same inset, other edge. Unlike that one this IS interactive, so it
   is a real <button type="button"> — never a click handler on a span, and never inside the label's
   own hit area by accident (it is a sibling of the input inside `.field-control`, and clicking it
   toggles rather than focusing).

   `hidden` until js/password-reveal.js takes it off. The attribute selector below outranks the
   `display: flex`, so the un-enhanced page shows nothing here rather than a button that does not
   work. */
.field-reveal {
    position: absolute;
    right: var(--sp-1);
    top: 50%;
    transform: translateY(-50%);
    display: flex;
    align-items: center;
    justify-content: center;
    /* §8's 40px floor would swamp a 42px field, but this sits INSIDE the input's own target and the
       field is still reachable everywhere else, so 32px is the honest size of the extra control. */
    width: 32px;
    height: 32px;
    padding: 0;
    border: 0;
    border-radius: var(--radius-control);
    background: none;
    color: var(--c-text-muted);
    cursor: pointer;
}

.field-reveal[hidden] { display: none; }

.field-reveal:hover { color: var(--c-text); }

.field-reveal:focus-visible {
    outline: var(--focus-width) solid var(--c-focus);
    outline-offset: calc(var(--focus-offset) * -1);   /* inset, so the ring stays inside the field */
}

/* The glyph is chosen by the button's own aria-pressed, so the state has ONE source and the screen
   reader and the sighted reader are told the same thing by the same attribute. Pressed = revealed =
   offer to hide. */
.field-reveal .icon-eye-off { display: none; }
.field-reveal[aria-pressed="true"] .icon-eye { display: none; }
.field-reveal[aria-pressed="true"] .icon-eye-off { display: block; }

/* Room for the button, so a long password never runs underneath it. */
.field-control-reveal input { padding-right: calc(var(--sp-1) + 32px); }

/* The one-time code is a six-figure number, so it gets the mono face, tabular figures and enough
   tracking to be read back off a phone screen one digit at a time. */
.field-code input {
    max-width: 13ch;
    font-family: var(--font-mono);
    font-size: 1.125rem;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.28em;
    text-align: center;
}

/* ---- The licence key field ------------------------------------------------
   /license is the only screen in the product that asks for a long opaque string. It gets a textarea
   because keys wrap in email and arrive across several lines; LicenseStore.Normalize puts them back
   together, so the field never has to ask the customer to tidy the paste up first.

   The textarea inherits none of `.field input`'s rules, so the box treatment is restated here rather
   than by widening that selector — an input and a multi-line control differ in enough places
   (min-height, resize, line-height, the mono face) that sharing one rule would mean overriding most
   of it back. */
.field textarea {
    width: 100%;
    padding: var(--sp-2) var(--sp-3);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-control);
    background: var(--c-surface-raised);
    color: var(--c-text);
    /* Mono, and wrapped anywhere: a licence key is base64url with no spaces in it, so a proportional
       face with word-wrapping would show one unbroken line running off the right edge. */
    font-family: var(--font-mono);
    font-size: 0.8125rem;
    line-height: 1.6;
    overflow-wrap: anywhere;
    /* Vertical only. Horizontal resize would let the box escape the card's width. */
    resize: vertical;
    transition: border-color var(--motion-fast) var(--motion-ease);
}

.field textarea::placeholder { color: var(--c-text-muted); opacity: 0.7; }

.field textarea:hover { border-color: color-mix(in srgb, var(--c-border) 50%, var(--c-text-muted)); }

.field textarea:focus {
    border-color: var(--c-primary);
    outline: var(--focus-width) solid var(--c-focus);
    outline-offset: var(--focus-offset);
}

.field textarea.invalid:not(:focus) { border-color: var(--s-alarm); }

/* The machine id exists to be copied into an email, so it is mono and tracked out — the same
   argument `.field-code` makes about a one-time code being read back off a phone. Read-only rather
   than disabled: a disabled field cannot be focused or copied from, which is the only thing this
   one is for. Muted text says "not yours to edit" without taking the copy away. */
.field-machine input[readonly] {
    font-family: var(--font-mono);
    font-size: 0.8125rem;
    letter-spacing: 0.06em;
    color: var(--c-text-muted);
    cursor: text;
}

.field-machine input[readonly]:hover { border-color: var(--c-border); }

/* ---- Locked-session identity ----------------------------------------------
   /lock states whose session is being unlocked. Built as a panel rather than as a disabled input on
   purpose: a greyed-out username box invites people to try to edit it, and this is not a field the
   form has — the only account this screen can unlock is the one already signed in. Someone else uses
   the sign-out link below it. */
.lock-identity {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    min-height: 42px;
    padding: var(--sp-2) var(--sp-3);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-control);
    background: var(--c-surface-raised);
}

.lock-identity-icon { flex: none; color: var(--c-text-muted); }

.lock-identity-text { display: flex; flex-direction: column; min-width: 0; }

.lock-identity-label {
    font-size: 0.6875rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--c-text-muted);
}

/* Truncates rather than wraps: a long full name must not push the password field down the card. */
.lock-identity-name {
    overflow: hidden;
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 0.875rem;
    color: var(--c-text);
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ---- Buttons and links ---------------------------------------------------- */

/* .btn-primary on the auth screens is a separate class from the app shell's .btn (see shell.css) —
   it inherits none of the shell's sizing on purpose, because these are the only full-width,
   single-commit buttons in the product. */
/* INVERTED, not primary-coloured, and that is a deliberate departure from the rest of the product.

   --c-text on --c-bg, the page's own two ends swapped over. On every light preset that lands as a
   near-black button with pale text, which is the design; on a dark preset it inverts with the theme
   and stays a button rather than becoming a hole.

   It is also the one pair on this screen whose ratio needs no arithmetic from anyone: --c-text on
   --c-bg is what the theme contrast gate already holds at 4.5:1, and swapping foreground for
   background leaves a contrast ratio unchanged. So this button is AA on any theme that passes the
   gate, by construction.

   Why not --c-primary: the panel beside it is now --c-primary at full strength. A primary button on
   a primary panel put the same colour on both halves of the screen and left the commit competing
   with the decoration; against a near-black one the eye goes to the button, which is the thing to
   press. */
.btn-primary {
    padding: var(--sp-2) var(--sp-4);
    background: var(--c-text);
    color: var(--c-bg);
    border: 0;
    border-radius: var(--radius-control);
    font-weight: 500;
    cursor: pointer;
}

/* Primary, not a status colour: signing in is an action, not a condition (docs/16 §5.3 makes the
   same point about the signature dialog's commit button). */
.auth-submit {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    min-height: 46px;
    margin-top: var(--sp-2);
    padding: var(--sp-2) var(--sp-4);
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 0.9375rem;
    letter-spacing: 0.02em;
    /* The one raised thing on this side of the screen. It is the only control on the form that
       commits, and with the card gone there is nothing else here casting a shadow for it to compete
       with. */
    box-shadow: var(--elev-overlay);
    transition: background-color var(--motion-fast) var(--motion-ease),
                box-shadow var(--motion-fast) var(--motion-ease);
}

/* Mixed toward --c-text-muted, which is the one direction that softens the button without moving it
   toward its own label: it lifts a near-black button slightly on a light theme and settles a pale one
   on a dark theme, and --c-text-muted is itself gated against --c-bg, so the label keeps its ratio
   whichever way the mix runs. */
.auth-submit:hover { background: color-mix(in srgb, var(--c-text) 86%, var(--c-text-muted)); }

.auth-submit:focus-visible,
.link-muted:focus-visible {
    outline: var(--focus-width) solid var(--c-focus);
    outline-offset: var(--focus-offset);
    border-radius: var(--radius-control);
}

.link-muted {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    color: var(--c-text-muted);
    font-size: 0.8125rem;
    text-decoration: none;
}

.link-muted:hover { color: var(--c-text); text-decoration: underline; }

/* The way OUT of the field it belongs to, ranged right and sitting directly above the commit.

   Not `.auth-alt`, and the difference is what each one is a footer to. This is a footnote on the
   password box — "the one above, if you have lost it" — so it belongs against that box, on the
   edge the caret finishes at, with no rule between them. `.auth-alt` below is a footer to the whole
   FORM, centred under the commit, and /change-password still uses it for its sign-out.

   Negative top margin because the parent form gaps its rows at --sp-4 and this is not a row; it is
   a tail on the one above it. */
.auth-aside {
    display: flex;
    justify-content: flex-end;
    margin-top: calc(var(--sp-4) * -1 + var(--sp-1));
}

/* `.form-actions` used to live here for the auth screens' commit-plus-escape row. Nothing on these
   screens uses it any more — the sign-in, reset and change-password forms all take `.auth-submit`
   full-width with `.auth-alt` beneath — so the name now belongs to shell.css, where it is the row of
   buttons under an admin form. Two sheets defining it differently would have been a coin toss decided
   by link order. */

/* The way out of the screen, centred under the commit. A hairline rule, not a box. */
.auth-alt {
    display: flex;
    justify-content: center;
    gap: var(--sp-4);
    /* Cancels most of the parent form's `gap` — this is a footer to the commit above it, not another
       row of the form. Only correct INSIDE the form; the sibling case below resets it. */
    margin-top: calc(var(--sp-4) * -1 + var(--sp-2));
    padding-top: var(--sp-4);
    border-top: 1px solid var(--c-border);
}

/* /change-password's forced state puts its sign-out here, and sign-out is a POST — so this escape is
   a <form>, a SIBLING of the EditForm rather than a row inside it (HTML forbids nested forms).
   That drops it into `.auth-card form` above, which would lay it out as a column of fields, and out
   of the EditForm's `gap`, which the negative margin above was cancelling — with no gap left to
   cancel it would ride 8px up into the commit button. Both are put back here, and two classes beat
   that rule's one-class-one-type. */
.auth-card form.auth-alt {
    flex-direction: row;
    margin-top: var(--sp-4);
}

/* A <button> that has to read as the link beside it. `.link-muted` supplies the colour, size and
   icon gap; this strips the chrome a button arrives with (font-family is the one property a button
   will not inherit on its own).

   NEVER the other way round — sign-out must not be re-written as an <a> to save these five lines. A
   GET sign-out is a link any third-party page can make a browser follow. */
.link-button {
    padding: 0;
    border: 0;
    background: none;
    font-family: inherit;
    cursor: pointer;
}

/* ---- Step indicator (forgot password is two POSTs, so say which one) ------- */

.auth-steps {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    margin: 0;
    padding: 0;
    list-style: none;
}

.auth-step {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    font-family: var(--font-display);
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--c-text-muted);
}

/* The number, not just the colour, tells you where you are — and `aria-current` on the active step
   tells a screen reader the same thing. */
.auth-step-num {
    display: grid;
    place-items: center;
    width: 20px;
    height: 20px;
    border: 1px solid var(--c-border);
    border-radius: 50%;
    background: var(--c-surface);
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    color: var(--c-text-muted);
}

.auth-step-current { color: var(--c-text); }

.auth-step-current .auth-step-num {
    border-color: var(--c-primary);
    background: var(--c-primary);
    color: var(--c-primary-ink);
}

/* A done step is marked with a tick as well as the colour change. */
.auth-step-done .auth-step-num {
    border-color: var(--c-primary);
    color: var(--c-primary);
}

.auth-step-sep { flex: none; color: var(--c-border); }

/* ---- Password-policy checklist --------------------------------------------
   Progressive enhancement over js/password-policy.js; the server-side policy is authoritative and
   this renders as a plain requirements list with no JavaScript at all. */

/* --c-surface-raised, not --c-surface: this sits INSIDE the card, and the card is --c-surface itself
   now, so the old fill left the checklist as a border around nothing. Same step-forward rule the
   fields follow. */
.policy-check {
    margin: 0;
    padding: var(--sp-3);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-control);
    background: var(--c-surface-raised);
}

.policy-title {
    margin: 0 0 var(--sp-2);
    font-family: var(--font-display);
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--c-text-muted);
}

/* Two columns, so six rules occupy three rows instead of six. /change-password is the tallest of the
   three auth screens by a long way and the first to overflow a laptop viewport; this is ~62px of it,
   taken without changing a single rule's wording. (The 1400x950 fit that was measured here predates
   the card, its padding and the top inset on `.auth-side` — the saving still stands, the exact fit
   no longer does.)

   Grid with `auto-flow: column` and three explicit rows, NOT CSS `columns`: multi-column would let the
   browser choose the break, so adding a seventh rule would silently produce a 4+3 split with a stranded
   line. Three rows is stated, so the shape is predictable and the reading order stays top-to-bottom
   within each column.

   `minmax(0, 1fr)` on the columns, or a long rule ("A special character ($@!%*?&)") sets the column to
   its own min-content width and pushes the second column past the form's measure. */
.policy-list {
    list-style: none; margin: 0; padding: 0;
    display: grid;
    grid-auto-flow: column;
    grid-template-rows: repeat(3, auto);
    grid-auto-columns: minmax(0, 1fr);
    /* Column gap tightened from --sp-3 when the card narrowed to 480 — those 4px are the difference
       between the longest rule fitting its column and wrapping. */
    gap: var(--sp-1) var(--sp-2);
}

/* One column again on a narrow screen — two columns of 12px text in a 340px form is where the labels
   start wrapping mid-rule, which costs back the height it saved. */
@media (max-width: 420px) {
    .policy-list { grid-auto-flow: row; grid-template-rows: none; }
}

.policy-list li {
    display: flex;
    gap: var(--sp-2);
    align-items: baseline;
    font-size: 0.75rem;
    line-height: 1.4;
    color: var(--c-text-muted);
}

/* The mark is `·` / `✓` / `✕` written by the script, so the state is legible without the colour
   (§8, WCAG 1.4.1). Mono and fixed-width so the labels stay in one column as marks change. */
.policy-list li span[data-mark] { font-family: var(--font-mono); width: 1em; flex: none; display: inline-block; }

.policy-list li.rule-met { color: var(--s-normal); }
.policy-list li.rule-unmet { color: var(--s-alarm); }
.policy-list li.rule-pending { color: var(--c-text-muted); }

.policy-warn { font-size: 0.75rem; line-height: 1.4; color: var(--s-alarm); margin: var(--sp-2) 0 0; }

/* ---- Responsive -----------------------------------------------------------
   Below the point where the split stops paying for itself the two columns
   stack — and the panel goes FIRST, as a band across the top rather than a
   column down the side.

   THE BREAKPOINT IS 1100px, RAISED FROM 960 WHEN THE PANEL TOOK THE WIDER HALF,
   and it is arithmetic rather than taste. The form column is 45% of the viewport
   less --sp-6 of padding each side, and it needs 440px to hold the password
   checklist in two columns without a rule wrapping mid-sentence. Solving
   0.45V - 48 >= 440 gives V >= 1084. At the old 960 breakpoint the form column
   came out at 384px, which is 176px per checklist column against the ~194 the
   longest rule measures — so /change-password would have wrapped in a band
   roughly 120px wide that nothing else on the screen would have explained.

   Raising it costs a landscape tablet the split layout, which is the right trade:
   at 1024px a 45% form is already all margin and a 55% panel is cramped for a
   photograph and a headline. Stacked, both get the full width.

   That inverts the old rule here, which put the form first on the grounds that
   it is what the visitor came for. It was the right call for what the panel used
   to be: a full headline plus a framed illustration, which pushed the form off a
   phone screen entirely. It is the wrong call for what the panel is now, because
   the site's mark lives in it — form-first would have put the branding of the
   page somewhere below the sign-in button, where nobody would meet it.

   The band is capped instead, so it stays a header rather than becoming a hero:
   the photograph crops to it, the headline shrinks, and the form is still within
   a thumb's reach of the top. DOM order is untouched by any of this, so the
   keyboard still reaches the username field first either way. */
@media (max-width: 1100px) {
    /* Two rows: panel, then form. */
    .auth-shell { grid-template-columns: 1fr; grid-template-rows: auto auto; }

    .auth-hero {
        grid-column: 1;
        grid-row: 1;
        /* A band, not a column. `min` so a short landscape phone does not give a third of its
           viewport to decoration. */
        min-height: min(42vh, 320px);
    }

    .auth-side { grid-column: 1; grid-row: 2; padding: var(--sp-8) var(--sp-4); }

    /* Stacked, so there is no slack left to distribute and the auto margins would only add a void. */
    .auth-side-inner { margin: 0 auto; }

    /* The curve divides two columns, and there are none any more — on this axis the seam is
       horizontal, and a shape authored for a vertical edge cannot be turned into one by stretching it
       (objectBoundingBox would simply squash the same eleven segments into a wide short box). The
       band ends square. */
    .auth-hero { clip-path: none; }

    .auth-hero-inner {
        gap: var(--sp-6);
        padding: var(--sp-6) var(--sp-4);
    }

    .auth-hero-title { font-size: clamp(1.375rem, 4.4vw, 1.875rem); }
}

/* On a phone the panel gives up its sentence: the blurb is the longest text on the screen and the
   one part of it the headline above already implies. The headline stays — it says what the product
   is — and so does the mark. */
@media (max-width: 560px) {
    .auth-hero { min-height: min(34vh, 232px); }
    .auth-hero-sub { display: none; }

    .auth-mark-logo { max-height: 40px; }
    .auth-mark-emblem { width: 32px; height: 32px; }
    .auth-mark-name { font-size: 0.9375rem; }
}
