/* 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: #6b6459;
        --bk-border: #dfd5c2;
        --bk-border-strong: #c6b89e;
        --bk-edge: #23201b;
        --bk-shadow-ink: #23201b;
        --bk-accent: #7b2d26;
        --bk-accent-hover: #6a2620;
        --bk-accent-subtle: #f0e2df;
        --bk-accent-contrast: #faf6ee;
        --bk-highlight: #c8912f;
        --bk-danger: #b3261e;
        --bk-success: #2f5d50;
        --bk-focus: #7b2d26;

        /* ── 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 app shipped 'Helvetica Neue', Helvetica, Arial — the typeface
           of a system dialog, on a product about set 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. Literata is the intended
           reading face and is NOT vendored yet — no font files are in this repo,
           and adding binaries is its own decision — so these stacks name the
           fallbacks the plan already specifies. Georgia ships everywhere and is a
           real serif; the improvement over Helvetica for prose is most of the
           distance, and swapping in Literata later is a change to two lines. */
        --bk-font-ui: system-ui, -apple-system, "Segoe UI", sans-serif;
        --bk-font-reading: Literata, Georgia, "Times New Roman", serif;
        /* LABELS — eyebrows, stamps, counts, window titles. The pixel library's
           retro texture comes from this stack and from the geometry below, NOT
           from a pixel typeface: pixel fonts 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 — DECIDED 2026-09-02 (pixel library, superseding §4.4's 4/8/12):
           square. Three names are kept so that no call site changes, and all
           three are zero; a distinct sm/md/lg would be the lottery coming back.
           `pill` is the one exception the direction allows, and `full` stays
           for the avatar-shaped things that have no edges to draw. */
        --bk-radius-sm: 0;
        --bk-radius-md: 0;
        --bk-radius-lg: 0;
        --bk-radius-pill: 2px;
        --bk-radius-full: 999px;

        /* EDGE AND SHADOW — the pixel rule, and the whole of it. The boundary
           of an object is a 2px line in --bk-edge; rows INSIDE an object keep
           the 1px --bk-border hairline; a shelf board is 3px. The shadow is a
           HARD OFFSET in --bk-shadow-ink, no blur: it is a drawn edge, not a
           light effect, which is why it lives with the scales rather than the
           palette — only its ink varies by theme. This overrules §4.3's "paper
           casts no black shadow" deliberately, and it is also an accessibility
           fix: the 1px --bk-border-strong boundary measured 1.81:1 (light),
           2.15:1 (dark) and 1.92:1 (sepia) against its surface, under the 3:1
           WCAG 2.2 SC 1.4.11 asks of a control boundary; --bk-edge clears it
           in all three. */
        --bk-edge-width: 2px;
        --bk-shadow-1: 3px 3px 0 var(--bk-shadow-ink);
        --bk-shadow-2: 4px 4px 0 var(--bk-shadow-ink);

        /* 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). */
    --bk-nav-block-size: 3.5rem;

    /* 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 dark;
    }

    /* 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;
        }
    }

    /* THE SYSTEM'S CHOICE, and only while the reader has not made one (M5-10).
       The guard is what makes an explicit "light" stick on a machine set to dark:
       without it this block still applies and the two fight, with the winner
       decided by specificity rather than by intent. `system` is spelled out as
       well as absent, because that is the value user_preferences stores by
       default and a rendered attribute saying so must behave like no attribute at
       all. */
    @media (prefers-color-scheme: dark) {
        :root:not([data-theme]),
        :root[data-theme="system"] {
            --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: the offset is a drawn edge, and black on black is
               the deepest line 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: #33231f;
            --bk-accent-contrast: #1a1712;
            --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: #6b6459;
        --bk-border: #dfd5c2;
        --bk-border-strong: #c6b89e;
        --bk-edge: #23201b;
        --bk-shadow-ink: #23201b;
        --bk-accent: #7b2d26;
        --bk-accent-hover: #6a2620;
        --bk-accent-subtle: #f0e2df;
        --bk-accent-contrast: #faf6ee;
        --bk-highlight: #c8912f;
        --bk-danger: #b3261e;
        --bk-success: #2f5d50;
        --bk-focus: #7b2d26;

        --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: #33231f;
        --bk-accent-contrast: #1a1712;
        --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);
    }

    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);
        border-block-start: var(--bk-edge-width) solid var(--bk-edge);
        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). */
    .bk-button-primary {
        background: var(--bk-accent-cta, var(--bk-accent));
        border-color: var(--bk-edge);
        color: var(--bk-accent-contrast);
        box-shadow: var(--bk-shadow-1);
    }

    .bk-button-primary:hover:not(:disabled) {
        background: 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. Two
       levels of elevation exist in this system and no more (§4.3). Since the
       pixel decision the shadow is a hard offset rather than a warm blur, but
       the sentence that mattered still holds: the border is the primary edge —
       that is this design's character — and the shadow only says "this
       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);
        box-shadow: var(--bk-shadow-2);
    }

    /* ── .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;
    }

    /* ══════════════════════════════════════════════════════════════════════
       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);
        border-block-start: var(--bk-edge-width) solid var(--bk-edge);
    }

    /* THE LOOK IS SHARED BY BOTH GROUPS, THE ARRANGEMENT IS NOT. `.nav-link` is
       worn by the four destinations AND by Sign in / Register 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. */
    .app-nav .nav-link {
        flex: 1 1 0;
        min-inline-size: 0;
        padding-inline: var(--bk-space-1);
        font-size: var(--bk-text-xs);
        text-align: center;
    }

    /* THE ACTIVE STATE IS NOT COLOUR ALONE. The tint says "here" at a glance
       and the 2px rule is what carries it to a reader who cannot separate the
       two colours — 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.

       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 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 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. The tint would
       wash the fill out and the accent ink would sit on the accent fill —
       both outrank the primitive from here, which is how #182 measured. */
    .nav-link.active:not(.bk-button-primary) {
        color: var(--bk-accent);
        background-color: var(--bk-accent-subtle);
    }

    /* The rule 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 a tint there says "you are on the sign-in
       page" without claiming to be a position in a list. 4px rather than 2:
       at 2px it read as a hairline beside the new 2px edge above it. The CTA
       is guarded out with the rest of the active treatment — a 4px accent bar
       inset on the accent fill is invisible in light and sepia and a stray
       line in dark. */
    .app-nav .nav-link.active:not(.bk-button-primary) {
        box-shadow: inset 0 4px 0 0 var(--bk-accent);
    }

    /* ── 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;
        }

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

        .app-nav .nav-link.active:not(.bk-button-primary) {
            /* The same treatment, on the edge nearest the content — which at
               this width is below the link rather than above it. */
            box-shadow: inset 0 -4px 0 0 var(--bk-accent);
        }
    }

    /* ── 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;
    }
}
