/* THE LAYER ORDER FOR THE WHOLE APP ORIGIN, DECLARED HERE BECAUSE ORDER IS SET
   BY FIRST APPEARANCE AND THIS IS THE FIRST STYLESHEET THE PAGE LINKS.
   Two of these five layers hold no rule written in this repository:

     tokens      the palette and the scales (below)
     radzen      Radzen.Blazor's `standard-base.css`, pulled in with
                 `@import ... layer(radzen)` by bk-radzen.css — the ONLY way a
                 stylesheet this repo does not author gets to be layered at all
     bk-radzen   the `--rz-*` -> `--bk-*` mapping, which must come after the
                 sheet whose variables it is overriding
     base        the app's own element styles and page modes
     components  the primitives and everything built on them

   WHY radzen SITS BELOW base RATHER THAN ABOVE IT, measured in 11.2.8 rather
   than assumed: `standard-base.css` is not confined to `.rz-*`. It carries
   `body{margin:0;color:…;background-color:…;font-family:…;font-size:…}`, a
   second `body{background:… url("data:image/svg+xml;base64,…")}` gradient, and
   bare `input` and `input::placeholder` rules. Unlayered — which is what a
   plain <link> would leave them — every one of those OUTRANKS this file, because
   unlayered normal declarations beat layered ones no matter what the layer
   order says. Bookary's paper background, its type and its `.bk-field` would
   have lost to a library that was never asked for an opinion about them.
   Underneath `base`, those rules are outranked — for every property this app
   actually declares. THE EXCEPTION IS WORTH KNOWING BEFORE YOU TRUST THE
   MECHANISM: a layer orders competing declarations of the SAME property, and
   `html, body` below declares `background-color`, a longhand. Radzen's
   `background` SHORTHAND also sets `background-image`, which nothing here
   names, so the gradient came through anyway and Chromium refused its data:
   URI under `default-src 'self'`. bk-radzen.css carries the one declaration
   that closes it, and the measurement that found it.

   THAT IS THE BOUNDARY, WRITTEN AS A MECHANISM. A library that cannot outrank
   the app's own base layer cannot creep past the components it was hired for,
   and the day someone deletes `radzen` from this line the app will look wrong
   immediately rather than slowly. */
@layer tokens, radzen, bk-radzen, base, components;

@layer tokens {
    /* Bookary theme (M1-5). Every colour the shell uses is one of these custom
       properties — component styles must reference the tokens, never a literal
       colour, so the dark-mode block below stays the only place a palette is
       defined. */
    :root {
        --bk-surface: #faf6ee;
        --bk-surface-alt: #f1ead9;
        --bk-surface-raised: #fffdf7;
        --bk-text: #23201b;
        --bk-muted: #645b50;
        --bk-border: #e4dac6;
        --bk-border-strong: #c9bca4;
        --bk-edge: #8c8070;
        --bk-shadow-ink: #1f1b16;
        --bk-accent: #7b2d26;
        --bk-accent-hover: #6a2620;
        --bk-accent-subtle: #ece2cc;
        --bk-accent-contrast: #fffdf7;
        --bk-highlight: #c8912f;
        --bk-danger: #b3261e;
        --bk-success: #2f6552;
        --bk-focus: #245faf;

        /* ── THE THEME SWATCH PALETTE (phase 2 R4) ────────────────────────────
           Every theme's page colours, side by side and never applied: the
           reader's settings offer the four themes as swatches, and a swatch
           has to show the palette its theme chooses while the page is wearing
           some other one. `--bk-surface`/`--bk-text` cannot do that — they are
           one palette at a time, resolved on :root — so these six name each
           theme's pair explicitly. They are fixed and theme-independent (one
           :root definition, no theme block redefines them, exactly as the
           materials below), and each carries the value its theme's block gives
           --bk-surface/--bk-text. A PROJECTION like every other copy of a
           palette in this app: PaletteProjectionTests pins each to its theme
           block, so a palette edit that forgets the swatches turns the build
           red instead of quietly painting them the old colours forever. */
        --bk-theme-light-surface: #faf6ee;
        --bk-theme-light-ink: #23201b;
        --bk-theme-dark-surface: #1a1712;
        --bk-theme-dark-ink: #ece4d6;
        --bk-theme-sepia-surface: #f4e9d3;
        --bk-theme-sepia-ink: #3c3428;

        /* ── THE CLOTH LIBRARY (hoisted here for T1's token contract) ─────────
           The materials a cover is bound in and the one info colour, from the
           approved library palette: navy cloth for Kindle, Account and info
           states; gilt foil for titles stamped on a binding; shelf oak for the
           boards; and five cloth colours for the cover bindings themselves.
           The MATERIALS (foil, shelf, cloth-1…5) are defined once, here, and
           not redefined in any theme — a material is the same cloth at night —
           and they are fills and decoration only, never ink on a surface.
           `--bk-info` is different: it is a ROLE colour like the accent, and
           the dark blocks redefine it as a light navy so it can be ink on a
           dark ground; as a fill it carries --bk-accent-contrast text, never
           foil. As everywhere in this app,
           the colour is beside words or an icon and never carries meaning
           alone. */
        --bk-info: #24395c;
        --bk-foil: #f3e6c4;
        --bk-shelf: #6b4a2e;
        --bk-cloth-1: #7b2d26;
        --bk-cloth-2: #356857;
        --bk-cloth-3: #24395c;
        --bk-cloth-4: #805a14;
        --bk-cloth-5: #6e5a45;

        /* ── THE HIGHLIGHT PALETTE (M6, hoisted here for #52) ──────────────────
           AnnotationColors names five colours, the reader shell draws them over
           the book, the notes panel shows them as swatches beside each quote,
           and — since #52 — the selection bar offers them at the moment the
           reader picks a passage. Three surfaces, one palette, so it is stated
           where every other palette in this app is stated rather than a third
           time in a third scoped stylesheet.

           IT LIVES HERE RATHER THAN IN THE PANEL BECAUSE THE THEME IS THE
           READER'S, NOT THE MACHINE'S. The panel used to define these under a
           bare `prefers-color-scheme: dark` query, which a Bookary theme cannot
           reach: the reader's choice is a stored preference rendered as
           `data-theme`, and a media query cannot see it. A reader who chose Dark
           on a light machine got the light highlights over a dark book. As
           tokens they follow the same four blocks as everything else and that
           stops being true.

           SEPIA TAKES THE LIGHT VALUES DELIBERATELY. Sepia is a light scheme
           with the blue taken out; these five are already pale washes meant to
           sit under black text, and warming them further would collapse yellow
           into the page. */
        --bk-annotation-yellow: #fde68a;
        --bk-annotation-green: #bbf7d0;
        --bk-annotation-blue: #bfdbfe;
        --bk-annotation-pink: #fbcfe8;
        --bk-annotation-purple: #ddd6fe;

        /* ── THE COVER-PLATE LIGHTNESS (hoisted here for #155) ──────────────
           The typeset fallback cover in BookCard.razor.css paints its plate as
           hsl(--book-cover-hue, s, l), the hue from the book's id. The s and l
           used to split across a bare `prefers-color-scheme: dark` query,
           which a Bookary theme cannot reach — the reader's choice is a stored
           preference rendered as `data-theme`, and a media query cannot see
           it. A reader who chose Dark on a light machine got the pale plate
           under the dark theme's pale --bk-text title, and the title vanished;
           the reverse hid it the other way. As tokens the plate follows the
           same four blocks as everything else and that stops being true.

           SATURATION TRAVELS WITH LIGHTNESS, because the dark plate was never
           only darker, it was duller — 22%, not 30%. Hoisting the lightness
           alone would have quietly resaturated every dark plate. Sepia takes
           the light values deliberately, as the annotation tokens do: it is a
           light scheme with the blue taken out, and its plates are the same
           pale ones under a warm page. */
        --bk-cover-plate-s: 30%;
        --bk-cover-plate-l: 86%;

        /* ── THE SCALES (DESIGN-PLAN §4.4) ─────────────────────────────────────
           Defined once, here, because none of them vary by theme: a radius is not
           lighter at night. Only the palette above is redefined per theme, which
           is what keeps the theme blocks readable as palettes rather than as
           four copies of the whole design.

           TYPE. The rule is legible in one line: if a human reads it as LANGUAGE
           it is the reading face; if they read it as a CONTROL it is the UI
           face. Figtree is the UI face and Literata the reading face, and both
           are SELF-HOSTED: the CSP on this origin has style-src-elem 'self' and
           no font-src, so a font may be served from wwwroot and from nowhere
           else — no CDN, no Google Fonts link. Each is one latin subset variable
           woff2 with its OFL text beside it (FontVendorTests pins the pair), and
           both declare font-display: swap, so a slow font degrades to the
           fallbacks below for a line rather than hiding the page. Georgia ships
           everywhere and is a real serif; the system sans is the UI fallback. */
        --bk-font-ui: "Figtree", system-ui, -apple-system, "Segoe UI", sans-serif;
        --bk-font-reading: Literata, Georgia, "Times New Roman", serif;
        /* LABELS — eyebrows, stamps, counts, window titles. The slightly
           mechanical label voice comes from this stack and from the geometry
           below, NOT from a display face: decorative faces break at 200% zoom
           and under 16px and would cost a download for decoration. Zero bytes. */
        --bk-font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;

        --bk-text-xs: 0.8rem;
        --bk-text-sm: 0.9rem;
        --bk-text-base: 1rem;
        --bk-text-md: 1.25rem;
        --bk-text-lg: 1.563rem;
        --bk-text-xl: 1.953rem;
        --bk-text-2xl: 2.441rem;

        --bk-leading-tight: 1.2;
        --bk-leading-normal: 1.4;
        --bk-leading-reading: 1.65;

        /* SPACE — 4px base, naming the de-facto scale already in use. */
        --bk-space-1: 0.25rem;
        --bk-space-2: 0.5rem;
        --bk-space-3: 0.75rem;
        --bk-space-4: 1rem;
        --bk-space-5: 1.5rem;
        --bk-space-6: 2rem;
        --bk-space-7: 3rem;
        --bk-space-8: 4rem;

        /* RADIUS — 4/8/12, RESTORED 2026-09-26 (Direction A — Quiet Bookcloth,
           overruling the square-corners decision of 2026-09-02). Three names,
           three steps: a field, a card and a panel are three sizes of thing and
           were all flat. `pill` is the capsule — chips, tags — and `full` stays
           for the avatar-shaped things that have no edges to draw. The old
           2/3/4/6/8px lottery stays dead: these four values are the whole
           scale. */
        --bk-radius-sm: 4px;
        --bk-radius-md: 8px;
        --bk-radius-lg: 12px;
        --bk-radius-pill: 999px;
        --bk-radius-full: 999px;

        /* EDGE AND SHADOW — the boundary rule, and the whole of it. The boundary
           of a CONTROL is a 1px line in --bk-edge; rows INSIDE an object keep
           the --bk-border hairline, which is decoration and never a control's
           only edge (ColourRuleTests). --bk-edge is the boundary colour for
           accessibility as much as for looks: the hairline measures under the
           3:1 WCAG 2.2 SC 1.4.11 asks of a control boundary, and --bk-edge
           clears 3.5:1 on --bk-surface and --bk-surface-raised in every theme
           (PaletteProjectionTests pins the margin, not just the pass).
           The shadow is a SOFT, LOW one and it is for the things that LIFT
           off the page: a book cover, a floating menu, and — since the sheets
           and dialogs landed — the library's edit/membership/delete sheets and
           the dialogs that open over them. It is built from the per-theme
           --bk-shadow-ink with color-mix rather than a literal rgba(), so dark
           and sepia are tinted by their own ink instead of wearing a black
           smear — no rgb()/rgba() literal in any shadow. The
           tokens live with the scales rather than the palette because only
           their ink varies by theme. */
        --bk-edge-width: 1px;
        --bk-shadow-1: 0 1px 2px color-mix(in srgb, var(--bk-shadow-ink) 14%, transparent);
        --bk-shadow-2: 0 1px 2px color-mix(in srgb, var(--bk-shadow-ink) 12%, transparent), 0 12px 24px color-mix(in srgb, var(--bk-shadow-ink) 14%, transparent);

        /* MOTION — a library is quiet. Nothing bounces. */
        --bk-duration-quick: 120ms;
        --bk-duration-gentle: 260ms;
        --bk-ease: cubic-bezier(0.2, 0, 0, 1);

        /* The bottom navigation bar's own height, without the safe-area inset it
       also reserves. A token because TWO files need the same number: app.css
       sizes the bar and MainLayout.razor.css pads the footer clear of it, and
       a bar that is 3.5rem beside a footer that cleared 3rem is a footer with
       its last line under the navigation. Same arrangement, and same reason,
       as ImmersiveLayout publishing its two reader-bar sizes (§5A.1). The
       phone bar's slot is an icon over its label since T1's nav change, which
       is why the bar is 4.25rem and not the 3.5rem it started at: 22px of
       icon, a gap and a label do not fit a 56px slot without touching. */
    --bk-nav-block-size: 4.25rem;

    /* SAFE AREA — DESIGN-PLAN §5.2 rule 5 and §5A.1. These four declarations
           are the ONLY place in the app origin that calls env(safe-area-inset-*);
           every other site reads var(--bk-safe-*). The confinement is
           the point: env() cannot be grepped for intent, so a rule saying only a
           layout may name an inset is unenforceable while the function can appear
           anywhere — reduced to one declaration each, the rule becomes a source
           test that reads one file. The 0px fallback lives here rather than at
           every call site, so a browser that has never heard of the function
           computes a length once instead of dropping declarations all over the
           app. These are PHYSICAL and have no logical equivalent — there is no
           safe-area-inset-inline-start — so this is the single seam where RTL
           would have to be picked up if it ever came into scope (§9), instead of
           a dozen. And none of them are non-zero without viewport-fit=cover on
           the viewport meta in App.razor. */
        --bk-safe-top: env(safe-area-inset-top, 0px);
        --bk-safe-right: env(safe-area-inset-right, 0px);
        --bk-safe-bottom: env(safe-area-inset-bottom, 0px);
        --bk-safe-left: env(safe-area-inset-left, 0px);

        color-scheme: light;
    }

    /* Everything that moves, stops moving, for a reader who asked for that. One
       block rather than a per-component opt-in, because the per-component version
       is a list somebody forgets to add to. */
    @media (prefers-reduced-motion: reduce) {
        :root {
            --bk-duration-quick: 0ms;
            --bk-duration-gentle: 0ms;
        }
    }

    /* Follow the device only when explicitly selected. Without a stored
       theme, the light palette above is the default on every device. */
    @media (prefers-color-scheme: dark) {
        :root[data-theme="system"] {
            color-scheme: dark;
            --bk-surface: #1a1712;
            --bk-surface-alt: #232019;
            --bk-surface-raised: #24211a;
            --bk-text: #ece4d6;
            --bk-muted: #a79e8c;
            --bk-border: #3a342a;
            --bk-border-strong: #564d3c;
            /* #183: lifted one step from #7a6f5d, which cleared SC 1.4.11's
               3:1 for a control boundary with no margin — 3.62:1 on
               --bk-surface and 3.26:1 on --bk-surface-raised, so one shade of
               surface drift or one translucent overlay took it under. #8a7e6b
               measures 4.49:1 on surface and 4.04:1 on raised. The shadow
               stays black: it is a soft one now, a low wash rather than a
               drawn edge, and black on black is the deepest lamplight can
               draw. */
            --bk-edge: #8a7e6b;
            --bk-shadow-ink: #000000;
            --bk-accent: #c87a70;
            --bk-accent-hover: #d78d83;
            /* #141: accent-as-ink (#c87a70) stays for text and focus, but a
               filled CTA in it is a pastel pink on black — the product's most
               important control with the least visual authority. Filled
               primaries take this deeper, higher-chroma red instead: 4.73:1
               against --bk-surface and against --bk-accent-contrast, so the
               dark label still passes AA. */
            --bk-accent-cta: #cd6549;
            --bk-accent-cta-hover: #d87a5c;
            --bk-accent-subtle: #2a2519;
            --bk-accent-contrast: #1a1712;
            /* Navy cloth cannot be ink on a dark ground (1.5:1). In dark it is a
               light navy, used as ink on the surfaces or as a fill under
               --bk-accent-contrast, the same way the dark accent works. */
            --bk-info: #8fa6cf;
            --bk-highlight: #d9a85b;
            --bk-danger: #e58e83;
            --bk-success: #7fb7a4;
            --bk-focus: #c87a70;

            --bk-annotation-yellow: #78350f;
            --bk-annotation-green: #14532d;
            --bk-annotation-blue: #1e3a8a;
            --bk-annotation-pink: #831843;
            --bk-annotation-purple: #4c1d95;

            --bk-cover-plate-s: 22%;
            --bk-cover-plate-l: 27%;
        }
    }

    /* ── A READER'S OWN CHOICE (M5-10) ──────────────────────────────────────────

       The reader's theme is not only the book's. The book renders in a frame on
       the books origin and the chrome around it — the bar, the controls, the
       contents — is app-origin markup styled from these tokens, so a dark book in
       a light frame is a page that looks broken rather than themed. One preference,
       applied in two places: here for the chrome, and over the protocol's
       setAppearance for the book itself.

       `color-scheme` is set alongside the palette rather than left at `light dark`,
       because it is what tells the browser which way to paint the things CSS does
       not reach — form controls, scrollbars, and the canvas behind a frame that has
       not painted yet. Left alone, an explicit dark theme keeps light scrollbars. */
    :root[data-theme="light"] {
        --bk-surface: #faf6ee;
        --bk-surface-alt: #f1ead9;
        --bk-surface-raised: #fffdf7;
        --bk-text: #23201b;
        --bk-muted: #645b50;
        --bk-border: #e4dac6;
        --bk-border-strong: #c9bca4;
        --bk-edge: #8c8070;
        --bk-shadow-ink: #1f1b16;
        --bk-accent: #7b2d26;
        --bk-accent-hover: #6a2620;
        --bk-accent-subtle: #ece2cc;
        --bk-accent-contrast: #fffdf7;
        --bk-highlight: #c8912f;
        --bk-danger: #b3261e;
        --bk-success: #2f6552;
        --bk-focus: #245faf;

        --bk-annotation-yellow: #fde68a;
        --bk-annotation-green: #bbf7d0;
        --bk-annotation-blue: #bfdbfe;
        --bk-annotation-pink: #fbcfe8;
        --bk-annotation-purple: #ddd6fe;

        --bk-cover-plate-s: 30%;
        --bk-cover-plate-l: 86%;

        color-scheme: light;
    }

    :root[data-theme="dark"] {
        --bk-surface: #1a1712;
        --bk-surface-alt: #232019;
        --bk-surface-raised: #24211a;
        --bk-text: #ece4d6;
        --bk-muted: #a79e8c;
        --bk-border: #3a342a;
        --bk-border-strong: #564d3c;
        /* #183: same lift as the system-dark block above — the two dark
           palettes must stay identical (PaletteProjectionTests pins them). */
        --bk-edge: #8a7e6b;
        --bk-shadow-ink: #000000;
        --bk-accent: #c87a70;
        --bk-accent-hover: #d78d83;
        /* #141: same CTA-fill split as the system-dark block above — the two
           dark palettes must stay identical (PaletteProjectionTests pins
           them). */
        --bk-accent-cta: #cd6549;
        --bk-accent-cta-hover: #d87a5c;
        --bk-accent-subtle: #2a2519;
        --bk-accent-contrast: #1a1712;
        /* Navy cloth cannot be ink on a dark ground (1.5:1). In dark it is a
           light navy, used as ink on the surfaces or as a fill under
           --bk-accent-contrast, the same way the dark accent works. */
        --bk-info: #8fa6cf;
        --bk-highlight: #d9a85b;
        --bk-danger: #e58e83;
        --bk-success: #7fb7a4;
        --bk-focus: #c87a70;

        --bk-annotation-yellow: #78350f;
        --bk-annotation-green: #14532d;
        --bk-annotation-blue: #1e3a8a;
        --bk-annotation-pink: #831843;
        --bk-annotation-purple: #4c1d95;

        --bk-cover-plate-s: 22%;
        --bk-cover-plate-l: 27%;

        color-scheme: dark;
    }

    /* Sepia is a LIGHT scheme with the blue taken out, not a third mode: it exists
       because reading a novel on paper-white for an hour is tiring, and every
       e-reader that has ever sold offers it. `color-scheme: light` is correct here
       — the browser's own furniture should stay light, because this is. */
    :root[data-theme="sepia"] {
        --bk-surface: #f4e9d3;
        --bk-surface-alt: #e9dcc0;
        --bk-surface-raised: #fbf3e3;
        --bk-text: #3c3428;
        /* #726751 measured 4.10:1 on --bk-surface-alt, which is below the 4.5:1
           the muted role has to clear as a caption on the desk. #675c47 is 4.83:1
           there and 5.6:1 on the page; found by the pixel-library contrast sweep. */
        --bk-muted: #675c47;
        --bk-border: #d5c5a3;
        --bk-border-strong: #bda87e;
        --bk-edge: #3c3428;
        --bk-shadow-ink: #3c3428;
        --bk-accent: #73281f;
        --bk-accent-hover: #63221a;
        --bk-accent-subtle: #ecdcc9;
        --bk-accent-contrast: #f4e9d3;
        --bk-highlight: #b08329;
        --bk-danger: #a5301f;
        --bk-success: #2c5a4d;
        --bk-focus: #73281f;

        --bk-annotation-yellow: #fde68a;
        --bk-annotation-green: #bbf7d0;
        --bk-annotation-blue: #bfdbfe;
        --bk-annotation-pink: #fbcfe8;
        --bk-annotation-purple: #ddd6fe;

        --bk-cover-plate-s: 30%;
        --bk-cover-plate-l: 86%;

        color-scheme: light;
    }
}

@layer base {
    /* ── THE BASE LAYER ────────────────────────────────────────────────────
       DESIGN-PLAN §5.2 rule 8. Every element in the app was content-box, and
       the codebase was already working around it one element at a time: the
       page-mode comment below explains why its width is auto, #blazor-error-ui
       carried its own box-sizing declaration, and #components-reconnect-modal
       computed to 20rem + 2rem of padding + a border = 386px, which overflowed
       a 360px phone by 26px — the banner a reader sees when the network is at
       its worst, on the narrowest screen (§2.5 finding 5, measured 2026-08-27).
       One declaration removes the class.

       It is in a LAYER so that a component which deliberately wants
       content-box can still say so unlayered — Blazor's scoped-CSS bundle is
       unlayered by construction, and unlayered beats layered whatever the
       specificity. Nothing currently wants it; the point is that the reset is
       a default rather than a decree. */
    *,
    *::before,
    *::after {
        box-sizing: border-box;
    }

    html, body {
        margin: 0;
        font-family: var(--bk-font-ui);
        background-color: var(--bk-surface);
        color: var(--bk-text);
    }

    /* ── THE TYPE RULE (DESIGN-PLAN §4.4) ──────────────────────────────────
       Read as LANGUAGE and it is the reading face: a heading is a sentence a
       human reads, so h1-h3 take --bk-font-reading wherever they appear —
       including outside `.prose`, where they used to stay in the UI face and
       half the app's headings were set in a system dialog's typeface on a
       product about set type (the review that fed this change measured
       Library, Add a book, Sign in and Create account).

       Read as a CONTROL and it is the UI face, named here rather than left to
       inheritance: `label` inherits whatever its container is wearing, so a
       checkbox label on a prose page (Account) is a control sitting in the
       reading face without this, and `.nav-link` must not follow a heading it
       happens to sit under. Same rule, same reason as `.bk-button`'s own
       `font-family` (#207) — these are the element-level stragglers the
       primitive does not reach. */
    h1,
    h2,
    h3 {
        font-family: var(--bk-font-reading);
    }

    button,
    input,
    label,
    .nav-link {
        font-family: var(--bk-font-ui);
    }

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

    h1:focus {
        outline: none;
    }

    .content {
        padding-block-start: 1.1rem;
    }

    /* ── PAGE MODES (DESIGN-PLAN §5, phase 0) ──────────────────────────────────

       Every routable page declares one of three modes, and the declaration is in
       the page's own first lines: `@layout ImmersiveLayout` for a screen that owns
       the viewport (the reader), or one of these two classes on the page's root
       element under MainLayout. There is deliberately NO unclassed default: the
       old one-size `.shell-content` prose box is the container the reader broke
       inside (§2.1), and a page that declares nothing now fails LayoutModeTests
       rather than silently inheriting a reading measure it never asked for.

       Global rather than scoped for the same reason `.prose` is: pages in two
       assemblies share these, and component-scoped CSS cannot cross a component.

       `inline-size` is left auto, and the reason changed under it. It used to be
       that `width: 100%` plus padding overflowed the viewport horizontally
       (content-box arithmetic) — the base layer's universal border-box reset
       ended that class of bug, and this comment was the codebase working around
       its absence one element at a time (§2.5 finding 5). Auto is still right,
       because it lets `max-inline-size` plus auto inline margins centre the
       column, but it is no longer load-bearing against overflow.

       The horizontal padding carries the safe-area inset because §5A.1 puts the
       HORIZONTAL insets on the page modes: a landscape phone puts its notch on
       the SIDE, and neither the phone's 1rem nor the 1.5rem a wide screen gets
       below clears it. `max()`, not addition — the inset
       replaces the design gutter when it is larger rather than adding to it.
       There is no bottom inset here on purpose: MainLayout's .shell-footer
       carries it and sits below this content, so adding one here would reserve
       the same band twice. The two tokens are PHYSICAL — the underlying env()
       functions have no logical form — so in an RTL document these two values
       would need swapping; RTL is an explicit non-goal (§9), and the token block
       above is the one seam where that would be picked up. */
    .bk-prose,
    .bk-workspace {
        margin-block: 0;
        margin-inline: auto;
        padding-block: 1rem;
        padding-inline: max(1rem, var(--bk-safe-left))
                        max(1rem, var(--bk-safe-right));
    }

    /* DESIGN-PLAN §5.3: the two named breakpoints, and the only two.
       --bk-bp-wide is 48rem (768px) and --bk-bp-desk is 64rem (1024px); a
       custom property cannot be used in a media query's condition, so they are
       named here in a comment rather than read as tokens. The unqualified
       value above is the PHONE value and this query adds the wider screen —
       the old rule was a `max-width: 640.98px` subtraction, and the `.98` was
       a Bootstrap-era workaround for a max-width off-by-one that min-width
       does not have. */
    @media (min-width: 48rem) {
        .bk-prose,
        .bk-workspace {
            padding-block: 1.5rem;
            padding-inline: max(1.5rem, var(--bk-safe-left))
                            max(1.5rem, var(--bk-safe-right));
        }
    }

    /* The reading column MainLayout used to impose on everything — now only on
       the pages that are actually documents: terms, privacy, contact, pricing,
       account, the auth cards, home until phase 4 gives it a landing. */
    .bk-prose {
        max-inline-size: 64rem;
    }

    /* A working surface: the library grid, upload. Wider so the grid breathes;
       `min-width: 0` so a grid or flex child inside it may shrink to fit rather
       than force the shell wide (the horizontal half of the flexbox trap). */
    .bk-workspace {
        max-inline-size: 90rem;
        min-inline-size: 0;
    }

    /* Long-form pages: terms, privacy, contact. Global rather than scoped,
       because component-scoped CSS applies only to the component that declares
       it and these three share one shape.

       DECLARED AFTER `.bk-prose` AND COMPOSED WITH IT on the same element
       (`class="prose bk-prose"`): equal specificity means source order decides,
       so the measure here (42rem) beats the mode's 64rem cap, and the
       `padding-block` longhand overrides only the vertical padding while the
       mode's 1.5rem inline padding keeps the text off the viewport edge. The
       block values are the SUM of the two boxes that used to nest (1.5rem of
       .shell-content + 1rem/3rem of .prose), so the pages did not move.

       THE PAGES DID MOVE ON 2026-08-28, and this is the one place item (b)'s
       box-sizing reset is visible rather than invisible. `max-inline-size:
       42rem` now caps the BORDER box instead of the content box, so the
       measure is 42rem INCLUDING the mode's 1.5rem gutters rather than plus
       them: measured on /terms at 768px and 1280px, the line of text went
       from 672px to 624px, which is 85 characters to 79.

       Kept rather than compensated for, on two grounds. The declaration now
       says what §5.1's own mode table says — `.bk-prose` owns "a centred,
       padded, max-width 42-64rem reading COLUMN", and a column is the thing
       including its gutters. And §7 wants 60-70 characters: 85 was over it and
       79 is less over it, so the reset moves the measure toward the target
       rather than away. Closing the remaining gap is phase 3's, which owns
       measure and will be setting it against real column geometry anyway;
       adding 3rem back here to preserve a number nobody chose would be
       compensating for the fix. */
    .prose {
        max-inline-size: 42rem;
        margin-block: 0;
        margin-inline: auto;
        padding-block: 2.5rem 4.5rem;
        line-height: 1.65;
    }

    .prose h1 {
        margin-block-end: 0.5rem;
    }

    .prose h2 {
        margin-block-start: 2rem;
        font-size: 1.15rem;
    }

    .prose ul {
        padding-inline-start: 1.25rem;
    }

    .prose li {
        margin-block: 0.35rem;
    }

    .prose-note {
        border-inline-start: 3px solid var(--bk-border);
        padding-block: 0.75rem;
        padding-inline: 1rem;
        background: var(--bk-surface, transparent);
        font-size: 0.95rem;
    }
}

@layer components {
    /* The reconnect banner: what a reader sees when their train enters a tunnel.
       It is position: fixed to the bottom of the VIEWPORT, so on an installed
       phone its text sat inside the home-indicator gesture band. §5.2 rule 5 says
       only a layout may name a safe-area inset, and this is the one carve-out:
       position: fixed against the viewport IS the claim that this element's edge
       is the screen edge, so it is entitled to reserve it. A statically
       positioned component is not, because it cannot know. */
    #blazor-error-ui {
        background: var(--bk-surface-alt);
        color: var(--bk-text);
        /* The banner's top edge is a separator, not a control boundary — the
           hairline seam, like every row inside an object; only controls take
           --bk-edge. */
        border-block-start: var(--bk-edge-width) solid var(--bk-border);
        inset-block-end: 0;
        display: none;
        inset-inline-start: 0;
        padding-block: 0.6rem calc(0.7rem + var(--bk-safe-bottom));
        padding-inline: max(1.25rem, var(--bk-safe-left))
                        max(1.25rem, var(--bk-safe-right));
        position: fixed;
        inline-size: 100%;
        z-index: 1000;
    }

        #blazor-error-ui .dismiss {
            cursor: pointer;
            position: absolute;
            inset-inline-end: 0.75rem;
            inset-block-start: 0.5rem;
        }


    /* ══════════════════════════════════════════════════════════════════════════
       CONTROL PRIMITIVES (DESIGN-PLAN §4.5, phase 1)

       These exist because seven components had each invented their own button and
       two pages reached for shared classes that were DEFINED NOWHERE. `.button`
       and `.button-danger` are on Account and Pricing today and resolve to no rule
       at all, so the most destructive control in the product — "Delete my account
       permanently" — renders as a browser default. That is the bug these close.

       ── THE DESTRUCTIVE GRAMMAR, which is the reason for the variants ─────────

       Oxblood and alarm red sit 0.7° apart in hue. Colour therefore cannot be what
       separates "proceed" from "this erases things", because to most eyes at most
       sizes it is the same red. FORM carries it instead:

         filled      = proceed. One per view, the thing you came to do.
         text        = secondary safe. Any number. NOT outlined — if it were,
                       "outlined" would mean "not primary", the rule would
                       collapse, and hue would be all that was left.
         bordered    = neutral tertiary. Toolbars, card rows. Never red.
         outlined red = destructive, and NEVER filled.

       A destructive control is never icon-only: where an outline cannot read,
       language and friction carry it instead. */

    /* ── 44 x 44, AND THE THREE DECLARATIONS ARE NOT INTERCHANGEABLE ────────
       DESIGN-PLAN §5.4. Measured 2026-08-27, not one interactive control in
       the app cleared 44x44 — `.bk-button`, the primitive phase 1 built to end
       inconsistent controls, was itself 38px, so adopting it as written would
       have made the app fail WCAG 2.2 2.5.8 uniformly instead of variously.
       That is why the minimum lands in the primitive, before adoption.

       `min-block-size` alone leaves a 21px label vertically centred in a 44px
       box that LOOKS right and still fails, because the accessible name's box
       is what an assistive pointer targets; the flex centring above is what
       makes the whole 44px live. `min-inline-size` is separate because an
       icon-only control is square and a labelled one is not.

       44 rather than the 24 axe gates on: 24 needs the spacing exceptions to
       hold, 44 is Apple's HIG figure and makes them unnecessary, and §9 keeps
       the CI gate exactly where it is — our bar is higher than the gate's, so
       axe passing is not evidence for this rule. */
    .bk-button {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        gap: var(--bk-space-2);
        min-block-size: 2.75rem;
        min-inline-size: 2.75rem;
        padding-block: var(--bk-space-2);
        padding-inline: var(--bk-space-4);
        border: var(--bk-edge-width) solid transparent;
        border-radius: var(--bk-radius-md);
        background: none;
        color: var(--bk-text);
        font: inherit;
        /* §4.4: a control reads as a CONTROL, not as language, so the family
           is the UI face — declared AFTER `font: inherit`, which resets the
           family to the parent's and would otherwise hand a button inside
           `.prose bk-prose` (Account, Pricing) the reading face (#207). Size
           and weight still inherit through the declarations that follow. */
        font-family: var(--bk-font-ui);
        font-size: var(--bk-text-sm);
        font-weight: 600;
        line-height: var(--bk-leading-normal);
        text-align: center;
        text-decoration: none;
        cursor: pointer;
        transition: background-color var(--bk-duration-quick) var(--bk-ease),
                    border-color var(--bk-duration-quick) var(--bk-ease),
                    color var(--bk-duration-quick) var(--bk-ease);
    }

    /* ONE RING EVERYWHERE, and :focus-visible rather than :focus so a mouse click
       does not leave one behind. Offset, because a ring drawn on the border of a
       filled button is invisible against it. */
    .bk-button:focus-visible,
    .bk-field:focus-visible,
    .bk-field:focus {
        outline: 2px solid var(--bk-focus);
        outline-offset: 2px;
    }

    .bk-button:disabled,
    .bk-button[aria-disabled="true"] {
        cursor: default;
        opacity: 0.55;
    }

    /* PRIMARY — filled. One per view. The CTA fill falls back to --bk-accent
       where no --bk-accent-cta is defined (every theme but dark), so the two
       dark blocks alone carry the deeper CTA red (#141). The border is the
       fill rather than an edge: a filled button's boundary is its own colour,
       so there is no dark rim around it — and no shadow, since the soft one
       belongs to the things that lift off the page (covers, menus, sheets,
       dialogs), not to a control sitting on it. The hover rule repeats
       the border because the fill changes there and a rim of the resting
       colour would reappear around it otherwise. */
    .bk-button-primary {
        background: var(--bk-accent-cta, var(--bk-accent));
        border-color: var(--bk-accent-cta, var(--bk-accent));
        color: var(--bk-accent-contrast);
    }

    .bk-button-primary:hover:not(:disabled) {
        background: var(--bk-accent-cta-hover, var(--bk-accent-hover));
        border-color: var(--bk-accent-cta-hover, var(--bk-accent-hover));
    }

    /* SECONDARY — text. No border, deliberately: see the grammar above. */
    .bk-button-secondary:hover:not(:disabled) {
        background: var(--bk-surface-alt);
    }

    /* TERTIARY — neutral bordered. Never red. */
    .bk-button-tertiary {
        border-color: var(--bk-edge);
        background: var(--bk-surface-raised);
    }

    .bk-button-tertiary:hover:not(:disabled) {
        background: var(--bk-surface-alt);
    }

    /* DESTRUCTIVE — outlined, never filled. The fill is what says "proceed", and
       nothing that erases a library gets to say that. */
    .bk-button-danger {
        border-color: var(--bk-danger);
        color: var(--bk-danger);
    }

    .bk-button-danger:hover:not(:disabled) {
        background: var(--bk-danger);
        color: var(--bk-accent-contrast);
    }

    /* ══════════════════════════════════════════════════════════════════════
       .bk-field — THE PRIMITIVE PHASE 1 NAMED AND DID NOT BUILD

       Until 2026-08-28 `.bk-field` appeared in exactly one place in this file:
       inside the shared focus-ring selector, where it matched nothing. Every
       input in the app was a browser default or hand-rolled, and §2.5 finding 3
       measured what that cost — `.auth-field input` at 38px, a `<select>` at
       25-32px, a checkbox at 13px.

       `font-size` is at least 1rem and that is not a typographic preference:
       iOS Safari ZOOMS THE PAGE when a field smaller than 16px takes focus, and
       then leaves it zoomed. A reader who taps the email box on an installed
       phone and finds the app has scaled up around them has met a bug, not a
       type scale. `.auth-field input` already got this right at 1rem by
       accident of inheritance; here it is deliberate.

       `appearance: none` on the select is what makes its height ours rather
       than the platform's — a native select ignores padding on most engines and
       sits wherever the UA puts it. The arrow it removes has to come back, and
       it comes back as a background so that it costs no markup.

       ── AND THE FIRST VERSION OF THAT ARROW NEVER RENDERED. CORRECTED
          2026-08-28, phase 4, and the reason is worth keeping ──────────────

       It was an inline SVG **data: URI**, with a comment in this very block
       saying it "needs no `img-src` change because a data: URI is already
       allowed on this origin". **That was false, and the comment is the reason
       nobody checked.** `SecurityHeaderExtensions.PolicyAround` emits
       `default-src 'self'` and names no `img-src` at all, so a `data:` image
       falls back to the floor and is blocked. Chromium says so in as many
       words: *"Loading the image 'data:image/svg+xml,…' violates the following
       Content Security Policy directive: default-src 'self'. Note that
       'img-src' was not explicitly set."*

       It went unnoticed for a day because **nothing wore `select.bk-field`**
       until the library's sort control adopted it — the rule was written, the
       arrow was blocked, and there was no select on any screen to see it
       missing on. Phase 4 is what made it visible, in the shape of a select
       with no arrow.

       THE FIX IS TWO GRADIENTS, NOT A WIDER POLICY. A CSS gradient is painted
       rather than fetched, so it is not `img-src`'s business and there is
       nothing to allow; adding `img-src 'self' data:` to buy back one glyph
       would be widening the policy for decoration, which is the trade §9's
       "no CSP loosening" exists to refuse. Two 6px linear-gradient triangles
       sitting side by side ARE a chevron, and the colour is a token rather than
       a hex baked into a URI — which the data URI could not be, and which is
       why its `%236b6459` was a literal in a file whose first rule is that
       colours are tokens.

       The two `background-position` values are PHYSICAL (`right`), as the
       original's were: there is no logical `background-position` keyword, so
       this joins `--bk-safe-*` as a place RTL would have to be picked up, and
       RTL is a non-goal (§9.0). */
    .bk-field {
        display: block;
        inline-size: 100%;
        min-block-size: 2.75rem;
        padding-block: var(--bk-space-2);
        padding-inline: var(--bk-space-3);
        border: var(--bk-edge-width) solid var(--bk-edge);
        border-radius: var(--bk-radius-sm);
        background: var(--bk-surface-raised);
        color: var(--bk-text);
        font: inherit;
        /* The UI face, for the same §4.4 reason `.bk-button` names it above
           (#207): a field is a control, and `font: inherit` alone hands one
           inside a prose container the reading face. */
        font-family: var(--bk-font-ui);
        font-size: var(--bk-text-base);
        line-height: var(--bk-leading-normal);
    }

    .bk-field:disabled {
        cursor: default;
        opacity: 0.55;
    }

    select.bk-field {
        appearance: none;
        /* Room for the arrow, on the END side, so an RTL document moves it
           with the text rather than leaving it stranded (§9.0). */
        padding-inline-end: var(--bk-space-7);
        background-image:
            linear-gradient(45deg, transparent 50%, var(--bk-muted) 50%),
            linear-gradient(135deg, var(--bk-muted) 50%, transparent 50%);
        background-repeat: no-repeat, no-repeat;
        background-position:
            right 1.125rem center,
            right 0.75rem center;
        background-size: 0.375rem 0.375rem, 0.375rem 0.375rem;
    }

    textarea.bk-field {
        min-block-size: 6rem;
        resize: vertical;
    }

    /* ── .bk-check — BECAUSE §5.4'S OWN MECHANISM DOES NOT WORK HERE ────────

       §5.4 says a visually small control gets a large TARGET rather than a
       large appearance, and prescribes "an `::after` pseudo-element with
       negative insets" to grow the hit area without moving the layout. That
       trick is right for a `<button>` and IMPOSSIBLE for a checkbox: `<input>`
       is a replaced element and generates no pseudo-element boxes in any
       engine, so `.auth-check input::after` matches and paints nothing. The
       plan's mechanism was written against the one control it named and does
       not work on it.

       So the ROW is the target. The container carries the 44px minimum and the
       LABEL is stretched to fill it, which is what makes the target real: a
       `<label for>` toggles its input from anywhere inside itself, so a 44px
       label is a 44px hit area without the markup having to nest. The box stays
       1.25rem, which is what a checkbox should look like — §5.4's "a large
       target, not a large appearance", arrived at by a different route than the
       one it prescribed. */
    .bk-check {
        display: flex;
        align-items: center;
        gap: var(--bk-space-3);
        min-block-size: 2.75rem;
    }

    .bk-check input {
        flex: 0 0 auto;
        inline-size: 1.25rem;
        block-size: 1.25rem;
        margin: 0;
        accent-color: var(--bk-accent);
        cursor: pointer;
    }

    .bk-check label {
        flex: 1 1 auto;
        display: flex;
        align-items: center;
        min-block-size: 2.75rem;
        cursor: pointer;
    }

    /* ── .bk-panel — A SURFACE THAT FLOATS ─────────────────────────────────

       Specified in phase 1, never built, and phase 4 is blocked on it: the
       library's card-action consolidation IS a panel with a menu in it. The
       border is the primary edge — that is this design's character — and the
       panel itself is flat: the soft shadow belongs to the things that LIFT —
       a cover, a menu, a sheet, a dialog — so a rule that needs the lift adds
       it where the thing actually floats. */
    .bk-panel {
        background: var(--bk-surface-raised);
        color: var(--bk-text);
        border: var(--bk-edge-width) solid var(--bk-edge);
        border-radius: var(--bk-radius-lg);
    }

    /* `bk-panel-static` (Account's cards) marks a section that does not float
       and now marks it only: a panel is FLAT whatever its class list says,
       because the soft shadow belongs to the things that lift — covers, menus,
       sheets and dialogs — not to a section sitting in the flow. The class
       stays in the markup and is harmless. */

    /* ── .bk-menu — THE OTHER HALF OF PHASE 4 ──────────────────────────────

       An overflow menu: a `.bk-panel` whose contents are a list of actions. It
       is deliberately NOT a `<select>` and not a dialog — it is the pattern the
       book card needs, where four stacked controls become one control and a
       list (§8 phase 4).

       Every item is a full-width 44px row rather than a padded inline button,
       because in a menu the ROW is the target and a reader aiming with a thumb
       should not be able to miss between two of them. `text-align: start`, not
       `center`: a list of actions is read down its leading edge.

       No `position` here on purpose. Where a menu is anchored is the business
       of the component that opens one, and a primitive that positioned itself
       would have to guess which corner it is opening from. */
    .bk-menu {
        display: flex;
        flex-direction: column;
        min-inline-size: 12rem;
        padding-block: var(--bk-space-2);
        overflow: hidden;
    }

    .bk-menu-item {
        display: flex;
        align-items: center;
        gap: var(--bk-space-2);
        min-block-size: 2.75rem;
        padding-block: var(--bk-space-2);
        padding-inline: var(--bk-space-4);
        border: 0;
        background: none;
        color: var(--bk-text);
        font: inherit;
        /* The UI face, as on `.bk-button` above (#207): a menu row is a
           control, and `font: inherit` alone re-voices one inside prose. */
        font-family: var(--bk-font-ui);
        font-size: var(--bk-text-sm);
        text-align: start;
        text-decoration: none;
        cursor: pointer;
    }

    .bk-menu-item:hover:not(:disabled) {
        background: var(--bk-surface-alt);
    }

    .bk-menu-item:focus-visible {
        outline: 2px solid var(--bk-focus);
        /* INSET, not offset. A ring drawn outside a full-bleed row is clipped
           by the panel's own `overflow: hidden`, which is how a focused item
           ends up looking unfocused at the top and bottom of a menu. */
        outline-offset: -2px;
    }

    .bk-menu-item:disabled {
        cursor: default;
        opacity: 0.55;
    }

    /* A destructive item is outlined red as a BUTTON and cannot be here (§4.5:
       outlined red means destructive and is never filled). In a menu there is
       no outline to draw, so the grammar falls back to colour plus language —
       which §4.0 says colour alone cannot carry, hence the rule that a
       destructive menu item's label always names what it destroys. */
    .bk-menu-item-danger {
        color: var(--bk-danger);
    }

    .bk-menu-item-danger:hover:not(:disabled) {
        background: var(--bk-danger);
        color: var(--bk-accent-contrast);
    }

    /* ── .bk-menu-popup — A `.bk-menu` THAT FLOATS (phase 2B) ──────────────

       `.bk-menu` above says nothing about position, on purpose: "where a menu
       is anchored is the business of the component that opens one". This is
       that business, written once, for the case where the component opening it
       is Radzen.

       IT PAINTS NOTHING, BECAUSE THE SEAM IS ALREADY DOING THAT. Radzen's
       `RadzenPopup` renders a bare <div> — no theme class, no inline styles —
       and its JS then writes `display: block`, `top`, `left` and `z-index` as
       INLINE styles when it opens one. So the app has to supply the two things
       that make those writes mean anything: the element must start hidden, and
       it must be positioned. Everything the reader sees is `.bk-panel` and
       `.bk-menu`, applied as classes beside this one.

       `display: none` RATHER THAN `visibility` OR AN `[hidden]` ATTRIBUTE,
       because Radzen's own toggle asks `popup.style.display == 'block'` to
       decide whether it is open. Any other mechanism would leave the library
       and the stylesheet disagreeing about the state of the same element.

       THE SIZE IS THE PHONE'S, IN rem AND NOT IN VIEWPORT UNITS. 16rem is wide
       enough for a "Send to Kindle" and a shelf name; 20rem still clears a
       360px screen with Radzen's smart positioning intact. A `100vi` cap would
       have been the obvious way to say the second half, and is exactly what
       §5.2 rule 1 rules out — the inline viewport unit is `100vw`'s logical
       twin and counts the scrollbar gutter the same way, which is how a "full
       width" element ends up causing the horizontal overflow it was written to
       prevent.

       AND IT SCROLLS RATHER THAN RUNNING OFF THE PAGE, WHICH IS A SMALLER
       NUMBER THAN IT LOOKS AND DELIBERATELY SO. A popup is measured and
       positioned ONCE, at the moment it opens; a disclosure inside it that
       expands afterwards — the download list and its conversion offers — grows
       downwards with nothing left to re-decide the placement. Measured at 390 x
       844: a menu opened on the first row starts 394px down and the bottom bar
       begins at 724 on the shortest phone this app targets, so anything taller
       than ~290px puts its last item — which is Delete — under that bar. Capping
       the panel BELOW that height turns the failure into a scroll, which is
       bounded and predictable, instead of into a Delete nobody can reach. `svb`
       is the small-viewport logical height, the one that does not change under
       a retracting browser toolbar. Scrolling inside the panel does not dismiss
       it — Radzen closes a popup when an ANCESTOR scrolls, and a popup is not
       its own ancestor.

       ONE THING THIS PRIMITIVE CANNOT FIX, AND THE CALLER MUST: a popup is
       absolutely positioned and Radzen writes its coordinates in PAGE space, so
       it must not be rendered inside an element with layout containment —
       which every `container-type` establishes. LibraryView.razor carries the
       measurement that makes that concrete. */
    .bk-menu-popup {
        display: none;
        position: absolute;
        inline-size: max-content;
        min-inline-size: 16rem;
        max-inline-size: 20rem;
        max-block-size: min(55svb, 18rem);
        overflow-y: auto;
        /* The lift, earned HERE rather than on the panel: a menu that floats
           casts the soft shadow — as covers, sheets and dialogs do — while
           `.bk-panel` itself is flat. */
        box-shadow: var(--bk-shadow-2);
    }

    /* ══════════════════════════════════════════════════════════════════════
       THE TITLE PAGE'S PRIMITIVES (redesign A — the title-page style)

       The homepage built this design's furniture by hand, in its own scoped
       sheet: the double rule, the small-caps eyebrow, the display h1 with its
       italic accent, the drop-cap lede, the colophon strip (Home.razor.css).
       Every page is now to wear the same design, and a copy of those rules
       per page is one chance to drift per page per rule — which is how the
       app came to have seven buttons before `.bk-button`. These are the
       shared definitions; Home keeps its tuned copies (it is the reference,
       and its display scale is its own), and the pages adopt these.

       MOBILE-FIRST like everything here: the unqualified values are the
       phone's and the wider screens arrive through §5.3's two named
       breakpoints — 48rem for the strip, 64rem for the lede's measure.
       COLOURS AND FONTS ARE TOKENS AND NOTHING ELSE — every ink is a
       `var(--bk-*)`, so the set follows light, dark and sepia without
       knowing they exist (TitlePagePrimitiveTests pins that, and the
       structural shapes each primitive owes). */

    /* ── .bk-title-rule — THE DOUBLE RULE ─────────────────────────────────

       Two 1px ink lines 3px apart. `border-block` on a 5px box under the
       global border-box reset is exactly that — 1 + 3 + 1 — and the ink is
       the text colour because a title page's rules are printed in the same
       ink as its type (the reason Home's hero rule is not a border grey). */
    .bk-title-rule {
        block-size: 5px;
        /* THE TITLE STACK'S RHYTHM lives on the primitives themselves, so a
           page that stacks rule, eyebrow, title and lede as bare siblings is
           spaced like the homepage without a wrapper of its own (its hero
           gaps are space-3 and space-4). A component that needs a different
           rhythm overrides the block-end margins in its scoped sheet. */
        margin-block-end: var(--bk-space-4);
        border-block: 1px solid var(--bk-text);
    }

    /* ── THE LABEL VOICE — ONE LIST, EVERY WEARER ──────────────────────────

       The eyebrow above a title, the section label at a section's head, the
       colophon cell's label, and the shell footer's tagline and links: four
       wearers of ONE voice. REAL small caps (the title page's eyebrow, not
       the uppercase stand-in), letter-spaced because small caps at 0.8rem
       without tracking set tight and read as an abbreviation, muted because
       a label names what follows rather than speaking itself.

       ONE selector list and not four copies of these declarations. A voice
       copied four times is the seven-buttons failure one step earlier — every
       wearer free to drift a step from the others — and the drift is what
       this redesign came to remove. The list also resets what a heading or a
       <dd> drags in with it: `margin: 0` and `font-weight: 400`, so an
       `<h2 class="bk-section-label">` sets as a label and not as the §4.4
       heading it happens to be.

       The voice has no face of its own; each label names the face it stands
       in. The plain labels take the UI face — named rather than inherited,
       the same argument §4.4 makes for a control's own font-family, because
       an h2 would otherwise keep the reading face and set a label in Literata
       — and the colophon's label takes the STRIP's face instead, which is how
       the homepage's colophon prints its labels (below). */
    .bk-eyebrow,
    .bk-section-label,
    .bk-colophon-label,
    .shell-footer > span,
    .shell-footer-links a {
        margin: 0;
        font-size: var(--bk-text-xs);
        font-weight: 400;
        font-variant: all-small-caps;
        letter-spacing: 0.14em;
        color: var(--bk-muted);
    }

    .bk-eyebrow,
    .bk-section-label,
    .shell-footer > span,
    .shell-footer-links a {
        font-family: var(--bk-font-ui);
    }

    /* `.bk-eyebrow-accent` is the one word inside a label that DOES speak —
       the "Alpha" of "Alpha · Your own ebooks and comics" (Home) — oxblood
       (--bk-accent) beside the muted words, never colour alone. */
    .bk-eyebrow {
        margin-block-end: var(--bk-space-2);
    }

    .bk-eyebrow-accent {
        font-weight: 700;
        color: var(--bk-accent);
    }

    /* ── .bk-page-title — THE SERIF h1, AT A PAGE'S SCALE ─────────────────

       The homepage's display h1 runs a clamp from 2.75rem up to 5.75rem; a
       page title is not a landing page's flourish and takes the scale's top
       step instead — --bk-text-2xl, 2.441rem — which is below the homepage's
       FLOOR at every width. That relationship is the whole spec ("a page
       scale smaller than the homepage's") and TitlePagePrimitiveTests pins
       it numerically rather than by eye.

       An `em` inside is Home's `.home-h1-em` grammar minus the forced line
       break: italic, regular weight, the accent — the title page's second
       line, flowed instead of set apart. */
    .bk-page-title {
        margin: 0 0 var(--bk-space-4);
        font-family: var(--bk-font-reading);
        font-size: var(--bk-text-2xl);
        font-weight: 600;
        line-height: var(--bk-leading-tight);
        letter-spacing: -0.02em;
        text-wrap: balance;
    }

    .bk-page-title em {
        font-style: italic;
        font-weight: 400;
        color: var(--bk-accent);
    }

    /* ── .bk-lede — THE OPENING PARAGRAPH, SET AS BOOK TYPE ───────────────

       The reading face at 20px — the mockup's 20px lead mapped onto
       --bk-text-md, as Home's hero lede maps it — in the ink rather than the
       muted grey: this is the opening paragraph of the argument, not a
       caption.

       THE DROP CAP is the title page's decorated initial: floated and padded
       inline-end (and nudged off the cap line) rather than margin-ed, so the
       first lines wrap around it and the lede opens the way a chapter does.
       Oxblood at 3em, and it PAINTS nothing — so the one-filled-control rule
       is untouched by it. */
    .bk-lede {
        margin: 0;
        font-family: var(--bk-font-reading);
        font-size: var(--bk-text-md);
        line-height: 1.45;
        color: var(--bk-text);
        /* THE MEASURE, the homepage lede's own (Home's `.home-lede`): the lede
           is the one paragraph a visitor certainly reads, so it takes 34rem
           rather than the workspace's full width — §7's 60-70 characters,
           applied where it matters most — and `text-wrap: pretty` beside the
           `margin: 0` that removes the UA paragraph margins. Without all
           three a page lede runs ~120 characters a line at 1440 and sits in
           the browser's own paragraph spacing. */
        max-inline-size: 34rem;
        text-wrap: pretty;
    }

    .bk-lede::first-letter {
        float: left;
        font-size: 3em;
        line-height: 0.85;
        font-weight: 600;
        color: var(--bk-accent);
        padding-block-start: 5px;
        padding-inline-end: 8px;
    }

    /* ── .bk-section-label — A LABEL WITH A HAIRLINE BENEATH ──────────────

       The label voice above standing at the head of a section — its size,
       small caps, tracking, muted ink and resets arrive from the one list —
       closed underneath by the SEPARATOR hairline: the seam rule the shell's
       header and footer wear, applied to a label. Not a box and not a card:
       the restraint rule is "thin rules instead of boxes" and this is the
       thin rule. --bk-border because the hairline is the separator; a
       control's boundary would be --bk-edge (ColourRuleTests), and this is
       not a control. */
    .bk-section-label {
        padding-block-end: var(--bk-space-2);
        border-block-end: 1px solid var(--bk-border);
    }

    /* ── .bk-rule — ONE HAIRLINE ──────────────────────────────────────────

       The separator on its own: a row between rows, a band between bands.
       Zero block-size and no margins so the element IS the line — which also
       makes it correct on an `<hr>`, whose UA margins and borders this zeroes
       first. */
    .bk-rule {
        block-size: 0;
        margin: 0;
        border: 0;
        border-block-start: 1px solid var(--bk-border);
    }

    /* ── .bk-colophon — THE STRIP A TITLE PAGE PRINTS ─────────────────────

       An OPEN strip, as the homepage prints it: a rule above, a rule below,
       no side edges — the particulars between. Labels small caps
       (`.bk-colophon-label`, the label voice above), values in the reading
       face (`.bk-colophon-value`), and the whole strip in the reading face so
       the labels are serif small caps as on the homepage.

       ANY COUNT OF CELLS, TWO TO SIX, AND NO GEOMETRY TO GO WRONG. A grid
       that wraps cells into rows cannot know where a short last row ends,
       so it either draws rules where no cell is or misses one where a cell
       is. So the strip never wraps: on a phone the cells are one column,
       each after the first opening with a hairline; from 48rem they stand in
       ONE row, each after the first opening with a vertical rule. A rule is
       always a cell's own leading edge, between two cells that exist. */
    .bk-colophon {
        display: grid;
        grid-template-columns: minmax(0, 1fr);
        font-family: var(--bk-font-reading);
        border-block: 1px solid var(--bk-border);
    }

    .bk-colophon-cell {
        display: flex;
        flex-direction: column;
        gap: var(--bk-space-1);
        padding: var(--bk-space-3) 0;
    }

    .bk-colophon-cell + .bk-colophon-cell {
        border-block-start: 1px solid var(--bk-border);
    }

    .bk-colophon-value {
        /* --bk-text-sm and not the base step, as Home's value: a format list
           is the widest value a strip carries and Literata is wide — at 16px
           "EPUB · PDF · MOBI · AZW3 · CBZ · CBR" breaks its last name onto a
           line of its own inside a phone's cell. `margin: 0` because a page
           may wrap the value in a <dd>, whose 40px inline margin is not this
           primitive's to keep. */
        margin: 0;
        font-family: var(--bk-font-reading);
        font-size: var(--bk-text-sm);
        line-height: var(--bk-leading-normal);
        color: var(--bk-text);
        text-wrap: balance;
    }

    @media (min-width: 48rem) {
        /* One row, as many equal columns as there are cells. */
        .bk-colophon {
            grid-auto-flow: column;
            grid-auto-columns: minmax(0, 1fr);
            grid-template-columns: none;
        }

        .bk-colophon-cell {
            padding: var(--bk-space-5) var(--bk-space-4);
        }

        .bk-colophon-cell:first-child {
            padding-inline-start: 0;
        }

        .bk-colophon-cell + .bk-colophon-cell {
            border-block-start: 0;
            border-inline-start: 1px solid var(--bk-border);
        }
    }

    /* ── THE SHELL SPEAKS THE TITLE PAGE'S VOICE ──────────────────────────

       The wordmark and the footer are the frame every page is printed in, so
       they wear the same two voices the primitives above define: the serif
       for the name, the small-caps label for the furniture around it. The
       footer's tagline and links join the label voice in its ONE list above —
       size, small caps, tracking, muted ink and the resets together — so what
       is left here is the wordmark.

       ALL OF IT STYLED HERE AND NONE OF IT IN THE MARKUP: MainLayout's and
       NavMenu's markup is frozen — its link semantics and 44px hit areas are
       behaviour — and MainLayout.razor.css is Blazor's scoped bundle,
       unlayered and therefore the winner against this layered file for any
       property the two disagree on (the same cascade argument this file's
       header makes about Radzen, from the other side). So these rules take
       only what the scoped rules do NOT declare: `.brand`'s face beside its
       scoped 700 weight and 1.25rem size, and the footer's label voice
       beside its scoped muted ink and its two 44px targets. The links stay
       links and stay 44 × 44 precisely because the declarations that say so
       live in the scoped sheet and nothing here can outrank them. */

    /* The wordmark is the name of the place and sets like a title page sets
       its imprint: the reading face, bold. It is also the way home
       (MainLayout), so its size and its target stay exactly where they are. */
    .brand {
        font-family: var(--bk-font-reading);
        font-weight: 700;
    }

    /* ══════════════════════════════════════════════════════════════════════
       WHERE NAVIGATION LIVES (DESIGN-PLAN §5.3, phase 2 item d)

       Measured 2026-08-27 and again on the 28th: the brand and four links need
       408px, and 473px with the session corner, so on a 390px phone the header
       overflowed by 166px, "Add a book" wrapped onto three lines and "Account"
       was sliced off the right edge. There was no narrow-viewport nav treatment
       of any kind — no wrap, no menu, no bar — and `.app-nav`'s `min-width: 0`
       let the NAV shrink while its `display: block` children would not, so the
       overflow was structural rather than a missing breakpoint.

       THE FIX IS A PLACE, NOT A SIZE. The four destinations move to a bar
       under the thumb; the header keeps the brand and the session corner. It is
       ONE component in ONE DOM order (NavMenu.razor) and the arrangement is
       entirely here: fixed to the bottom below 48rem, static in the header
       above it. Nothing is duplicated, so no destination appears twice in the
       accessibility tree.

       WHY THIS LIVES IN app.css AND NOT IN NavMenu.razor.css. The bar's box is
       the SHELL's, not the component's: `.shell-footer` in MainLayout has to be
       padded clear of it, and the two live in different assemblies' scoped
       stylesheets, which cannot see each other. That is the same argument the
       page modes make for being global — this is the layout system's
       containment, and it is also what keeps `var(--bk-safe-*)` inside the
       four-file allowlist §5.2 rule 5 pins rather than growing it for a
       component.

       WHY IT IS `position: fixed` AND ENTITLED TO THE INSET. A bar pinned to
       the bottom of the viewport IS the claim that its edge is the screen's
       edge, which is exactly the entitlement §5.2 rule 5 grants
       #blazor-error-ui. Installed on a notched phone this bar sits IN the
       home-indicator band by definition, so `--bk-safe-bottom` here is not a
       refinement — without it the labels are under a system gesture strip, the
       defect §2.5 finding 2 measured on the reader's page-turn controls.

       THE READER HAS NO BAR, AND NOT BECAUSE ANYTHING HIDES IT. `Reader.razor`
       declares `@layout ImmersiveLayout`, and this element is in MainLayout's
       markup, so on the one screen that wants the bottom strip for Previous and
       Next the bar does not exist. That is a mechanism rather than a
       convention (§5.7) — there is no `display: none` to get wrong, no z-index
       race with the reader's own controls, and LayoutModeTests already fails
       the build if an immersive route renders shell chrome. */
    .app-nav {
        position: fixed;
        inset-block-end: 0;
        inset-inline: 0;
        /* Above the page, below #blazor-error-ui (1000): when the connection
           drops, the thing a reader needs to read is the banner. */
        z-index: 20;
        display: flex;
        align-items: stretch;
        gap: var(--bk-space-1);
        min-block-size: calc(var(--bk-nav-block-size) + var(--bk-safe-bottom));
        padding-block: var(--bk-space-1) calc(var(--bk-space-1) + var(--bk-safe-bottom));
        padding-inline: max(var(--bk-space-1), var(--bk-safe-left))
                        max(var(--bk-space-1), var(--bk-safe-right));
        background-color: var(--bk-surface-alt);
        /* The bar's top edge is a seam between the page and the navigation —
           another separator, not a control boundary, so it is the hairline. */
        border-block-start: var(--bk-edge-width) solid var(--bk-border);
    }

    /* THE LOOK IS SHARED BY BOTH GROUPS, THE ARRANGEMENT IS NOT. `.nav-link` is
       worn by the four destinations AND by Sign in in the session corner, and
       they should read as the same kind of thing because they are
       — a place you can go. Only the arrangement differs, and the arrangement
       rules below are scoped to `.app-nav` for exactly that reason.

       This was learned by breaking it: scoping the whole treatment to
       `.app-nav` left Sign in and Register as 18px browser-default links at
       every width, which is the orphan-class failure of §2.3 committed fresh
       while fixing something else.

       Since #140 the signed-out bar's "Create a free account" wears the class
       too — for the slot and the one-line rule. Its INK is the button
       primitive's, which is what the `:not(.bk-button-primary)` guard below
       is for (#182). */
    .nav-link {
        text-decoration: none;
        /* One line, always. "Add a book" wrapping to three lines inside a
           fixed header is the defect this item exists to fix; wrapping it
           inside a bar slot would be the same defect in a nicer place. */
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    /* THE INK IS WHERE THE SHARED GRAMMAR STOPS. #182 measured this colour on
       "Create a free account" at 1.6:1 against the accent fill: `.nav-link`
       and `.bk-button-primary` are equal-specificity, and this rule's LATER
       source position is what won. The `:not` guard is the fix rather than a
       specificity escalation, because it says the true thing — a nav-link
       that IS a filled primary takes its ink from the primitive, at rest and
       in both states below — and it cannot change how the other links read:
       every one of them is a `bk-button-secondary`, so the guard matches them
       unconditionally. ControlPrimitiveTests fails the build if a colour on
       `.nav-link` without this guard comes back. */
    .nav-link:not(.bk-button-primary) {
        color: var(--bk-muted);
    }

    .nav-link:hover:not(.bk-button-primary) {
        color: var(--bk-text);
        background-color: var(--bk-surface);
    }

    /* Equal slots, and `min-inline-size: 0` because the primitive sets 2.75rem
       and four of those plus gaps would not divide a 360px screen. The BLOCK
       minimum is what matters for a thumb and it is untouched; the inline one
       is a floor for an icon-only control, which these are not.

       COLUMN, because the phone bar's slot is an ICON OVER ITS LABEL (the
       review's "icon + label phone tab bar"): NavMenu renders a `.nav-icon`
       svg before each label and this stacks the two. It costs nothing at the
       header width — the icon is hidden there, so a column with one item in
       it is a row with one item in it. */
    .app-nav .nav-link {
        flex: 1 1 0;
        flex-direction: column;
        min-inline-size: 0;
        padding-inline: var(--bk-space-1);
        /* Tight enough that icon + gap + label + the 2px rule fit the 4.25rem
           slot the footer clears (MainLayout reserves exactly that); the
           .bk-button defaults (space-2 padding and gap) grow it to ~78px. */
        padding-block: var(--bk-space-1);
        gap: 0.125rem;
        font-size: var(--bk-text-xs);
        text-align: center;
        /* The containing block for the active marker below (phase 2 T1). */
        position: relative;
    }

    /* The tab bar's icon, and the one size it wears. `flex: 0 0 auto` because
       a stacked slot must not squash its icon before it squashes its label,
       and 1.375rem because that is the mark that still reads at 22px beside a
       0.8rem label in a 4.25rem slot. It is decorative by contract — NavMenu
       renders it `aria-hidden`, the label is the name — so nothing here may be
       the only carrier of a fact. */
    .app-nav .nav-icon {
        flex: 0 0 auto;
        inline-size: 1.375rem;
        block-size: 1.375rem;
    }

    /* THE ACTIVE STATE IS NOT COLOUR ALONE, AND ITS MARKER IS A RULE — NOT A
       BLOB (phase 2 T1). The ink is the strongest one there is: --bk-text
       where the inactive links are --bk-muted, at the weight the primitive
       already gives a control. The 2px accent rule is what carries "here" to a
       reader who cannot separate those two inks — WCAG 1.4.1, and the same
       reasoning §4.0 uses for the destructive grammar, where form rather than
       hue does the work. NavLink also renders `aria-current="page"`, so the
       fact reaches assistive technology without depending on either.

       NO TINT, deliberately. This used to take `--bk-accent-subtle` as its
       fill — the pale pink the palette review asked to be removed outright,
       now a neutral wash other call sites are free to use. A filled tab beside
       an unfilled one also borrowed the fill that says "proceed" for a state
       that is only a position in a list.

       THE MARKER IS A PSEUDO-ELEMENT, NOT AN inset box-shadow. The shadow
       follows the chip's border-radius, so its ends are the rounded blob this
       replaces; and a pseudo-element alone is not enough either, because
       `.nav-link` is `overflow: hidden` for its ellipsis and that clip follows
       the radius too — it would sweep the rule's ends the same way. So the
       marker's own edge is squared and un-bordered: the two corners on that
       edge lose the primitive's radius and the 1px transparent border there
       goes with them, which also puts the rule ON the edge rather than 1px
       inside it. That is the point at desktop, where the edge is the header's
       bottom edge and the rule must sit flush against the hairline seam.

       The rule sits on the edge nearest the CONTENT: the top of a bar slot,
       the bottom of a header link. That is one property pair flipped in the
       breakpoint below rather than a second treatment to maintain, and it
       points the same way in both — at the page it belongs to.

       The marker is on the DESTINATIONS only. It means "the page you are
       looking at is this one", which is a statement about a set of places; the
       session corner is not one, so an active rule there says "you are on the
       sign-in page" without claiming to be a position in a list.

       The guard is the same one as the ink above and for the same reason: on
       its own page a filled primary stays a filled primary, and the accent
       rule on the accent fill is invisible in light and sepia and a stray line
       in dark. */
    .nav-link.active:not(.bk-button-primary) {
        color: var(--bk-text);
        font-weight: 600;
    }

    .app-nav .nav-link.active:not(.bk-button-primary) {
        border-radius: 0 0 var(--bk-radius-md) var(--bk-radius-md);
        border-block-start-width: 0;
    }

    .app-nav .nav-link.active:not(.bk-button-primary)::after {
        content: "";
        position: absolute;
        inset-inline: 0;
        inset-block-start: 0;
        block-size: 2px;
        background-color: var(--bk-accent);
    }

    /* ── SIGNED OUT, A PHONE GETS NO BAR (phase 2 T1) ───────────────────────

       What the bar held for a stranger on a phone was "Plans" and a call to
       action every page already carries — and on the two screens with the
       most calls to action, "Plans" alone. A fixed strip spending the thumb
       zone on one secondary link is the loudest way to say "this chrome is
       not for you", so below 48rem it is not rendered at all: "Plans" is a
       footer link now (MainLayout), and each page's own call to action stays
       in the page, where it was written for. Above 48rem the pair is the
       header's and the header keeps it — the breakpoint below puts the bar
       back for exactly that.

       THE HOOK IS `:has()` ON THE SIGNED-OUT LINKS, not a data attribute
       NavMenu sets from the auth state. The signed-out branch is already
       distinguishable by the two hrefs it renders — they are the detector the
       auth and home rules have keyed on since #140 — so this reads the ONE
       copy of the fact there is, the branch the AuthorizeView actually
       rendered. A data attribute would mean plumbing the authentication state
       out of the AuthorizeView into an attribute on its own parent: a second
       copy that can disagree with the first. `:where()` zeroes the detector's
       specificity on purpose, which makes this rule as plain as `.app-nav` —
       so the `display: flex` in the ≥48rem block below outranks it by source
       order alone and the detector is written once rather than twice.

       WHICH IS WHY THIS RULE IS BEFORE THE BREAKPOINT. Same specificity on
       both halves, so the later declaration wins at ≥48rem and this one wins
       below it.

       It hides the WHOLE bar and not just its links for the reason the auth
       rule after the breakpoint spells out: hiding only the links would leave
       a fixed 4.25rem strip of empty surface-alt across the bottom of a phone
       — the complaint above with a border on it. */
    .app-nav:has(:where(.nav-link[href$="register"])) {
        display: none;
    }

    /* ── AND THE FOOTER STOPS CLEARING A BAR THAT IS NOT THERE ──────────────

       MainLayout.razor.css pads `.shell-footer`'s bottom by
       `--bk-nav-block-size` below 48rem so the last links clear the fixed bar.
       Signed out there IS no bar (the rule just above), so a visitor's phone
       carried 4.25rem of empty gap under the footer — the clearance outliving
       the thing it clears. This keys on the SAME detector — the signed-out
       branch's own register link — so signed-in phones keep the clearance and
       their bar.

       THE DECLARATION IS THE TOKEN, NOT THE PADDING, and that is the whole
       mechanism. MainLayout.razor.css is Blazor's scoped-CSS bundle:
       unlayered by construction, so its `.shell-footer` rule outranks every
       rule in this layered file whatever the specificity (the same argument
       this file's header makes about Radzen's unlayered body rules, and
       BaseLayerTests pins that there is no unlayered escape from these
       layers). Overriding `padding-block` from here would therefore need
       `!important` — but the scoped rule COMPUTES its padding from
       `var(--bk-nav-block-size)`, and a custom property declared closer to
       the footer wins by inheritance, not by cascade layer. For a visitor the
       bar has no box, so the bar's block size is zero. The safe-area inset
       below the 1rem is untouched: the footer still meets the home indicator
       whether or not a bar sits in it. */
    .shell:has(.app-nav :where(.nav-link[href$="register"])) .shell-footer {
        --bk-nav-block-size: 0rem;
    }

    /* ── ABOVE 48rem THE LINKS GO BACK TO THE HEADER ───────────────────────
       Where a pointer is. §5.3's first named breakpoint, and the width at
       which the four links first fit beside the brand and the session corner:
       measured 473px of content, so 768px is not a tight fit but a
       comfortable one, and 48rem is the number the app already has rather
       than a third one invented for this. */
    @media (min-width: 48rem) {
        .app-nav {
            position: static;
            z-index: auto;
            min-block-size: 0;
            padding: 0;
            gap: var(--bk-space-1);
            background: none;
            border: 0;
            /* THE NAV STRETCHES TO THE HEADER'S FULL HEIGHT; THE BUTTONS DO
               NOT (phase 2 review, loop 1). `align-self: stretch` here puts
               the nav's box flush with the header's content edge, and
               `.app-nav` is align-items: stretch so each link's box reaches
               the same seam — which is still what puts the active rule flush
               with the header's bottom hairline: the marker rides the link's
               own bottom edge (the ::after below), and a link box that stops
               at its own 44px minimum would leave the rule six pixels above
               the seam no matter where it is drawn.

               What the stretch may NOT do is stretch the PAINTED box. A
               filled "Create a free account" grown to the full header height
               is an accent slab that no longer lines up with the session
               corner's 44px "Sign in", and a hover wash filling the same 56px
               turns a text link into a header-height bar. So the stretch buys
               HIT AREA and MARKER PLACEMENT only, and the rules below take the
               paint back out of it: the primary keeps its own 44px box,
               vertically centred, and a secondary link's hover paints no wash
               (only its ink changes), with its focus ring drawn inside the box. */
            align-self: stretch;
            /* AND `display: flex` IS LOAD-BEARING HERE, not a restatement of
               the base rule: it is what puts the signed-out bar BACK at this
               width. Below 48rem it is not rendered at all (the rule under
               "SIGNED OUT ON A PHONE"), and that rule is specificity-zeroed
               with :where() so this plain declaration outranks it — one
               detector, two halves of one arrangement. */
            display: flex;
        }

        .app-nav .nav-link {
            flex: 0 0 auto;
            padding-inline: var(--bk-space-3);
            padding-block: var(--bk-space-2);
            gap: var(--bk-space-2);
            font-size: var(--bk-text-sm);
        }

        /* THE PRIMARY STAYS A BUTTON, NOT A SLAB. It is a normal 44px
           `.bk-button-primary`, vertically centred in the same header row the
           session corner's 44px "Sign in" is — the fill is its boundary, so a
           stretched one is a full-height accent slab and the two halves of the
           header stop lining up. The stretch above is for the secondaries'
           hit area and the active rule's flush edge; a filled control has a
           painted box and keeps it at the primitive's size. */
        .app-nav .nav-link.bk-button-primary {
            align-self: center;
        }

        /* THE HOVER IS INK, NOT A WASH. A secondary link's hit area is the full
           header height (above), and any wash painted on it is either a
           header-height bar or — clipped to the label — a tight highlighter
           block with no breathing room. So on this width a hover changes the
           ink from muted to text and paints no background at all. The
           selector names `:hover` so it overrides exactly the two hover
           washes — `.bk-button-secondary:hover` and `.nav-link:hover` — and
           nothing at rest.

           THE FOCUS RING GOES INSIDE. The stretched link's top edge is the
           sticky header's top edge — y = 0 — so the primitive's ring, drawn
           2px OUTSIDE the box, loses its top to the viewport and spills its
           bottom over the hairline. An inset offset keeps the whole ring
           visible inside the link. */
        .app-nav .nav-link:not(.bk-button-primary):hover {
            background: transparent;
        }

        .app-nav .nav-link:not(.bk-button-primary):focus-visible {
            outline-offset: calc(-1 * var(--bk-space-1));
        }

        /* The icon is a phone affordance — a label under a thumb needs one, a
           label beside a pointer does not, and §9 rejects an icon system for
           its own sake. Hidden rather than removed so one DOM serves both
           arrangements (the NavMenu rule this whole section exists for). */
        .app-nav .nav-icon {
            display: none;
        }

        /* The same treatment, on the edge nearest the content — which at this
           width is below the link rather than above it, flush with the
           header's bottom edge (see the stretch above). The squared, un-
           bordered edge flips with it: the chip is round again at the top and
           square where the rule rides. */
        .app-nav .nav-link.active:not(.bk-button-primary) {
            border-radius: var(--bk-radius-md) var(--bk-radius-md) 0 0;
            border-block-start-width: var(--bk-edge-width);
            border-block-end-width: 0;
        }

        .app-nav .nav-link.active:not(.bk-button-primary)::after {
            inset-block-start: auto;
            inset-block-end: 0;
        }
    }

    /* ── THE AUTH PAGES TAKE THE BAR's SIGNED-OUT CTAs AWAY ────────────────

       "Create a free account" in the bar of the register page is a link to the
       page you are already on, and "Plans" beside it is a third and fourth
       call to action on the two screens with the most of them — the review's
       "hide the sticky bar on /login and /register". NavMenu marks the bar
       `data-page-kind="auth"` on those two routes (T5's half of the hook);
       this is T1's. The two links are matched by their destinations rather
       than by a class because they share `nav-link` with every real
       destination and that is deliberate — the grammar is shared by design.

       AND THE BAR GOES WITH THEM WHEN THOSE WERE ALL IT HELD. Signed out,
       "Plans" and "Create a free account" ARE the bar's whole contents, so
       hiding only the links would leave a fixed 4.25rem strip of empty
       surface-alt across the bottom of a phone — the complaint above with a
       border on it. The `:has()` guard means a signed-in reader who lands on
       an auth page keeps their bar and its four destinations. */
    .app-nav[data-page-kind="auth"] .nav-link[href$="pricing"],
    .app-nav[data-page-kind="auth"] .nav-link[href$="register"] {
        display: none;
    }

    .app-nav[data-page-kind="auth"]:not(:has(.nav-link:not([href$="pricing"]):not([href$="register"]))) {
        display: none;
    }

    /* THE HOME PAGE'S HERO IS ITS ONE FILLED ACTION. NavMenu marks the bar
       `data-page-kind="home"` on "/" (T5), and the bar's filled "Create a free
       account" steps aside there so the hero's is the screen's only fill —
       one filled control per screen. "Plans" stays: it is navigation, not a
       second call to action. Every other signed-out page keeps the bar's CTA as
       its filled control, which is why page bodies there use outlined offers. */
    .app-nav[data-page-kind="home"] .nav-link[href$="register"] {
        display: none;
    }

    /* ── THE ORPHANS ──────────────────────────────────────────────────────────

       `.button`, `.button-danger` and `.form-actions` are written in Account.razor
       and Pricing.razor and matched NO rule in any stylesheet — the pages guessed
       a shared class name and the guess silently failed. Aliased onto the
       primitives rather than renamed in markup, so this commit changes appearance
       without touching behaviour or tests; the markup can move to `.bk-button`
       whenever somebody is editing those files anyway.

       `.auth-alert` and `.auth-form` are a different failure with the same look:
       they ARE defined, but only under `.auth-card ::deep` in AuthCard.razor.css,
       and Account.razor has no `.auth-card` ancestor — so they never applied
       there either. Defined globally here; AuthCard's scoped rules still win
       inside a card, which is where they were tuned. */
    /* The orphans get the 44px minimum too. They are `.bk-button` under another
       name on Account and Pricing, and a primitive that enforces a rule its own
       alias does not is a rule with a hole in it — the hole being the page that
       carries "Delete my account permanently". */
    .button {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        gap: var(--bk-space-2);
        min-block-size: 2.75rem;
        min-inline-size: 2.75rem;
        padding-block: var(--bk-space-2);
        padding-inline: var(--bk-space-4);
        border: var(--bk-edge-width) solid var(--bk-edge);
        border-radius: var(--bk-radius-md);
        background: none;
        color: var(--bk-text);
        font: inherit;
        /* The UI face, as on `.bk-button` above (#207): the alias lives on
           the very prose pages (Account, Pricing) whose reading face made
           the bug visible. */
        font-family: var(--bk-font-ui);
        font-size: var(--bk-text-sm);
        cursor: pointer;
        transition: background-color var(--bk-duration-quick) var(--bk-ease);
    }

    .button:hover:not(:disabled) {
        background: var(--bk-surface-alt);
    }

    .button:focus-visible {
        outline: 2px solid var(--bk-focus);
        outline-offset: 2px;
    }

    .button:disabled {
        cursor: default;
        opacity: 0.55;
    }

    .button-danger {
        border-color: var(--bk-danger);
        color: var(--bk-danger);
    }

    .button-danger:hover:not(:disabled) {
        background: var(--bk-danger);
        color: var(--bk-accent-contrast);
    }

    .form-actions {
        display: flex;
        flex-wrap: wrap;
        gap: var(--bk-space-3);
        align-items: center;
        margin-block-start: var(--bk-space-4);
    }

    .auth-form {
        display: flex;
        flex-direction: column;
        gap: var(--bk-space-3);
    }

    .auth-alert {
        padding: var(--bk-space-3);
        border: var(--bk-edge-width) solid var(--bk-danger);
        border-radius: var(--bk-radius-sm);
        color: var(--bk-danger);
    }

    .auth-alert p {
        margin: 0;
    }

    /* The page-level edge reservation for a full-screen page under
       ImmersiveLayout that, unlike the reader, keeps its content inside the
       safe area: the phone apps' /welcome screen. Here because only layouts
       and this file read the --bk-safe-* tokens (DESIGN-PLAN §5.2 rule 5). */
    .bk-standalone-edges {
        padding-block: max(1.5rem, var(--bk-safe-top)) calc(var(--bk-safe-bottom) + 0.5rem);
        padding-inline: max(1.5rem, var(--bk-safe-left)) max(1.5rem, var(--bk-safe-right));
    }

    /* APP MODE (html.bk-app, see MainLayout.razor.css for the shell half).
       The Back link on /login and /register exists in the markup everywhere
       and shows only inside the phone apps, which have no browser back
       button; those pages are WebAssembly and cannot read the user agent,
       so the class on <html> is what decides. The register subtitle's
       "Nothing to install." is false inside an installed app. */
    .auth-back {
        display: none;
    }

    html.bk-app .auth-back {
        display: inline-flex;
        align-items: center;
        gap: var(--bk-space-1);
        min-block-size: 2.75rem;
        margin-block-end: var(--bk-space-2);
    }

    .auth-back svg {
        inline-size: 1.25rem;
        block-size: 1.25rem;
    }

    html.bk-app .auth-subtitle-web {
        display: none;
    }
}
