/* ============================================================
   Reset & base
   ============================================================ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* No scrollbar-gutter reservation, no forced scrollbar — the document
   shows a scrollbar only when content actually overflows. On short
   pages .site-header reaches the viewport edge; on long pages the
   scrollbar appears and the header butts up to it. Going from short
   to long (e.g. opening a modal that locks body scroll) causes a
   horizontal shift of one scrollbar width; modals that mind this can
   compensate via their own lock styles. */
html { overflow-y: auto; }

body {
    font-family: system-ui, -apple-system, sans-serif;
    font-size: 14px;
    line-height: 1.5;
    color: var(--bspkfw-fg-1f2937);
    background: var(--bspkfw-page-bg);
}

a { color: var(--bspkfw-accent-text); text-decoration: none; }
a:hover { text-decoration: underline; }
/* A link inside running text is underlined: colour alone does not tell it
   from the words around it (WCAG 1.4.1; axe `link-in-text-block`, pinned by
   tests/Browser/specs/a11y.spec.js). A link standing on its own (nav, menus,
   buttons, table cells) keeps the plain style; a list item counts as running
   text only in prose (the manual, a markdown preview), never in a nav list. :where() keeps the weight of
   a bare `a:not()`, so any component's own link rule still wins. */
:where(p, dd, figcaption, blockquote, .manual-body li, .manual-modal-body li, .bspkfw-md-preview li) a:not([class*="btn"]) { text-decoration: underline; text-underline-offset: 2px; }

/* ============================================================
   Density spacing tokens — page chrome
   ============================================================
   The per-user Comfortable / Condensed toggle (see the "Table density"
   block further down) governs PAGE CHROME as well as table cells: the
   layout-holding spacing below is declared through these tokens, and the
   condensed override tightens them all at once. Comfortable (attribute
   absent) resolves every token to today's exact literal, so default
   rendering is byte-identical; every consuming declaration also carries
   the same literal as its var() fallback, so a rule is correct even in
   isolation.

   Deliberately COARSE: four tokens for layout spacing (page padding,
   card/section padding + big block margins, section gaps, form-row
   rhythm) — not a token per measurement. Fine positioning, borders,
   small nudges and CONTROL padding (buttons, inputs, menu items) stay
   literal: density moves layout whitespace, never touch-target size.

   Projects opt their own chrome in by consuming the tokens
   (`padding: var(--bspkfw-space-card, 24px);`) — they then tighten in
   step with framework chrome, for free. A project that wants the OLD
   table-only condensed look re-pins the tokens to their comfortable
   values in its own stylesheet:

       :root[data-table-density="condensed"] {
           --bspkfw-space-page: 32px 24px;  --bspkfw-space-card: 24px;
           --bspkfw-space-section-gap: 20px; --bspkfw-space-row: 16px;
       }
   */
:root {
    --bspkfw-space-page:        32px 24px;  /* <main> outer padding */
    --bspkfw-space-card:        24px;       /* card / .page-section padding, page-header + fieldset margins, modal x-padding */
    --bspkfw-space-section-gap: 20px;       /* .page-sections gap, section-desc + tab-bar margins */
    --bspkfw-space-row:         16px;       /* form rows / stacked blocks (.form-group, .alert, em-groups) */

    /* Modal stacking scale. Two `.modal`s at the same z-index stack by DOM
       order — pure luck — so a modal that OPENS FROM another modal (the
       photo cropper launched inside an edit modal) takes the nested rank.
       The next nested-modal case takes the same token rather than minting
       another magic number. */
    --bspkfw-z-modal:        100;
    --bspkfw-z-modal-nested: 110;
    --bspkfw-z-dialog:       120;   /* Dialog.confirm / alert / prompt — above whatever opened it */
}
:root[data-table-density="condensed"] {
    --bspkfw-space-page:        16px 20px;
    --bspkfw-space-card:        14px;
    --bspkfw-space-section-gap: 12px;
    --bspkfw-space-row:         10px;
}

/* ============================================================
   Colour tokens + the light / dark / auto scheme (CLAUDE/THEMING.md →
   "Light / dark / auto")
   ============================================================
   Every colour base.css (and the framework's other stylesheets) paints
   with is a token here. Its LIGHT value is the exact literal the rule
   used to carry, so the light scheme is pixel-identical to before; its
   DARK value applies under `:root[data-color-scheme="dark"]`, and under
   `="auto"` when the OS asks for dark. With no `data-color-scheme` on
   <html> (the scheme feature off) only the light values ever apply.

   --bspkfw-{bg|fg|bd|sh}-{colour} are base.css's OWN tokens, named after
   the role and the light literal — projects don't depend on them. The
   SEMANTIC aliases below are the public set: use them in project CSS and
   both schemes follow. */
:root {
    --bspkfw-bd-16a34a: #16a34a;
    --bspkfw-bd-1d4ed8: #1d4ed8;
    --bspkfw-bd-22c55e: #22c55e;
    --bspkfw-bd-2563eb: #2563eb;
    --bspkfw-bd-334155: #334155;
    --bspkfw-bd-4f46e5: #4f46e5;
    --bspkfw-bd-60a5fa: #60a5fa;
    --bspkfw-bd-6366f1: #6366f1;
    --bspkfw-bd-7c3aed: #7c3aed;
    --bspkfw-bd-818cf8: #818cf8;
    --bspkfw-bd-86efac: #86efac;
    --bspkfw-bd-93c5fd: #93c5fd;
    --bspkfw-bd-94a3b8: #94a3b8;
    --bspkfw-bd-969696: #969696;
    --bspkfw-bd-9ca3af: #9ca3af;
    --bspkfw-bd-b91c1c: #b91c1c;
    --bspkfw-bd-bbf7d0: #bbf7d0;
    --bspkfw-bd-bfdbfe: #bfdbfe;
    --bspkfw-bd-c53030: #c53030;
    --bspkfw-bd-c7d2fe: #c7d2fe;
    --bspkfw-bd-ca8a04: #ca8a04;
    --bspkfw-bd-cfd4dc: #cfd4dc;
    --bspkfw-bd-d1d5db: #d1d5db;
    --bspkfw-bd-d6deeb: #d6deeb;
    --bspkfw-bd-d97706: #d97706;
    --bspkfw-bd-dc2626: #dc2626;
    --bspkfw-bd-e11d48: #e11d48;
    --bspkfw-bd-e3e6eb: #e3e6eb;
    --bspkfw-bd-e5e7eb: #e5e7eb;
    --bspkfw-bd-f3f4f6: #f3f4f6;
    --bspkfw-bd-f59e0b: #f59e0b;
    --bspkfw-bd-f87171: #f87171;
    --bspkfw-bd-fca5a5: #fca5a5;
    --bspkfw-bd-fcd34d: #fcd34d;
    --bspkfw-bd-fde68a: #fde68a;
    --bspkfw-bd-fecaca: #fecaca;
    --bspkfw-bd-ffd591: #ffd591;
    --bspkfw-bd-fff: #fff;
    --bspkfw-bd-rgba-100-116-139-45: rgba(100,116,139,0.45);
    --bspkfw-bd-rgba-239-68-68-12: rgba(239,68,68,0.12);
    --bspkfw-bd-rgba-255-255-255-12: rgba(255,255,255,0.12);
    --bspkfw-bd-rgba-255-255-255-18: rgba(255,255,255,0.18);
    --bspkfw-bd-rgba-255-255-255-5: rgba(255,255,255,0.5);
    --bspkfw-bd-rgba-255-255-255-8: rgba(255,255,255,0.8);
    --bspkfw-bd-rgba-37-99-235-12: rgba(37,99,235,0.12);
    --bspkfw-bd-rgba-99-102-241-2: rgba(99,102,241,0.2);
    --bspkfw-bd-rgba-99-102-241-25: rgba(99,102,241,0.25);
    --bspkfw-bg-000: #000;
    --bspkfw-bg-111827: #111827;
    --bspkfw-bg-15803d: #15803d;
    --bspkfw-bg-1d4ed8: #1d4ed8;
    --bspkfw-bg-1e293b: #1e293b;
    --bspkfw-bg-1f2937: #1f2937;
    --bspkfw-bg-2563eb: #2563eb;
    --bspkfw-bg-334155: #334155;
    --bspkfw-bg-374151: #374151;
    --bspkfw-bg-5f6774: #5f6774;
    --bspkfw-bg-b45309: #b45309;
    --bspkfw-bg-b91c1c: #b91c1c;
    --bspkfw-bg-d1d5db: #d1d5db;
    --bspkfw-bg-d1fae5: #d1fae5;
    --bspkfw-bg-dbeafe: #dbeafe;
    --bspkfw-bg-dc2626: #dc2626;
    --bspkfw-bg-dcfce7: #dcfce7;
    --bspkfw-bg-e0e7ff: #e0e7ff;
    --bspkfw-bg-e2e8f0: #e2e8f0;
    --bspkfw-bg-e3e6eb: #e3e6eb;
    --bspkfw-bg-e5e7eb: #e5e7eb;
    --bspkfw-bg-ede9fe: #ede9fe;
    --bspkfw-bg-eef2ff: #eef2ff;
    --bspkfw-bg-eff6ff: #eff6ff;
    --bspkfw-bg-f0f0f0: #f0f0f0;
    --bspkfw-bg-f0fdf4: #f0fdf4;
    --bspkfw-bg-f1f5f9: #f1f5f9;
    --bspkfw-bg-f3f4f6: #f3f4f6;
    --bspkfw-bg-f3f6fa: #f3f6fa;
    --bspkfw-bg-f5f8ff: #f5f8ff;
    --bspkfw-bg-f6f7f9: #f6f7f9;
    --bspkfw-bg-f7f8fa: #f7f8fa;
    --bspkfw-bg-f87171: #f87171;
    --bspkfw-bg-f8fafc: #f8fafc;
    --bspkfw-bg-f9fafb: #f9fafb;
    --bspkfw-bg-fee2e2: #fee2e2;
    --bspkfw-bg-fef08a: #fef08a;
    --bspkfw-bg-fef2f2: #fef2f2;
    --bspkfw-bg-fef3c7: #fef3c7;
    --bspkfw-bg-fef9c3: #fef9c3;
    --bspkfw-bg-fff: #fff;
    --bspkfw-bg-fff7e6: #fff7e6;
    --bspkfw-bg-fffbeb: #fffbeb;
    --bspkfw-bg-rgba-0-0-0-02: rgba(0,0,0,0.02);
    --bspkfw-bg-rgba-0-0-0-04: rgba(0,0,0,0.04);
    --bspkfw-bg-rgba-0-0-0-06: rgba(0,0,0,0.06);
    --bspkfw-bg-rgba-0-0-0-45: rgba(0,0,0,0.45);
    --bspkfw-bg-rgba-15-23-42-0: rgba(15,23,42,0);
    --bspkfw-bg-rgba-15-23-42-20: rgba(15,23,42,0.20);
    --bspkfw-bg-rgba-15-23-42-55: rgba(15,23,42,0.55);
    --bspkfw-bg-rgba-15-23-42-92: rgba(15,23,42,0.92);
    --bspkfw-bg-rgba-17-24-39-65: rgba(17,24,39,0.65);
    --bspkfw-bg-rgba-17-24-39-92: rgba(17,24,39,0.92);
    --bspkfw-bg-rgba-255-255-255-12: rgba(255,255,255,0.12);
    --bspkfw-bg-rgba-255-255-255-24: rgba(255,255,255,0.24);
    --bspkfw-bg-rgba-255-255-255-65: rgba(255,255,255,0.65);
    --bspkfw-bg-rgba-37-99-235-12: rgba(37,99,235,0.12);
    --bspkfw-bg-rgba-37-99-235-18: rgba(37,99,235,0.18);
    --bspkfw-fg-047857: #047857;
    --bspkfw-fg-065f46: #065f46;
    --bspkfw-fg-0d9488: #0d9488;
    --bspkfw-fg-111: #111;
    --bspkfw-fg-111827: #111827;
    --bspkfw-fg-15803d: #15803d;
    --bspkfw-fg-166534: #166534;
    --bspkfw-fg-16a34a: #16a34a;
    --bspkfw-fg-1d4ed8: #1d4ed8;
    --bspkfw-fg-1e3a8a: #1e3a8a;
    --bspkfw-fg-1e40af: #1e40af;
    --bspkfw-fg-1f2937: #1f2937;
    --bspkfw-fg-2563eb: #2563eb;
    --bspkfw-fg-374151: #374151;
    --bspkfw-fg-4b5563: #4b5563;
    --bspkfw-fg-4f46e5: #4f46e5;
    --bspkfw-fg-555: #555;
    --bspkfw-fg-5f6774: #5f6774;
    --bspkfw-fg-6366f1: #6366f1;
    --bspkfw-fg-64748b: #64748b;
    --bspkfw-fg-6e6e6e: #6e6e6e;
    --bspkfw-fg-78350f: #78350f;
    --bspkfw-fg-7c3aed: #7c3aed;
    --bspkfw-fg-7f1d1d: #7f1d1d;
    --bspkfw-fg-854d0e: #854d0e;
    --bspkfw-fg-888: #888;
    --bspkfw-fg-8a5a00: #8a5a00;
    --bspkfw-fg-92400e: #92400e;
    --bspkfw-fg-94a3b8: #94a3b8;
    --bspkfw-fg-991b1b: #991b1b;
    --bspkfw-fg-9ca3af: #9ca3af;
    --bspkfw-fg-b45309: #b45309;
    --bspkfw-fg-b91c1c: #b91c1c;
    --bspkfw-fg-c53030: #c53030;
    --bspkfw-fg-cbd5e1: #cbd5e1;
    --bspkfw-fg-d97706: #d97706;
    --bspkfw-fg-dc2626: #dc2626;
    --bspkfw-fg-e5e7eb: #e5e7eb;
    --bspkfw-fg-ef4444: #ef4444;
    --bspkfw-fg-f1f5f9: #f1f5f9;
    --bspkfw-fg-f87171: #f87171;
    --bspkfw-fg-f8fafc: #f8fafc;
    --bspkfw-fg-f9fafb: #f9fafb;
    --bspkfw-fg-fff: #fff;
    --bspkfw-fg-rgba-37-99-235-35: rgba(37,99,235,0.35);
    --bspkfw-page-bg: #f3f4f6;
    --bspkfw-sh-2563eb: #2563eb;
    --bspkfw-sh-bfdbfe: #bfdbfe;
    --bspkfw-sh-f59e0b: #f59e0b;
    --bspkfw-sh-rgba-0-0-0-08: rgba(0,0,0,0.08);
    --bspkfw-sh-rgba-0-0-0-1: rgba(0,0,0,0.1);
    --bspkfw-sh-rgba-0-0-0-10: rgba(0,0,0,0.10);
    --bspkfw-sh-rgba-0-0-0-12: rgba(0,0,0,0.12);
    --bspkfw-sh-rgba-0-0-0-14: rgba(0,0,0,0.14);
    --bspkfw-sh-rgba-0-0-0-15: rgba(0,0,0,0.15);
    --bspkfw-sh-rgba-0-0-0-18: rgba(0,0,0,0.18);
    --bspkfw-sh-rgba-0-0-0-2: rgba(0,0,0,0.2);
    --bspkfw-sh-rgba-0-0-0-25: rgba(0,0,0,0.25);
    --bspkfw-sh-rgba-0-0-0-3: rgba(0,0,0,0.3);
    --bspkfw-sh-rgba-0-0-0-4: rgba(0,0,0,0.4);
    --bspkfw-sh-rgba-0-0-0-5: rgba(0,0,0,0.5);
    --bspkfw-sh-rgba-15-23-42-18: rgba(15,23,42,0.18);
    --bspkfw-sh-rgba-239-68-68-12: rgba(239,68,68,0.12);
    --bspkfw-sh-rgba-255-255-255-35: rgba(255,255,255,0.35);
    --bspkfw-sh-rgba-34-197-94-35: rgba(34,197,94,0.35);
    --bspkfw-sh-rgba-37-99-235-12: rgba(37,99,235,0.12);
    --bspkfw-sh-rgba-37-99-235-35: rgba(37,99,235,0.35);
}

/* The public, semantic set (alias the role tokens, so they switch with the scheme). */
:root {
    --bspkfw-surface: var(--bspkfw-bg-fff);                   /* cards, modals, menus, inputs */
    --bspkfw-surface-2: var(--bspkfw-bg-f9fafb);              /* a subtle second surface: table heads, modal headers */
    --bspkfw-sunken: var(--bspkfw-bg-f3f4f6);                 /* hover rows, chips, wells */
    --bspkfw-ink: var(--bspkfw-fg-111827);                    /* headings, the strongest text */
    --bspkfw-text: var(--bspkfw-fg-1f2937);                   /* body text */
    --bspkfw-text-2: var(--bspkfw-fg-374151);                 /* labels, secondary text */
    --bspkfw-muted: var(--bspkfw-fg-5f6774);                  /* help text, descriptions */
    --bspkfw-faint: var(--bspkfw-fg-9ca3af);                  /* placeholders, disabled text */
    --bspkfw-border: var(--bspkfw-bd-e5e7eb);                 /* card and table rules */
    --bspkfw-border-strong: var(--bspkfw-bd-d1d5db);          /* input and button borders */
    --bspkfw-success: var(--bspkfw-fg-166534);                /* success text */
    --bspkfw-success-soft: var(--bspkfw-bg-f0fdf4);           /* success background */
    --bspkfw-warn: var(--bspkfw-fg-92400e);                   /* warning text */
    --bspkfw-warn-soft: var(--bspkfw-bg-fffbeb);              /* warning background */
    --bspkfw-danger: var(--bspkfw-fg-991b1b);                 /* error text */
    --bspkfw-danger-soft: var(--bspkfw-bg-fef2f2);            /* error background */
    --bspkfw-danger-solid: var(--bspkfw-bg-dc2626);           /* a danger button */
    --bspkfw-scrim: var(--bspkfw-bg-rgba-15-23-42-55);        /* the backdrop behind a drawer or modal */
}

/* Dark: forced, or auto with a dark OS. Keep the two blocks identical. */
:root[data-color-scheme="dark"] {
    color-scheme: dark;
    --bspkfw-bd-16a34a: #3fb950;
    --bspkfw-bd-1d4ed8: #4493f8;
    --bspkfw-bd-22c55e: #3fb950;
    --bspkfw-bd-2563eb: #4493f8;
    --bspkfw-bd-334155: #444c56;
    --bspkfw-bd-4f46e5: #8b8cf8;
    --bspkfw-bd-60a5fa: #58a6ff;
    --bspkfw-bd-6366f1: #8b8cf8;
    --bspkfw-bd-7c3aed: #a371f7;
    --bspkfw-bd-818cf8: #a5a5ff;
    --bspkfw-bd-86efac: #2a6a3c;
    --bspkfw-bd-93c5fd: #388bfd;
    --bspkfw-bd-94a3b8: #59636e;
    --bspkfw-bd-969696: #545d68;
    --bspkfw-bd-9ca3af: #59636e;
    --bspkfw-bd-b91c1c: #f85149;
    --bspkfw-bd-bbf7d0: #1f4a2c;
    --bspkfw-bd-bfdbfe: #1f3d66;
    --bspkfw-bd-c53030: #f85149;
    --bspkfw-bd-c7d2fe: #2c3166;
    --bspkfw-bd-ca8a04: #d29922;
    --bspkfw-bd-cfd4dc: #3d444d;
    --bspkfw-bd-d1d5db: #3d444d;
    --bspkfw-bd-d6deeb: #30363d;
    --bspkfw-bd-d97706: #d29922;
    --bspkfw-bd-dc2626: #f85149;
    --bspkfw-bd-e11d48: #ff6b8b;
    --bspkfw-bd-e3e6eb: #30363d;
    --bspkfw-bd-e5e7eb: #30363d;
    --bspkfw-bd-f3f4f6: #262c34;
    --bspkfw-bd-f59e0b: #d29922;
    --bspkfw-bd-f87171: #f85149;
    --bspkfw-bd-fca5a5: #7a2c2f;
    --bspkfw-bd-fcd34d: #6e5618;
    --bspkfw-bd-fde68a: #5a4613;
    --bspkfw-bd-fecaca: #5c2326;
    --bspkfw-bd-ffd591: #5a4613;
    --bspkfw-bd-fff: #161b22;
    --bspkfw-bd-rgba-100-116-139-45: rgba(100,116,139,0.45);
    --bspkfw-bd-rgba-239-68-68-12: rgba(248,81,73,0.25);
    --bspkfw-bd-rgba-255-255-255-12: rgba(255,255,255,0.12);
    --bspkfw-bd-rgba-255-255-255-18: rgba(255,255,255,0.18);
    --bspkfw-bd-rgba-255-255-255-5: rgba(255,255,255,0.5);
    --bspkfw-bd-rgba-255-255-255-8: rgba(255,255,255,0.8);
    --bspkfw-bd-rgba-37-99-235-12: rgba(68,147,248,0.3);
    --bspkfw-bd-rgba-99-102-241-2: rgba(99,102,241,0.2);
    --bspkfw-bd-rgba-99-102-241-25: rgba(99,102,241,0.25);
    --bspkfw-bg-000: #000;
    --bspkfw-bg-111827: #2d333b;
    --bspkfw-bg-15803d: #238636;
    --bspkfw-bg-1d4ed8: #1f5ad6;
    --bspkfw-bg-1e293b: #1c2128;
    --bspkfw-bg-1f2937: #2d333b;
    --bspkfw-bg-2563eb: #2563eb;
    --bspkfw-bg-334155: #2d333b;
    --bspkfw-bg-374151: #444c56;
    --bspkfw-bg-5f6774: #6e7681;
    --bspkfw-bg-b45309: #9e6a03;
    --bspkfw-bg-b91c1c: #b62324;
    --bspkfw-bg-d1d5db: #373e47;
    --bspkfw-bg-d1fae5: #123028;
    --bspkfw-bg-dbeafe: #18304f;
    --bspkfw-bg-dc2626: #da3633;
    --bspkfw-bg-dcfce7: #153220;
    --bspkfw-bg-e0e7ff: #22264a;
    --bspkfw-bg-e2e8f0: #2d333b;
    --bspkfw-bg-e3e6eb: #2d333b;
    --bspkfw-bg-e5e7eb: #2d333b;
    --bspkfw-bg-ede9fe: #251d3d;
    --bspkfw-bg-eef2ff: #1b1e3a;
    --bspkfw-bg-eff6ff: #132339;
    --bspkfw-bg-f0f0f0: #22272e;
    --bspkfw-bg-f0fdf4: #11261a;
    --bspkfw-bg-f1f5f9: #22272e;
    --bspkfw-bg-f3f4f6: #22272e;
    --bspkfw-bg-f3f6fa: #1c2128;
    --bspkfw-bg-f5f8ff: #1a2231;
    --bspkfw-bg-f6f7f9: #1c2128;
    --bspkfw-bg-f7f8fa: #1c2128;
    --bspkfw-bg-f87171: #da3633;
    --bspkfw-bg-f8fafc: #1c2128;
    --bspkfw-bg-f9fafb: #1c2128;
    --bspkfw-bg-fee2e2: #3c1a1d;
    --bspkfw-bg-fef08a: #4d4310;
    --bspkfw-bg-fef2f2: #2d1517;
    --bspkfw-bg-fef3c7: #382b0e;
    --bspkfw-bg-fef9c3: #2f2b0e;
    --bspkfw-bg-fff: #161b22;
    --bspkfw-bg-fff7e6: #2a2111;
    --bspkfw-bg-fffbeb: #2a2111;
    --bspkfw-bg-rgba-0-0-0-02: rgba(255,255,255,0.03);
    --bspkfw-bg-rgba-0-0-0-04: rgba(255,255,255,0.04);
    --bspkfw-bg-rgba-0-0-0-06: rgba(255,255,255,0.06);
    --bspkfw-bg-rgba-0-0-0-45: rgba(0,0,0,0.6);
    --bspkfw-bg-rgba-15-23-42-0: rgba(1,4,9,0);
    --bspkfw-bg-rgba-15-23-42-20: rgba(1,4,9,0.5);
    --bspkfw-bg-rgba-15-23-42-55: rgba(1,4,9,0.7);
    --bspkfw-bg-rgba-15-23-42-92: rgba(1,4,9,0.92);
    --bspkfw-bg-rgba-17-24-39-65: rgba(1,4,9,0.7);
    --bspkfw-bg-rgba-17-24-39-92: rgba(1,4,9,0.92);
    --bspkfw-bg-rgba-255-255-255-12: rgba(255,255,255,0.12);
    --bspkfw-bg-rgba-255-255-255-24: rgba(255,255,255,0.24);
    --bspkfw-bg-rgba-255-255-255-65: rgba(13,17,23,0.65);
    --bspkfw-bg-rgba-37-99-235-12: rgba(68,147,248,0.18);
    --bspkfw-bg-rgba-37-99-235-18: rgba(68,147,248,0.25);
    --bspkfw-fg-047857: #56d364;
    --bspkfw-fg-065f46: #56d364;
    --bspkfw-fg-0d9488: #39c5bb;
    --bspkfw-fg-111: #e6edf3;
    --bspkfw-fg-111827: #e6edf3;
    --bspkfw-fg-15803d: #56d364;
    --bspkfw-fg-166534: #56d364;
    --bspkfw-fg-16a34a: #3fb950;
    --bspkfw-fg-1d4ed8: #79c0ff;
    --bspkfw-fg-1e3a8a: #a5d6ff;
    --bspkfw-fg-1e40af: #79c0ff;
    --bspkfw-fg-1f2937: #e6edf3;
    --bspkfw-fg-2563eb: #58a6ff;
    --bspkfw-fg-374151: #c9d1d9;
    --bspkfw-fg-4b5563: #b1bac4;
    --bspkfw-fg-4f46e5: #a5a5ff;
    --bspkfw-fg-555: #b1bac4;
    --bspkfw-fg-5f6774: #9198a1;
    --bspkfw-fg-6366f1: #a5a5ff;
    --bspkfw-fg-64748b: #8b949e;
    --bspkfw-fg-6e6e6e: #8b949e;
    --bspkfw-fg-78350f: #f2cc60;
    --bspkfw-fg-7c3aed: #bc8cff;
    --bspkfw-fg-7f1d1d: #ffa198;
    --bspkfw-fg-854d0e: #e3b341;
    --bspkfw-fg-888: #7d8590;
    --bspkfw-fg-8a5a00: #e3b341;
    --bspkfw-fg-92400e: #e3b341;
    --bspkfw-fg-94a3b8: #8b949e;
    --bspkfw-fg-991b1b: #ff7b72;
    --bspkfw-fg-9ca3af: #7d8590;
    --bspkfw-fg-b45309: #e3b341;
    --bspkfw-fg-b91c1c: #ff7b72;
    --bspkfw-fg-c53030: #ff7b72;
    --bspkfw-fg-cbd5e1: #cbd5e1;
    --bspkfw-fg-d97706: #d29922;
    --bspkfw-fg-dc2626: #f85149;
    --bspkfw-fg-e5e7eb: #e5e7eb;
    --bspkfw-fg-ef4444: #f85149;
    --bspkfw-fg-f1f5f9: #f1f5f9;
    --bspkfw-fg-f87171: #f87171;
    --bspkfw-fg-f8fafc: #f8fafc;
    --bspkfw-fg-f9fafb: #f9fafb;
    --bspkfw-fg-fff: #fff;
    --bspkfw-fg-rgba-37-99-235-35: rgba(88,166,255,0.45);
    --bspkfw-page-bg: #0d1117;
    --bspkfw-sh-2563eb: #4493f8;
    --bspkfw-sh-bfdbfe: #1f3d66;
    --bspkfw-sh-f59e0b: #d29922;
    --bspkfw-sh-rgba-0-0-0-08: rgba(0,0,0,0.2);
    --bspkfw-sh-rgba-0-0-0-1: rgba(0,0,0,0.25);
    --bspkfw-sh-rgba-0-0-0-10: rgba(0,0,0,0.25);
    --bspkfw-sh-rgba-0-0-0-12: rgba(0,0,0,0.3);
    --bspkfw-sh-rgba-0-0-0-14: rgba(0,0,0,0.35);
    --bspkfw-sh-rgba-0-0-0-15: rgba(0,0,0,0.38);
    --bspkfw-sh-rgba-0-0-0-18: rgba(0,0,0,0.45);
    --bspkfw-sh-rgba-0-0-0-2: rgba(0,0,0,0.5);
    --bspkfw-sh-rgba-0-0-0-25: rgba(0,0,0,0.62);
    --bspkfw-sh-rgba-0-0-0-3: rgba(0,0,0,0.7);
    --bspkfw-sh-rgba-0-0-0-4: rgba(0,0,0,0.7);
    --bspkfw-sh-rgba-0-0-0-5: rgba(0,0,0,0.7);
    --bspkfw-sh-rgba-15-23-42-18: rgba(0,0,0,0.5);
    --bspkfw-sh-rgba-239-68-68-12: rgba(248,81,73,0.25);
    --bspkfw-sh-rgba-255-255-255-35: rgba(255,255,255,0.2);
    --bspkfw-sh-rgba-34-197-94-35: rgba(63,185,80,0.4);
    --bspkfw-sh-rgba-37-99-235-12: rgba(68,147,248,0.3);
    --bspkfw-sh-rgba-37-99-235-35: rgba(68,147,248,0.45);
}
@media (prefers-color-scheme: dark) {
    :root[data-color-scheme="auto"] {
        color-scheme: dark;
        --bspkfw-bd-16a34a: #3fb950;
        --bspkfw-bd-1d4ed8: #4493f8;
        --bspkfw-bd-22c55e: #3fb950;
        --bspkfw-bd-2563eb: #4493f8;
        --bspkfw-bd-334155: #444c56;
        --bspkfw-bd-4f46e5: #8b8cf8;
        --bspkfw-bd-60a5fa: #58a6ff;
        --bspkfw-bd-6366f1: #8b8cf8;
        --bspkfw-bd-7c3aed: #a371f7;
        --bspkfw-bd-818cf8: #a5a5ff;
        --bspkfw-bd-86efac: #2a6a3c;
        --bspkfw-bd-93c5fd: #388bfd;
        --bspkfw-bd-94a3b8: #59636e;
        --bspkfw-bd-969696: #545d68;
        --bspkfw-bd-9ca3af: #59636e;
        --bspkfw-bd-b91c1c: #f85149;
        --bspkfw-bd-bbf7d0: #1f4a2c;
        --bspkfw-bd-bfdbfe: #1f3d66;
        --bspkfw-bd-c53030: #f85149;
        --bspkfw-bd-c7d2fe: #2c3166;
        --bspkfw-bd-ca8a04: #d29922;
        --bspkfw-bd-cfd4dc: #3d444d;
        --bspkfw-bd-d1d5db: #3d444d;
        --bspkfw-bd-d6deeb: #30363d;
        --bspkfw-bd-d97706: #d29922;
        --bspkfw-bd-dc2626: #f85149;
        --bspkfw-bd-e11d48: #ff6b8b;
        --bspkfw-bd-e3e6eb: #30363d;
        --bspkfw-bd-e5e7eb: #30363d;
        --bspkfw-bd-f3f4f6: #262c34;
        --bspkfw-bd-f59e0b: #d29922;
        --bspkfw-bd-f87171: #f85149;
        --bspkfw-bd-fca5a5: #7a2c2f;
        --bspkfw-bd-fcd34d: #6e5618;
        --bspkfw-bd-fde68a: #5a4613;
        --bspkfw-bd-fecaca: #5c2326;
        --bspkfw-bd-ffd591: #5a4613;
        --bspkfw-bd-fff: #161b22;
        --bspkfw-bd-rgba-100-116-139-45: rgba(100,116,139,0.45);
        --bspkfw-bd-rgba-239-68-68-12: rgba(248,81,73,0.25);
        --bspkfw-bd-rgba-255-255-255-12: rgba(255,255,255,0.12);
        --bspkfw-bd-rgba-255-255-255-18: rgba(255,255,255,0.18);
        --bspkfw-bd-rgba-255-255-255-5: rgba(255,255,255,0.5);
        --bspkfw-bd-rgba-255-255-255-8: rgba(255,255,255,0.8);
        --bspkfw-bd-rgba-37-99-235-12: rgba(68,147,248,0.3);
        --bspkfw-bd-rgba-99-102-241-2: rgba(99,102,241,0.2);
        --bspkfw-bd-rgba-99-102-241-25: rgba(99,102,241,0.25);
        --bspkfw-bg-000: #000;
        --bspkfw-bg-111827: #2d333b;
        --bspkfw-bg-15803d: #238636;
        --bspkfw-bg-1d4ed8: #1f5ad6;
        --bspkfw-bg-1e293b: #1c2128;
        --bspkfw-bg-1f2937: #2d333b;
        --bspkfw-bg-2563eb: #2563eb;
        --bspkfw-bg-334155: #2d333b;
        --bspkfw-bg-374151: #444c56;
        --bspkfw-bg-5f6774: #6e7681;
        --bspkfw-bg-b45309: #9e6a03;
        --bspkfw-bg-b91c1c: #b62324;
        --bspkfw-bg-d1d5db: #373e47;
        --bspkfw-bg-d1fae5: #123028;
        --bspkfw-bg-dbeafe: #18304f;
        --bspkfw-bg-dc2626: #da3633;
        --bspkfw-bg-dcfce7: #153220;
        --bspkfw-bg-e0e7ff: #22264a;
        --bspkfw-bg-e2e8f0: #2d333b;
        --bspkfw-bg-e3e6eb: #2d333b;
        --bspkfw-bg-e5e7eb: #2d333b;
        --bspkfw-bg-ede9fe: #251d3d;
        --bspkfw-bg-eef2ff: #1b1e3a;
        --bspkfw-bg-eff6ff: #132339;
        --bspkfw-bg-f0f0f0: #22272e;
        --bspkfw-bg-f0fdf4: #11261a;
        --bspkfw-bg-f1f5f9: #22272e;
        --bspkfw-bg-f3f4f6: #22272e;
        --bspkfw-bg-f3f6fa: #1c2128;
        --bspkfw-bg-f5f8ff: #1a2231;
        --bspkfw-bg-f6f7f9: #1c2128;
        --bspkfw-bg-f7f8fa: #1c2128;
        --bspkfw-bg-f87171: #da3633;
        --bspkfw-bg-f8fafc: #1c2128;
        --bspkfw-bg-f9fafb: #1c2128;
        --bspkfw-bg-fee2e2: #3c1a1d;
        --bspkfw-bg-fef08a: #4d4310;
        --bspkfw-bg-fef2f2: #2d1517;
        --bspkfw-bg-fef3c7: #382b0e;
        --bspkfw-bg-fef9c3: #2f2b0e;
        --bspkfw-bg-fff: #161b22;
        --bspkfw-bg-fff7e6: #2a2111;
        --bspkfw-bg-fffbeb: #2a2111;
        --bspkfw-bg-rgba-0-0-0-02: rgba(255,255,255,0.03);
        --bspkfw-bg-rgba-0-0-0-04: rgba(255,255,255,0.04);
        --bspkfw-bg-rgba-0-0-0-06: rgba(255,255,255,0.06);
        --bspkfw-bg-rgba-0-0-0-45: rgba(0,0,0,0.6);
        --bspkfw-bg-rgba-15-23-42-0: rgba(1,4,9,0);
        --bspkfw-bg-rgba-15-23-42-20: rgba(1,4,9,0.5);
        --bspkfw-bg-rgba-15-23-42-55: rgba(1,4,9,0.7);
        --bspkfw-bg-rgba-15-23-42-92: rgba(1,4,9,0.92);
        --bspkfw-bg-rgba-17-24-39-65: rgba(1,4,9,0.7);
        --bspkfw-bg-rgba-17-24-39-92: rgba(1,4,9,0.92);
        --bspkfw-bg-rgba-255-255-255-12: rgba(255,255,255,0.12);
        --bspkfw-bg-rgba-255-255-255-24: rgba(255,255,255,0.24);
        --bspkfw-bg-rgba-255-255-255-65: rgba(13,17,23,0.65);
        --bspkfw-bg-rgba-37-99-235-12: rgba(68,147,248,0.18);
        --bspkfw-bg-rgba-37-99-235-18: rgba(68,147,248,0.25);
        --bspkfw-fg-047857: #56d364;
        --bspkfw-fg-065f46: #56d364;
        --bspkfw-fg-0d9488: #39c5bb;
        --bspkfw-fg-111: #e6edf3;
        --bspkfw-fg-111827: #e6edf3;
        --bspkfw-fg-15803d: #56d364;
        --bspkfw-fg-166534: #56d364;
        --bspkfw-fg-16a34a: #3fb950;
        --bspkfw-fg-1d4ed8: #79c0ff;
        --bspkfw-fg-1e3a8a: #a5d6ff;
        --bspkfw-fg-1e40af: #79c0ff;
        --bspkfw-fg-1f2937: #e6edf3;
        --bspkfw-fg-2563eb: #58a6ff;
        --bspkfw-fg-374151: #c9d1d9;
        --bspkfw-fg-4b5563: #b1bac4;
        --bspkfw-fg-4f46e5: #a5a5ff;
        --bspkfw-fg-555: #b1bac4;
        --bspkfw-fg-5f6774: #9198a1;
        --bspkfw-fg-6366f1: #a5a5ff;
        --bspkfw-fg-64748b: #8b949e;
        --bspkfw-fg-6e6e6e: #8b949e;
        --bspkfw-fg-78350f: #f2cc60;
        --bspkfw-fg-7c3aed: #bc8cff;
        --bspkfw-fg-7f1d1d: #ffa198;
        --bspkfw-fg-854d0e: #e3b341;
        --bspkfw-fg-888: #7d8590;
        --bspkfw-fg-8a5a00: #e3b341;
        --bspkfw-fg-92400e: #e3b341;
        --bspkfw-fg-94a3b8: #8b949e;
        --bspkfw-fg-991b1b: #ff7b72;
        --bspkfw-fg-9ca3af: #7d8590;
        --bspkfw-fg-b45309: #e3b341;
        --bspkfw-fg-b91c1c: #ff7b72;
        --bspkfw-fg-c53030: #ff7b72;
        --bspkfw-fg-cbd5e1: #cbd5e1;
        --bspkfw-fg-d97706: #d29922;
        --bspkfw-fg-dc2626: #f85149;
        --bspkfw-fg-e5e7eb: #e5e7eb;
        --bspkfw-fg-ef4444: #f85149;
        --bspkfw-fg-f1f5f9: #f1f5f9;
        --bspkfw-fg-f87171: #f87171;
        --bspkfw-fg-f8fafc: #f8fafc;
        --bspkfw-fg-f9fafb: #f9fafb;
        --bspkfw-fg-fff: #fff;
        --bspkfw-fg-rgba-37-99-235-35: rgba(88,166,255,0.45);
        --bspkfw-page-bg: #0d1117;
        --bspkfw-sh-2563eb: #4493f8;
        --bspkfw-sh-bfdbfe: #1f3d66;
        --bspkfw-sh-f59e0b: #d29922;
        --bspkfw-sh-rgba-0-0-0-08: rgba(0,0,0,0.2);
        --bspkfw-sh-rgba-0-0-0-1: rgba(0,0,0,0.25);
        --bspkfw-sh-rgba-0-0-0-10: rgba(0,0,0,0.25);
        --bspkfw-sh-rgba-0-0-0-12: rgba(0,0,0,0.3);
        --bspkfw-sh-rgba-0-0-0-14: rgba(0,0,0,0.35);
        --bspkfw-sh-rgba-0-0-0-15: rgba(0,0,0,0.38);
        --bspkfw-sh-rgba-0-0-0-18: rgba(0,0,0,0.45);
        --bspkfw-sh-rgba-0-0-0-2: rgba(0,0,0,0.5);
        --bspkfw-sh-rgba-0-0-0-25: rgba(0,0,0,0.62);
        --bspkfw-sh-rgba-0-0-0-3: rgba(0,0,0,0.7);
        --bspkfw-sh-rgba-0-0-0-4: rgba(0,0,0,0.7);
        --bspkfw-sh-rgba-0-0-0-5: rgba(0,0,0,0.7);
        --bspkfw-sh-rgba-15-23-42-18: rgba(0,0,0,0.5);
        --bspkfw-sh-rgba-239-68-68-12: rgba(248,81,73,0.25);
        --bspkfw-sh-rgba-255-255-255-35: rgba(255,255,255,0.2);
        --bspkfw-sh-rgba-34-197-94-35: rgba(63,185,80,0.4);
        --bspkfw-sh-rgba-37-99-235-12: rgba(68,147,248,0.3);
        --bspkfw-sh-rgba-37-99-235-35: rgba(68,147,248,0.45);
    }
}

/* The accent and header families (CLAUDE/THEMING.md → "Branding an install").
   base.css rules paint the accent and the header chrome with THESE, never the
   internal tokens, so a theme that sets them recolours every button, link,
   focus ring, selected row, Kselect, chart and the header bar, in light and
   (under the two dark selectors below) in dark. Each reads its internal token,
   so a project that still overrides an internal one keeps its colour. They are
   declared again in both dark blocks so a theme's light value never leaks into
   dark; a theme gives its dark half under the same two selectors. */
:root {
    --bspkfw-accent:              var(--bspkfw-bg-2563eb);   /* primary fill: .btn-primary, the header CTA, busy bars */
    --bspkfw-accent-hover:        var(--bspkfw-bg-1d4ed8);   /* primary fill on hover */
    --bspkfw-accent-text:         var(--bspkfw-fg-2563eb);   /* links, accent text, the active tab */
    --bspkfw-accent-text-strong:  var(--bspkfw-fg-1e40af);   /* accent text on a soft fill: info badges, callout labels */
    --bspkfw-accent-text-deep:    var(--bspkfw-fg-1e3a8a);   /* the darkest accent text (a strong link on hover) */
    --bspkfw-accent-soft:         var(--bspkfw-bg-eff6ff);   /* a selected row, an info background */
    --bspkfw-accent-soft-2:       var(--bspkfw-bg-dbeafe);   /* a stronger soft fill: chips, an active tab */
    --bspkfw-accent-border:       var(--bspkfw-bd-bfdbfe);   /* the border (and ring) of a soft fill */
    --bspkfw-accent-border-hover: var(--bspkfw-bd-93c5fd);   /* that border on hover */
    --bspkfw-accent-bright:       var(--bspkfw-bd-60a5fa);   /* a lighter accent: toolbar focus outlines, callout rules */
    --bspkfw-focus-ring:          var(--bspkfw-bd-2563eb);   /* focus outlines */
    --bspkfw-header-bg:           var(--bspkfw-bg-1e293b);   /* the header bar, its scroll cues, the phone drawer */
    --bspkfw-header-fg:           var(--bspkfw-fg-cbd5e1);   /* nav items, header pills, the login link */
    --bspkfw-header-fg-strong:    var(--bspkfw-fg-f8fafc);   /* the site title, a hovered / active nav item */
    --bspkfw-header-fg-hover:     var(--bspkfw-fg-f1f5f9);   /* a hovered header pill or button, the unread bell */
    --bspkfw-header-muted:        var(--bspkfw-fg-94a3b8);   /* the bell at rest */
    --bspkfw-header-hover-bg:     var(--bspkfw-bg-334155);   /* a hovered nav item, pill or button */
    --bspkfw-header-border:       var(--bspkfw-bd-334155);   /* pill and button outlines */
    /* Derived: a theme need not set these. */
    --bspkfw-header-focus:        var(--bspkfw-accent-bright);   /* focus outlines on the bar */
    --bspkfw-accent-line:         var(--bspkfw-accent);   /* an accent border or indicator: a primary button's edge, the active tab's underline */
    --bspkfw-accent-hover-border: var(--bspkfw-accent-hover);   /* that edge on hover */
    --bspkfw-accent-text-hover:   var(--bspkfw-accent-hover);   /* accent text on hover */
    --bspkfw-focus-glow:          color-mix(in srgb, var(--bspkfw-focus-ring) 12%, transparent);   /* the halo round a focused field */
    --bspkfw-focus-glow-strong:   color-mix(in srgb, var(--bspkfw-focus-ring) 35%, transparent);   /* a hovered / focused card's ring */
    --bspkfw-accent-tint:         color-mix(in srgb, var(--bspkfw-focus-ring) 12%, transparent);   /* a translucent accent wash */
    --bspkfw-accent-tint-strong:  color-mix(in srgb, var(--bspkfw-focus-ring) 18%, transparent);   /* a busy bar's track */
    --bspkfw-accent-text-tint:    color-mix(in srgb, var(--bspkfw-accent-text) 35%, transparent);   /* a focus mark drawn in accent text */
}
/* Dark: forced, or auto with a dark OS. Keep the two blocks identical. */
:root[data-color-scheme="dark"] {
    --bspkfw-accent:              var(--bspkfw-bg-2563eb);
    --bspkfw-accent-hover:        var(--bspkfw-bg-1d4ed8);
    --bspkfw-accent-text:         var(--bspkfw-fg-2563eb);
    --bspkfw-accent-text-strong:  var(--bspkfw-fg-1e40af);
    --bspkfw-accent-text-deep:    var(--bspkfw-fg-1e3a8a);
    --bspkfw-accent-soft:         var(--bspkfw-bg-eff6ff);
    --bspkfw-accent-soft-2:       var(--bspkfw-bg-dbeafe);
    --bspkfw-accent-border:       var(--bspkfw-bd-bfdbfe);
    --bspkfw-accent-border-hover: var(--bspkfw-bd-93c5fd);
    --bspkfw-accent-bright:       var(--bspkfw-bd-60a5fa);
    --bspkfw-focus-ring:          var(--bspkfw-bd-2563eb);
    --bspkfw-header-bg:           var(--bspkfw-bg-1e293b);
    --bspkfw-header-fg:           var(--bspkfw-fg-cbd5e1);
    --bspkfw-header-fg-strong:    var(--bspkfw-fg-f8fafc);
    --bspkfw-header-fg-hover:     var(--bspkfw-fg-f1f5f9);
    --bspkfw-header-muted:        var(--bspkfw-fg-94a3b8);
    --bspkfw-header-hover-bg:     var(--bspkfw-bg-334155);
    --bspkfw-header-border:       var(--bspkfw-bd-334155);
    /* Derived (dark formulas). */
    --bspkfw-header-focus:        var(--bspkfw-accent-bright);
    --bspkfw-accent-line:         var(--bspkfw-focus-ring);
    --bspkfw-accent-hover-border: var(--bspkfw-focus-ring);
    --bspkfw-accent-text-hover:   var(--bspkfw-accent-text-strong);
    --bspkfw-focus-glow:          color-mix(in srgb, var(--bspkfw-focus-ring) 30%, transparent);
    --bspkfw-focus-glow-strong:   color-mix(in srgb, var(--bspkfw-focus-ring) 45%, transparent);
    --bspkfw-accent-tint:         color-mix(in srgb, var(--bspkfw-focus-ring) 18%, transparent);
    --bspkfw-accent-tint-strong:  color-mix(in srgb, var(--bspkfw-focus-ring) 25%, transparent);
    --bspkfw-accent-text-tint:    color-mix(in srgb, var(--bspkfw-accent-text) 45%, transparent);
}
@media (prefers-color-scheme: dark) {
    :root[data-color-scheme="auto"] {
        --bspkfw-accent:              var(--bspkfw-bg-2563eb);
        --bspkfw-accent-hover:        var(--bspkfw-bg-1d4ed8);
        --bspkfw-accent-text:         var(--bspkfw-fg-2563eb);
        --bspkfw-accent-text-strong:  var(--bspkfw-fg-1e40af);
        --bspkfw-accent-text-deep:    var(--bspkfw-fg-1e3a8a);
        --bspkfw-accent-soft:         var(--bspkfw-bg-eff6ff);
        --bspkfw-accent-soft-2:       var(--bspkfw-bg-dbeafe);
        --bspkfw-accent-border:       var(--bspkfw-bd-bfdbfe);
        --bspkfw-accent-border-hover: var(--bspkfw-bd-93c5fd);
        --bspkfw-accent-bright:       var(--bspkfw-bd-60a5fa);
        --bspkfw-focus-ring:          var(--bspkfw-bd-2563eb);
        --bspkfw-header-bg:           var(--bspkfw-bg-1e293b);
        --bspkfw-header-fg:           var(--bspkfw-fg-cbd5e1);
        --bspkfw-header-fg-strong:    var(--bspkfw-fg-f8fafc);
        --bspkfw-header-fg-hover:     var(--bspkfw-fg-f1f5f9);
        --bspkfw-header-muted:        var(--bspkfw-fg-94a3b8);
        --bspkfw-header-hover-bg:     var(--bspkfw-bg-334155);
        --bspkfw-header-border:       var(--bspkfw-bd-334155);
        /* Derived (dark formulas). */
        --bspkfw-header-focus:        var(--bspkfw-accent-bright);
        --bspkfw-accent-line:         var(--bspkfw-focus-ring);
        --bspkfw-accent-hover-border: var(--bspkfw-focus-ring);
        --bspkfw-accent-text-hover:   var(--bspkfw-accent-text-strong);
        --bspkfw-focus-glow:          color-mix(in srgb, var(--bspkfw-focus-ring) 30%, transparent);
        --bspkfw-focus-glow-strong:   color-mix(in srgb, var(--bspkfw-focus-ring) 45%, transparent);
        --bspkfw-accent-tint:         color-mix(in srgb, var(--bspkfw-focus-ring) 18%, transparent);
        --bspkfw-accent-tint-strong:  color-mix(in srgb, var(--bspkfw-focus-ring) 25%, transparent);
        --bspkfw-accent-text-tint:    color-mix(in srgb, var(--bspkfw-accent-text) 45%, transparent);
    }
}

/* ============================================================
   Form-field tokens — .form-control AND the Kselect widget
   ============================================================
   One palette for "things that look like a form field", so a theme can
   restyle inputs and the searchable-select widget together instead of
   chasing two vocabularies. Every token defaults to the exact literal
   .form-control already used, and every consuming declaration repeats
   that literal as its var() fallback — so an untouched install renders
   byte-identically and a rule stays correct in isolation. Same
   discipline as the density tokens above.

   WHY THIS EXISTS: kselect ships ~44 of its own --ks-* custom
   properties with hardcoded light values. Before this block, the
   midnight theme darkened .form-control and overrode none of them, so
   the enhanced control and its body-level dropdown rendered white on a
   dark page. Mapping --ks-* through these tokens (below) means a theme
   sets ONE palette and both surfaces follow.

   A theme overrides the --bspkfw-field-* tokens and never touches
   --ks-* directly (see public_html/themes/midnight/theme.css).

   Load-order note: kselect 1.7.0 declares its defaults at
   :where(:root) — zero specificity — so this :root block wins even
   though kselect.min.css is linked after base.css and after the theme
   (LayoutRenderer). On kselect < 1.7.0 it would have lost, and the
   mapping had to live on `body` to win by proximity instead. */
:root {
    --bspkfw-field-bg:              var(--bspkfw-bg-fff);
    --bspkfw-field-text:            var(--bspkfw-fg-1f2937);
    --bspkfw-field-border:          var(--bspkfw-bd-d1d5db);
    --bspkfw-field-border-focus:    var(--bspkfw-focus-ring);
    --bspkfw-field-focus-ring:      var(--bspkfw-focus-glow);
    --bspkfw-field-placeholder:     var(--bspkfw-fg-5f6774);
    --bspkfw-field-muted:           var(--bspkfw-fg-5f6774);   /* group labels, badges, arrow, clear */
    --bspkfw-field-surface:         var(--bspkfw-bg-f9fafb);   /* dropdown search bar, group headers */
    --bspkfw-field-hover:           var(--bspkfw-bg-f3f4f6);   /* option row hover / keyboard focus */
    --bspkfw-field-accent:          var(--bspkfw-accent-text);   /* selected ink, checkbox, tag text */
    --bspkfw-field-accent-soft:     var(--bspkfw-accent-soft);   /* tag fill, selected-row fill */
    --bspkfw-field-accent-border:   var(--bspkfw-accent-border);
    --bspkfw-field-disabled-bg:     var(--bspkfw-bg-f3f4f6);
    --bspkfw-field-disabled-fg:     var(--bspkfw-fg-9ca3af);
    --bspkfw-field-disabled-border: var(--bspkfw-bd-e5e7eb);
    --bspkfw-field-radius:          6px;
    --bspkfw-field-invalid:         var(--bspkfw-fg-f87171);
    --bspkfw-field-invalid-ring:    var(--bspkfw-bd-rgba-239-68-68-12);

    /* Kselect ← framework. Two deliberate departures from kselect's own
       defaults, both for consistency rather than taste: its accent is
       indigo (#6366f1) where every other focus ring, link and primary
       button here is blue (#2563eb), and its font stack is a hardcoded
       'Inter' where the rest of the page inherits system-ui. Geometry is
       pulled back to .form-control's 6px so an enhanced select and a
       plain one sit level in the same form. */
    --ks-font-family:               inherit;
    --ks-font-size:                 14px;
    --ks-color-bg:                  var(--bspkfw-field-bg, var(--bspkfw-bg-fff));
    --ks-color-text:                var(--bspkfw-field-text, var(--bspkfw-fg-1f2937));
    --ks-color-border:              var(--bspkfw-field-border, var(--bspkfw-bd-d1d5db));
    --ks-color-border-focus:        var(--bspkfw-field-border-focus, var(--bspkfw-focus-ring));
    --ks-color-placeholder:         var(--bspkfw-field-placeholder, var(--bspkfw-fg-5f6774));
    --ks-color-disabled-bg:         var(--bspkfw-field-disabled-bg, var(--bspkfw-bg-f3f4f6));
    --ks-color-disabled-text:       var(--bspkfw-field-disabled-fg, var(--bspkfw-fg-9ca3af));
    --ks-color-disabled-border:     var(--bspkfw-field-disabled-border, var(--bspkfw-bd-e5e7eb));
    --ks-color-option-hover:        var(--bspkfw-field-hover, var(--bspkfw-bg-f3f4f6));
    --ks-color-option-selected:     var(--bspkfw-field-accent, var(--bspkfw-accent-text));
    --ks-color-option-selected-bg:  var(--bspkfw-field-accent-soft, var(--bspkfw-accent-soft));
    --ks-color-option-selected-fg:  var(--bspkfw-field-accent, var(--bspkfw-accent-text));
    --ks-color-group-header:        var(--bspkfw-field-surface, var(--bspkfw-bg-f9fafb));
    --ks-color-group-label:         var(--bspkfw-field-muted, var(--bspkfw-fg-5f6774));
    --ks-color-search-bg:           var(--bspkfw-field-surface, var(--bspkfw-bg-f9fafb));
    --ks-color-tag-bg:              var(--bspkfw-field-accent-soft, var(--bspkfw-accent-soft));
    --ks-color-tag-text:            var(--bspkfw-field-accent, var(--bspkfw-accent-text));
    --ks-color-tag-border:          var(--bspkfw-field-accent-border, var(--bspkfw-accent-border));
    --ks-color-tag-remove:          var(--bspkfw-field-accent, var(--bspkfw-accent-text));
    --ks-color-tag-remove-hover:    var(--bspkfw-field-invalid, var(--bspkfw-bg-f87171));
    --ks-color-clear:               var(--bspkfw-field-muted, var(--bspkfw-fg-5f6774));
    --ks-color-arrow:               var(--bspkfw-field-muted, var(--bspkfw-fg-5f6774));
    --ks-color-arrow-open:          var(--bspkfw-field-border-focus, var(--bspkfw-accent-text));
    --ks-color-checkbox-border:     var(--bspkfw-field-border, var(--bspkfw-bd-d1d5db));
    --ks-color-checkbox-checked:    var(--bspkfw-field-accent, var(--bspkfw-accent-text));
    --ks-color-limit-badge-bg:      var(--bspkfw-field-surface, var(--bspkfw-bg-f9fafb));
    --ks-color-limit-badge-text:    var(--bspkfw-field-muted, var(--bspkfw-fg-5f6774));
    --ks-radius-control:            var(--bspkfw-field-radius, 6px);
    --ks-radius-dropdown:           var(--bspkfw-field-radius, 6px);
    /* Left at kselect's defaults deliberately: --ks-color-limit-reached-*
       (an amber warning state, semantic rather than themed),
       --ks-color-option-selected-text / -selected-row / -checkbox-tick
       (derived from the tokens above by kselect itself), --ks-radius-tag,
       --ks-radius-checkbox, --ks-shadow-dropdown, --ks-transition,
       --ks-z-index, --ks-line-height. */
}

/* ============================================================
   Site chrome
   ============================================================ */
.site-header {
    background: var(--bspkfw-header-bg);
    padding: 0 24px;
    height: 52px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    /* Containing block for the scroll-mode nav edge cues (.site-nav-scroll-btn) —
       they anchor over the nav's edges, OUTSIDE the scroll container. */
    position: relative;
}

.site-title {
    color: var(--bspkfw-header-fg-strong);
    font-size: 16px;
    font-weight: 600;
    text-decoration: none;
    /* The header's only shrinkable item. Everything to its right (region
       pill, bell, user menu / guest auth affordances) is fixed-size, so
       without this a long brand + a busy right-hand cluster pushes the
       chrome off the side of a phone rather than truncating. Inert whenever
       the title fits — which is every install whose brand isn't already too
       wide for the viewport. */
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The admin's header logo (SiteImages `header_logo`, LayoutRenderer::
   siteLogoBrandHtml()): the 80 px variant shown 40 px tall beside the short
   name, which keeps the title's ellipsis. */
.site-title:has(> .site-logo) {
    display: inline-flex;
    align-items: center;
    gap: 8px;
}
.site-logo {
    height: 40px;
    width: auto;
    max-width: 160px;
    flex: 0 0 auto;
    object-fit: contain;
}
.site-title-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* /settings → Branding (BrandingSettingsForm): each image slot's current picture. */
.branding-preview {
    display: flex;
    align-items: center;
    min-height: 48px;
    max-width: 320px;
    margin: 12px 0 4px;
    padding: 8px;
    border: 1px dashed var(--bspkfw-border);
    border-radius: 6px;
    background: var(--bspkfw-surface-2);
}
.branding-preview img {
    max-width: 100%;
    max-height: 120px;
    object-fit: contain;
}
.branding-preview-empty {
    color: var(--bspkfw-muted);
    font-size: 13px;
}

/* Brand MENU (Project::brandMenu(); LayoutRenderer::renderBrandMenu +
   /js/brand-menu.js): the site-title as a <button> that opens a small menu.
   It keeps the .site-title class, so the colour, size, weight, a theme's
   override and the squeeze above all apply unchanged; this only strips the
   UA button chrome. The brand sits in .site-title-text, which does the
   ellipsizing so the caret stays visible as the title shrinks. */
.site-title--menu {
    display: inline-flex;
    align-items: center;
    margin: 0;
    padding: 0;
    border: 0;
    background: transparent;
    font-family: inherit;
    line-height: inherit;
    letter-spacing: inherit;
    text-align: left;
    cursor: pointer;
}
.site-title--menu:focus-visible {
    outline: 2px solid currentColor;
    outline-offset: 3px;
    border-radius: 4px;
}
.site-title-text {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.site-title-caret {
    flex-shrink: 0;
    margin-left: 6px;
    font-size: 11px;
    line-height: 1;
    opacity: 0.85;
    transition: transform 120ms ease;
}
.site-title--menu[aria-expanded="true"] > .site-title-caret { transform: rotate(180deg); }
/* The panel hangs off the HEADER (position: relative), not the button —
   the button clips its own overflow to ellipsize. brand-menu.js sets `left`
   under the button and keeps the panel on screen. */
.site-brand-menu {
    position: absolute;
    top: 100%;
    left: 24px;
    min-width: 220px;
    max-width: calc(100vw - 16px);
    max-height: calc(100vh - 64px);
    overflow-y: auto;
    box-sizing: border-box;
    background: var(--bspkfw-bg-fff);
    border: 1px solid var(--bspkfw-bd-e5e7eb);
    border-radius: 0 0 8px 8px;
    box-shadow: 0 6px 20px var(--bspkfw-sh-rgba-0-0-0-12);
    padding: 4px 0;
    z-index: 200;
}
.site-brand-menu[hidden] { display: none; }
.site-brand-menu-item {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    box-sizing: border-box;
    margin: 0;
    padding: 8px 14px;
    border: 0;
    background: transparent;
    color: var(--bspkfw-fg-374151);
    font: inherit;
    font-size: 14px;
    text-align: left;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
}
.site-brand-menu-item:hover,
.site-brand-menu-item:focus-visible { background: var(--bspkfw-bg-f3f4f6); color: var(--bspkfw-fg-111827); }
.site-brand-menu-item.is-active { font-weight: 700; color: var(--bspkfw-fg-111827); }
/* The current choice's tick — a radio item's checked state, or a link the
   project marked active. Drawn, not a ✓ glyph: a font without one draws
   nothing at all. */
.site-brand-menu-item.is-active::after {
    content: "";
    flex-shrink: 0;
    width: 5px;
    height: 10px;
    margin: -3px 2px 0 auto;
    padding: 0;
    border: solid currentColor;
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
}
.site-brand-menu-item.is-active > .site-brand-menu-label { margin-right: 12px; }
.site-brand-menu-icon {
    flex-shrink: 0;
    width: 20px;
    height: 20px;
    object-fit: contain;
    border-radius: 3px;
}
.site-brand-menu-label {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}
.site-brand-menu-heading {
    padding: 8px 14px 2px;
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--bspkfw-fg-5f6774);
    white-space: nowrap;
}
.site-brand-menu-divider {
    height: 1px;
    margin: 4px 0;
    background: var(--bspkfw-bg-e5e7eb);
}
@media print {
    .site-title-caret, .site-brand-menu { display: none !important; }
}

.site-nav-right {
    display: flex;
    align-items: center;
    gap: 12px;
    /* Never squeezed — the title above absorbs the shortfall instead. */
    flex-shrink: 0;
}

/* Optional project main-nav slot (Project::topNavItems()). Rendered between
   the site-title and the right-side chrome; absent entirely unless the
   project opts in. margin-right:auto keeps the links beside the title and
   pushes .site-nav-right to the far right. Projects override freely. */
.site-nav-main {
    display: flex;
    align-items: center;
    gap: 4px;
    margin-left: 24px;
    margin-right: auto;
}

.site-nav-main-item {
    color: var(--bspkfw-header-fg);
    text-decoration: none;
    font-size: 14px;
    font-weight: 500;
    padding: 6px 10px;
    border-radius: 6px;
    line-height: 1;
}

.site-nav-main-item:hover,
.site-nav-main-item:focus-visible {
    color: var(--bspkfw-header-fg-strong);
    background: var(--bspkfw-header-hover-bg);
}

/* Current/active section — set by Project::topNavItems() items with
   'active' => true. Theme-neutral: brighter text, bolder weight, and an
   accent underline drawn in currentColor (no layout shift, no fixed colour),
   so it stays visible against a project-restyled nav and is overridable. */
.site-nav-main-item.is-active {
    color: var(--bspkfw-header-fg-strong);
    font-weight: 700;
    text-decoration: underline;
    text-decoration-thickness: 2px;
    text-underline-offset: 6px;
}

/* Dropdown groups in the project main-nav — topNavItems() items with
   `children` (LayoutRenderer::renderTopNav + /js/site-nav.js). The nav and
   each group stretch to the full header height so the flyout panel sits
   flush under the header with NO hover dead-zone between trigger and panel
   (a gap would drop :hover mid-crossing and flicker the menu shut). Items
   stay vertically centered via the existing align-items, so a flat nav is
   visually unchanged. Hover-open is pointer-device-only (hover:hover);
   click/touch + keyboard toggle `.is-open` via site-nav.js. */
.site-nav-main { align-self: stretch; }
.site-nav-main-group {
    position: relative;
    display: flex;
    align-items: center;
    align-self: stretch;
}
/* Trigger may be a <button> (parent without href) — strip the UA button
   chrome so it reads identically to the <a> items beside it. */
.site-nav-main-trigger {
    background: transparent;
    border: 0;
    font: inherit;
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
}
.site-nav-main-trigger.is-active { font-weight: 700; }
.site-nav-caret {
    font-size: 9px;
    line-height: 1;
    margin-left: 5px;
    transition: transform 120ms ease;
}
.site-nav-main-group.is-open .site-nav-caret { transform: rotate(180deg); }

.site-nav-dropdown {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    min-width: 180px;
    max-width: calc(100vw - 16px);
    background: var(--bspkfw-bg-fff);
    border: 1px solid var(--bspkfw-bd-e5e7eb);
    border-radius: 0 0 8px 8px;
    box-shadow: 0 6px 20px var(--bspkfw-sh-rgba-0-0-0-12);
    padding: 4px 0;
    z-index: 200;
}
.site-nav-main-group.is-open > .site-nav-dropdown { display: block; }
@media (hover: hover) {
    .site-nav-main-group:hover > .site-nav-dropdown { display: block; }
}
.site-nav-dropdown-item {
    display: block;
    padding: 8px 14px;
    color: var(--bspkfw-fg-374151);
    text-decoration: none;
    font-size: 14px;
    white-space: nowrap;
}
.site-nav-dropdown-item:hover,
.site-nav-dropdown-item:focus-visible { background: var(--bspkfw-bg-f3f4f6); color: var(--bspkfw-fg-111827); }
.site-nav-dropdown-item.is-active { font-weight: 700; color: var(--bspkfw-fg-111827); }

/* Small-screen strategy: when the nav's items outgrow the header, the nav
   ITSELF scrolls and the page never does. site-nav.js measures on load /
   resize / font-load and toggles `.is-overflowing` — engagement is by
   MEASUREMENT, not a breakpoint, because the overflow point is a function
   of how many top-level menus the project defines (a twelve-menu project
   overflows at ~1370px; a four-menu one may never). A nav that fits keeps
   today's rules byte-for-byte: no scroll container, no clipping context.
   `min-width: 0` lets the flex item shrink below its content so the
   scrolling is contained; children keep their natural widths. The
   scrollbar is hidden (chip-row idiom — touch scrolls by momentum,
   desktop by trackpad / shift-wheel, and keyboard focus auto-scrolls
   items into view), so engaging the mode causes no height shift. */
.site-nav-main.is-overflowing {
    min-width: 0;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
}
.site-nav-main.is-overflowing::-webkit-scrollbar { display: none; }
.site-nav-main.is-overflowing > .site-nav-main-item,
.site-nav-main.is-overflowing > .site-nav-main-group { flex-shrink: 0; }
.site-nav-main.is-overflowing .site-nav-main-item { white-space: nowrap; }
/* A scroll container clips absolutely-positioned descendants — the same
   trap as `overflow: hidden` on a table wrapper clipping the row-actions
   popup, and the same escape: in overflow mode the dropdown is
   `position: fixed`, anchored to its group's rect by site-nav.js at open
   time (viewport coordinates are immune to every ancestor scroll/clip).
   The static `top: 100%` / `left: 0` are neutralised because they mean
   something entirely different against the viewport. */
.site-nav-main.is-overflowing .site-nav-dropdown {
    position: fixed;
    top: auto;
    left: auto;
}

/* Scroll-mode edge cues (site-nav.js, `data-nav-overflow="scroll"` navs).
   The hidden scrollbar removed the only signal that more content existed —
   these stand in its place: a gradient says "there is more" and the chevron
   gives a MOUSE a way to get it. Absolutely positioned in the HEADER (not
   the nav — anything inside the scroll container scrolls away with the
   content it cues), anchored over the nav's edges by JS. Only the edge(s)
   with hidden content show one. `--bspkfw-nav-cue` lets a themed header
   keep the gradient in its own colour. */
.site-nav-scroll-btn {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 0;
    padding: 0;
    margin: 0;
    cursor: pointer;
    color: var(--bspkfw-header-fg-strong);
    font-size: 18px;
    line-height: 1;
    z-index: 150;   /* above the nav items, below the flyout panels (200) */
    background: linear-gradient(to right, var(--bspkfw-nav-cue, var(--bspkfw-header-bg)) 45%, transparent);
}
.site-nav-scroll-btn-right {
    background: linear-gradient(to left, var(--bspkfw-nav-cue, var(--bspkfw-header-bg)) 45%, transparent);
}
/* display:flex above would defeat the [hidden] UA rule — restore it. */
.site-nav-scroll-btn[hidden] { display: none; }

/* Menu-mode overflow (the DEFAULT — no data-nav-overflow attribute): the
   items that don't fit are hidden from the bar and re-listed inside a
   JS-built trailing "More ▾" dropdown (site-nav.js syncMenuMode). The
   group is only created once a nav has actually overflowed — a nav that
   fits keeps today's markup byte-for-byte. */
.site-nav-main > .is-in-overflow-menu { display: none; }
.site-nav-more-group { display: none; }
.site-nav-more-group.is-engaged { display: flex; }
/* At small widths most of the nav lives in here — let the panel scroll
   rather than run off the bottom of the screen. */
.site-nav-more-group .site-nav-dropdown {
    max-height: calc(100vh - 70px);
    overflow-y: auto;
}
/* Flattened group inside the More panel: the parent introduces its children
   in the ONE panel (no nested flyout) — as a link when it had an href
   (.site-nav-dropdown-parent), else a plain heading. */
.site-nav-dropdown-heading {
    padding: 8px 14px 2px;
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--bspkfw-fg-5f6774);
    white-space: nowrap;
}
.site-nav-dropdown-parent { font-weight: 600; }
.site-nav-dropdown-subitem { padding-left: 26px; }
.site-nav-more-group .site-nav-dropdown-heading:not(:first-child),
.site-nav-more-group .site-nav-dropdown-parent:not(:first-child) {
    border-top: 1px solid var(--bspkfw-bd-e5e7eb);
    margin-top: 4px;
    padding-top: 10px;
}

/* Count pill after a nav label (optional `badge` key on any topNavItems()
   item; a group's trigger shows its own badge + the sum of its children's,
   so a collapsed group still surfaces the total). */
.site-nav-badge {
    display: inline-block;
    min-width: 18px;
    padding: 1px 5px;
    margin-left: 6px;
    border-radius: 999px;
    background: var(--bspkfw-bg-dc2626);
    color: var(--bspkfw-fg-fff);
    font-size: 11px;
    font-weight: 600;
    line-height: 16px;
    text-align: center;
}

/* Phone nav drawer (site-nav.js syncMenuMode / syncScrollMode → engageDrawer).
   At the COMPACT end of either overflow mode — when fewer than half of the
   top-level entries fit on the bar, i.e. More would hold the majority — the
   bar and More give way to a ☰ that opens a full-height slide-in drawer
   listing every entry, groups as fold-out sections. Engagement is measured,
   not a breakpoint (a twenty-menu project and a five-menu one flip at
   different widths); the header carries `has-nav-drawer` while engaged. The
   drawer is a child of <body> (a themed header with a transform /
   backdrop-filter would clip a fixed child), opened with `is-open`. Opt out
   per install via Project::navDrawer() → false. Palette follows the header
   (the bar's own colours) and is themeable through the custom properties
   below — a project with a light header sets `--bspkfw-nav-drawer-bg` etc. */
.site-nav-drawer-trigger {
    display: none;   /* shown only while engaged (below) */
    align-items: center;
    justify-content: center;
    position: relative;
    flex-shrink: 0;
    width: 40px;
    height: 40px;
    margin: 0 8px 0 -8px;
    padding: 0;
    border: 0;
    border-radius: 8px;
    background: transparent;
    color: var(--bspkfw-header-fg-strong);
    cursor: pointer;
}
.site-nav-drawer-trigger:hover,
.site-nav-drawer-trigger:focus-visible { background: var(--bspkfw-nav-drawer-hover, var(--bspkfw-header-hover-bg)); }
.site-header.has-nav-drawer > .site-nav-drawer-trigger { display: inline-flex; }
.site-header.has-nav-drawer > .site-nav-main { display: none; }
/* With the bar gone the brand would float to the header's centre at tablet
   widths (space-between over three children) — keep it beside the ☰, where
   it sits at every other width. */
.site-header.has-nav-drawer > .site-title { margin-right: auto; }
/* Three bars drawn in CSS — the ☰ glyph renders as an emoji-style icon in
   some system fonts. */
.site-nav-drawer-icon,
.site-nav-drawer-icon::before,
.site-nav-drawer-icon::after {
    display: block;
    width: 20px;
    height: 2px;
    border-radius: 2px;
    background: currentColor;
}
.site-nav-drawer-icon { position: relative; }
.site-nav-drawer-icon::before,
.site-nav-drawer-icon::after { content: ''; position: absolute; left: 0; }
.site-nav-drawer-icon::before { top: -6px; }
.site-nav-drawer-icon::after  { top: 6px; }
/* The roll-up pill sits on the icon's corner. */
.site-nav-drawer-trigger > .site-nav-badge {
    position: absolute;
    top: 1px;
    right: 0;
    margin: 0;
}

/* Scroll lock while ANY drawer is open (drawer.js — the site-nav drawer and
   page drawers share it): body is fixed at minus the scroll offset (set on
   the style property) — the technique iOS Safari honours for touch, where
   overflow:hidden on body is ignored — and restored to the exact pixel on
   close. `site-nav-drawer-open` is kept alongside for project CSS that
   keys on it. */
body.site-nav-drawer-open,
body.bspkfw-drawer-locked {
    position: fixed;
    left: 0;
    right: 0;
    width: 100%;
    overflow: hidden;
}

.site-nav-drawer-backdrop {
    position: fixed;
    top: 0; right: 0; bottom: 0; left: 0;
    background: var(--bspkfw-nav-drawer-backdrop, var(--bspkfw-bg-rgba-15-23-42-55));
    z-index: 300;   /* above the header flyouts (200) */
    opacity: 0;
    visibility: hidden;
    transition: opacity 200ms ease, visibility 0s linear 200ms;
}
.site-nav-drawer-backdrop.is-open {
    opacity: 1;
    visibility: visible;
    transition: opacity 200ms ease;
}
.site-nav-drawer {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    height: 100vh;
    height: 100dvh;
    width: min(85vw, 22rem);
    max-width: 100%;
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
    /* Notch / home indicator: the panel's own padding, so the head and the
       last entry stay clear of the hardware. */
    padding-top: env(safe-area-inset-top);
    padding-bottom: env(safe-area-inset-bottom);
    padding-left: env(safe-area-inset-left);
    background: var(--bspkfw-nav-drawer-bg, var(--bspkfw-nav-cue, var(--bspkfw-header-bg)));
    color: var(--bspkfw-nav-drawer-fg, var(--bspkfw-header-fg));
    box-shadow: 4px 0 24px var(--bspkfw-sh-rgba-0-0-0-3);
    z-index: 301;
    overscroll-behavior: contain;
    transform: translateX(-100%);
    visibility: hidden;   /* out of the tab order + a11y tree while shut */
    transition: transform 220ms ease, visibility 0s linear 220ms;
}
.site-nav-drawer.is-open {
    transform: none;
    visibility: visible;
    transition: transform 220ms ease;
}
@media (prefers-reduced-motion: reduce) {
    .site-nav-drawer, .site-nav-drawer-backdrop { transition: none; }
}
.site-nav-drawer-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    flex-shrink: 0;
    min-height: 52px;
    padding: 4px 8px 4px 16px;
    border-bottom: 1px solid var(--bspkfw-nav-drawer-divider, var(--bspkfw-bd-rgba-255-255-255-12));
}
.site-nav-drawer-brand {
    min-width: 0;
    color: var(--bspkfw-nav-drawer-heading, var(--bspkfw-header-fg-strong));
    font-size: 16px;
    font-weight: 600;
    text-decoration: none;
    overflow-wrap: anywhere;   /* the bar ellipsizes the brand; here it may wrap */
}
.site-nav-drawer-close {
    flex-shrink: 0;
    width: 44px;
    height: 44px;
    padding: 0;
    border: 0;
    border-radius: 8px;
    background: transparent;
    color: inherit;
    font-size: 26px;
    line-height: 1;
    cursor: pointer;
}
.site-nav-drawer-close:hover,
.site-nav-drawer-close:focus-visible {
    background: var(--bspkfw-nav-drawer-hover, var(--bspkfw-header-hover-bg));
    color: var(--bspkfw-nav-drawer-heading, var(--bspkfw-header-fg-strong));
}
.site-nav-drawer-nav {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    padding: 8px 0;
}
/* Every row full-width and at least 44px tall — one-thumb targets. */
.site-nav-drawer-item,
.site-nav-drawer-section-toggle {
    display: flex;
    align-items: center;
    width: 100%;
    box-sizing: border-box;
    min-height: 44px;
    padding: 10px 16px;
    color: inherit;
    font: inherit;
    font-size: 15px;
    font-weight: 500;
    text-align: left;
    text-decoration: none;
}
.site-nav-drawer-section-toggle {
    border: 0;
    background: transparent;
    font-weight: 600;
    cursor: pointer;
}
.site-nav-drawer-item:hover,
.site-nav-drawer-item:focus-visible,
.site-nav-drawer-section-toggle:hover,
.site-nav-drawer-section-toggle:focus-visible {
    background: var(--bspkfw-nav-drawer-hover, var(--bspkfw-header-hover-bg));
    color: var(--bspkfw-nav-drawer-heading, var(--bspkfw-header-fg-strong));
}
.site-nav-drawer-item.is-active,
.site-nav-drawer-section-toggle.is-active {
    color: var(--bspkfw-nav-drawer-heading, var(--bspkfw-header-fg-strong));
    font-weight: 700;
}
/* The "you are here" cue: an accent bar in currentColor, theme-neutral like
   the bar's underline. */
.site-nav-drawer-item.is-active { box-shadow: inset 3px 0 0 currentColor; }
.site-nav-drawer-subitem { padding-left: 32px; font-weight: 400; }
.site-nav-drawer-parent  { padding-left: 32px; font-weight: 600; }
.site-nav-drawer-item > .site-nav-badge,
.site-nav-drawer-section-toggle > .site-nav-badge { margin-left: 8px; }
.site-nav-drawer-section-toggle > .site-nav-caret {
    margin-left: auto;
    padding-left: 12px;
    font-size: 11px;
}
.site-nav-drawer-section.is-open > .site-nav-drawer-section-toggle > .site-nav-caret { transform: rotate(180deg); }
.site-nav-drawer-section-list { padding-bottom: 4px; }
.site-nav-drawer-section-list[hidden] { display: none; }
/* The account section (phone bar only — site-nav.js builds it on open): a
   heading with the user's name, the user menu's entries, dividers. */
.site-nav-drawer-account { margin-top: 8px; border-top: 1px solid var(--bspkfw-nav-drawer-divider, var(--bspkfw-bd-rgba-255-255-255-12)); padding-top: 8px; }
.site-nav-drawer-heading {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 16px 6px;
    color: var(--bspkfw-nav-drawer-heading, var(--bspkfw-header-fg-strong));
    font-size: 13px;
    font-weight: 700;
    overflow-wrap: anywhere;
}
.site-nav-drawer-heading > .user-menu-photo { flex-shrink: 0; }
.site-nav-drawer-divider { height: 1px; margin: 8px 16px; background: var(--bspkfw-nav-drawer-divider, var(--bspkfw-bd-rgba-255-255-255-12)); }
button.site-nav-drawer-item { border: 0; background: transparent; cursor: pointer; }
.site-nav-drawer-item[aria-pressed="true"] { font-weight: 700; }
.site-nav-drawer-item .user-menu-badge { margin-left: 8px; }
@media print {
    .site-nav-drawer-trigger, .site-nav-drawer, .site-nav-drawer-backdrop { display: none !important; }
}

/* ============================================================
   The phone bar — MobileBar::html() + mobile-bar.js
   ============================================================
   ≤640px (the manage cards' breakpoint, Drawer `sm`): a bar fixed to the
   bottom with the project's buttons and the MENU button at the bottom right.
   The media query is the decision (site-nav.js and mobile-bar.js ask the
   bar's computed display), so a phone paints it on the first frame. While it
   shows on the framework layout (`body:has(> .mobile-bar)`) the header keeps the
   brand and its small controls; the nav bar, the ☰ and the user menu step
   aside, because the drawer — now sliding in from the RIGHT — holds them.
   `--bspkfw-mobile-bar-offset` is the room the bar takes: anything else
   pinned to the bottom of the screen sits above it. */
.mobile-bar { display: none; }
@media (max-width: 640px) {
    body:has(> .mobile-bar) {
        --bspkfw-mobile-bar-offset: calc(56px + env(safe-area-inset-bottom));
        padding-bottom: var(--bspkfw-mobile-bar-offset);
    }
    .mobile-bar {
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 150;   /* under the header flyouts (200) and every drawer (300+) */
        display: flex;
        align-items: stretch;
        box-sizing: border-box;
        min-height: 56px;
        padding: 0 env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
        background: var(--bspkfw-mobile-bar-bg, var(--bspkfw-surface));
        color: var(--bspkfw-mobile-bar-fg, var(--bspkfw-text-2));
        border-top: 1px solid var(--bspkfw-border);
    }
    body:has(> .mobile-bar) .site-header > .site-nav-main,
    body:has(> .mobile-bar) .site-header > .site-nav-drawer-trigger,
    body:has(> .mobile-bar) .site-header .user-menu { display: none; }
    body:has(> .mobile-bar) .site-header > .site-title { margin-right: auto; }
    body:has(> .mobile-bar) .site-nav-drawer {
        left: auto;
        right: 0;
        padding-left: 0;
        padding-right: env(safe-area-inset-right);
        box-shadow: -4px 0 24px var(--bspkfw-sh-rgba-0-0-0-3);
        transform: translateX(100%);
    }
    body:has(> .mobile-bar) .site-nav-drawer.is-open { transform: none; }
    body:has(> .mobile-bar) .bspkfw-save-all-bar { bottom: calc(24px + var(--bspkfw-mobile-bar-offset)); }
    body.manage-cards-active:has(> .mobile-bar) .bspkfw-with-selected:has(> .bspkfw-with-selected-trigger:not(:disabled)) {
        bottom: calc(12px + var(--bspkfw-mobile-bar-offset));
    }
}
/* One thumb target each, sharing the width; the menu last (bottom right). */
.mobile-bar-item {
    position: relative;
    flex: 1 1 0;
    min-width: 0;
    min-height: 56px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    padding: 6px 4px;
    border: 0;
    background: transparent;
    color: inherit;
    font: inherit;
    font-size: 11px;
    font-weight: 600;
    line-height: 1.2;
    text-decoration: none;
    cursor: pointer;
}
/* The menu keeps its own slot at the right edge: it never stretches, so
   with no project buttons it still sits bottom right, not mid-bar. */
.mobile-bar-menu { flex: 0 0 72px; margin-left: auto; }
.mobile-bar-item:hover,
.mobile-bar-item:focus-visible { background: var(--bspkfw-sunken); color: var(--bspkfw-text); }
.mobile-bar-item.is-active,
.mobile-bar-menu[aria-expanded="true"] { color: var(--bspkfw-accent-text-strong); }
.mobile-bar-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    font-size: 20px;
    line-height: 1;
}
.mobile-bar-icon > svg { width: 24px; height: 24px; fill: currentColor; }
.mobile-bar-label { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mobile-bar-item > .site-nav-badge {
    position: absolute;
    top: 4px;
    left: calc(50% + 6px);
    margin: 0;
}
@media print {
    .mobile-bar { display: none !important; }
    body:has(> .mobile-bar) { padding-bottom: 0; }
}

/* ============================================================
   Page drawers — Drawer::panel() / Drawer::trigger() + drawer.js
   ============================================================
   A panel (a section menu, a filter panel, an outline) that is ORDINARY
   inline content at and above its breakpoint and an off-canvas slide-in
   below it, behind a Drawer::trigger() button. The breakpoint is a real
   media query keyed on `data-collapse` (sm ≤640 / md ≤900 / lg ≤1200 /
   always), so a phone never paints the inline panel before the script
   runs. The off-canvas form is the DEFAULT below; each min-width query
   then puts its preset back inline.

   Inline means "as if it were not a drawer": the wrapper, the panel and
   the body are `display: contents` (no box — a sticky menu inside stays
   sticky against the page's own layout, a grid item stays a grid item),
   the head and backdrop are not rendered, and the inherited properties
   the off-canvas panel sets are reset. The dialog role / aria-modal are
   added by drawer.js only while it is OPEN.

   Themeable: --bspkfw-drawer-width / -bg / -fg / -shadow / -backdrop /
   -divider / -hover. Like any position:fixed box, the panel is clipped by
   an ancestor with transform / filter / contain — don't render one inside
   such a container. */
.bspkfw-drawer { display: contents; }
.bspkfw-drawer-backdrop {
    position: fixed;
    top: 0; right: 0; bottom: 0; left: 0;
    background: var(--bspkfw-drawer-backdrop, var(--bspkfw-bg-rgba-15-23-42-55));
    z-index: 300;   /* the site-nav drawer's rank: only one is ever open */
    opacity: 0;
    visibility: hidden;
    transition: opacity 200ms ease, visibility 0s linear 200ms;
}
.bspkfw-drawer.is-open > .bspkfw-drawer-backdrop {
    opacity: 1;
    visibility: visible;
    transition: opacity 200ms ease;
}
.bspkfw-drawer-panel {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    height: 100vh;
    height: 100dvh;
    width: var(--bspkfw-drawer-width, min(320px, 85vw));
    max-width: 100%;
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
    padding-top: env(safe-area-inset-top);
    padding-bottom: env(safe-area-inset-bottom);
    padding-right: env(safe-area-inset-right);
    background: var(--bspkfw-drawer-bg, var(--bspkfw-bg-fff));
    color: var(--bspkfw-drawer-fg, var(--bspkfw-fg-1f2937));
    box-shadow: var(--bspkfw-drawer-shadow, -4px 0 24px var(--bspkfw-sh-rgba-0-0-0-2));
    z-index: 301;
    overscroll-behavior: contain;
    transform: translateX(100%);
    visibility: hidden;   /* out of the tab order + a11y tree while shut */
    transition: transform 220ms ease, visibility 0s linear 220ms;
}
.bspkfw-drawer[data-side="left"] > .bspkfw-drawer-panel {
    right: auto;
    left: 0;
    padding-right: 0;
    padding-left: env(safe-area-inset-left);
    box-shadow: var(--bspkfw-drawer-shadow, 4px 0 24px var(--bspkfw-sh-rgba-0-0-0-2));
    transform: translateX(-100%);
}
.bspkfw-drawer.is-open > .bspkfw-drawer-panel {
    transform: none;
    visibility: visible;
    transition: transform 220ms ease;
}
/* A panel under a finger (the phone bar's swipe, mobile-bar.js): visible,
   following it with no transition — after the open rules so it wins over
   them while an open drawer is dragged shut. The transform itself is set on
   the style property. */
.site-nav-drawer.is-dragging,
.bspkfw-drawer > .bspkfw-drawer-panel.is-dragging { visibility: visible; transition: none; }
@media (prefers-reduced-motion: reduce) {
    .bspkfw-drawer-panel, .bspkfw-drawer-backdrop { transition: none; }
}
.bspkfw-drawer-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    flex-shrink: 0;
    min-height: 52px;
    padding: 4px 8px 4px 16px;
    border-bottom: 1px solid var(--bspkfw-drawer-divider, var(--bspkfw-bd-e5e7eb));
}
.bspkfw-drawer-title {
    min-width: 0;
    font-size: 16px;
    font-weight: 600;
    overflow-wrap: anywhere;
}
.bspkfw-drawer-close {
    flex-shrink: 0;
    margin-left: auto;
    width: 44px;
    height: 44px;
    padding: 0;
    border: 0;
    border-radius: 8px;
    background: transparent;
    color: inherit;
    font-size: 26px;
    line-height: 1;
    cursor: pointer;
}
.bspkfw-drawer-close:hover,
.bspkfw-drawer-close:focus-visible { background: var(--bspkfw-drawer-hover, var(--bspkfw-bg-f3f4f6)); }
.bspkfw-drawer-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    padding: 12px 16px;
}
.bspkfw-drawer-trigger { display: inline-flex; align-items: center; gap: 6px; }
.bspkfw-drawer-trigger-icon { font-size: 1.1em; line-height: 1; }

/* Back inline at and above each preset's breakpoint. `always` never is. */
@media (min-width: 641px) {
    .bspkfw-drawer[data-collapse="sm"] > .bspkfw-drawer-panel,
    .bspkfw-drawer[data-collapse="sm"] > .bspkfw-drawer-panel > .bspkfw-drawer-body {
        display: contents; visibility: visible; color: inherit;
    }
    .bspkfw-drawer[data-collapse="sm"] > .bspkfw-drawer-backdrop,
    .bspkfw-drawer[data-collapse="sm"] > .bspkfw-drawer-panel > .bspkfw-drawer-head,
    .bspkfw-drawer-trigger[data-collapse="sm"] { display: none; }
}
@media (min-width: 901px) {
    .bspkfw-drawer[data-collapse="md"] > .bspkfw-drawer-panel,
    .bspkfw-drawer[data-collapse="md"] > .bspkfw-drawer-panel > .bspkfw-drawer-body {
        display: contents; visibility: visible; color: inherit;
    }
    .bspkfw-drawer[data-collapse="md"] > .bspkfw-drawer-backdrop,
    .bspkfw-drawer[data-collapse="md"] > .bspkfw-drawer-panel > .bspkfw-drawer-head,
    .bspkfw-drawer-trigger[data-collapse="md"] { display: none; }
}
@media (min-width: 1201px) {
    .bspkfw-drawer[data-collapse="lg"] > .bspkfw-drawer-panel,
    .bspkfw-drawer[data-collapse="lg"] > .bspkfw-drawer-panel > .bspkfw-drawer-body {
        display: contents; visibility: visible; color: inherit;
    }
    .bspkfw-drawer[data-collapse="lg"] > .bspkfw-drawer-backdrop,
    .bspkfw-drawer[data-collapse="lg"] > .bspkfw-drawer-panel > .bspkfw-drawer-head,
    .bspkfw-drawer-trigger[data-collapse="lg"] { display: none; }
}
/* Printed: the panel's content in place, never the slide-in. */
@media print {
    .bspkfw-drawer-panel, .bspkfw-drawer-body { display: contents; visibility: visible; color: inherit; }
    .bspkfw-drawer-backdrop, .bspkfw-drawer-head, .bspkfw-drawer-trigger { display: none !important; }
}

/* Header region picker — single glyph (flag or globe) opens the language /
   locale / currency picker. */
.region-trigger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: 1px solid var(--bspkfw-header-border);
    border-radius: 999px;
    width: 32px;
    height: 32px;
    color: var(--bspkfw-header-fg);
    font-family: inherit;
    cursor: pointer;
    line-height: 1;
    padding: 0;
}
.region-trigger:hover { background: var(--bspkfw-header-hover-bg); color: var(--bspkfw-header-fg-hover); }
.region-trigger-icon  { font-size: 16px; line-height: 1; }

/* The colour-scheme switcher (ColorScheme): signed out, a header button the
   size of .region-trigger opening three menuitemradio choices; signed in, the
   same choices as a group in the user menu. The icons are CSS-drawn (CSP-safe):
   a sun, a crescent, a half-filled disc. */
.bspkfw-scheme-menu { position: relative; }
.bspkfw-scheme-trigger {
    display: inline-flex; align-items: center; justify-content: center;
    background: transparent; border: 1px solid var(--bspkfw-header-border); border-radius: 999px;
    width: 32px; height: 32px; color: var(--bspkfw-header-fg); cursor: pointer; padding: 0;
}
.bspkfw-scheme-trigger:hover { background: var(--bspkfw-header-hover-bg); color: var(--bspkfw-header-fg-hover); }
.bspkfw-scheme-trigger:focus-visible { outline: 2px solid var(--bspkfw-focus-ring); outline-offset: 2px; }
.bspkfw-scheme-dropdown {
    position: absolute; top: calc(100% + 8px); right: 0; min-width: 150px; z-index: 200; padding: 4px 0;
    /* As wide as its longest choice (its box is the 32px trigger, so shrink-to-fit stopped at
       min-width and "Auto (match my device)" wrapped onto four lines), never wider than the window. */
    width: max-content; max-width: calc(100vw - 16px);
    background: var(--bspkfw-surface); border: 1px solid var(--bspkfw-border); border-radius: 8px;
    box-shadow: 0 6px 20px var(--bspkfw-sh-rgba-0-0-0-12);
}
.bspkfw-scheme-dropdown[hidden] { display: none; }
.bspkfw-scheme-item, button.user-menu-item.bspkfw-scheme-choice {
    display: flex; align-items: center; gap: 10px; width: 100%; padding: 8px 14px;
    background: transparent; border: 0; font: inherit; font-size: 14px; text-align: left; cursor: pointer;
    color: var(--bspkfw-text-2);
}
.bspkfw-scheme-item { white-space: nowrap; }   /* one line each: icon, label, ✓ */
.bspkfw-scheme-item:hover, .bspkfw-scheme-item:focus-visible { background: var(--bspkfw-sunken); color: var(--bspkfw-ink); outline: none; }
.bspkfw-scheme-choice[aria-checked="true"] { font-weight: 600; color: var(--bspkfw-ink); }
.bspkfw-scheme-choice[aria-checked="true"]::after { content: '✓'; margin-left: auto; color: var(--bspkfw-accent-text); }
.user-menu-group-label {
    padding: 8px 14px 2px; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em;
    color: var(--bspkfw-muted);
}
.bspkfw-scheme-group { border-top: 1px solid var(--bspkfw-bd-f3f4f6); margin-top: 4px; padding-top: 2px; }
.bspkfw-scheme-icon { display: inline-flex; width: 18px; height: 18px; flex: 0 0 18px; }
.bspkfw-scheme-icon svg { width: 100%; height: 100%; display: block; }   /* sun / moon / half circle, ColorScheme::iconSvg() */

/* Header auth affordance for SIGNED-OUT visitors — occupies the same
   .site-nav-right slot the user menu fills when authed. Metrics deliberately
   match .region-trigger (32px pill, same border/ink) so the cluster lines up
   whichever combination renders.

   .header-login is the framework's own quiet "Sign in" link: a returning user
   is looking for it, so it shouldn't compete with the page's own acquisition
   CTA. .header-cta is the shared filled variant for a project's
   Project::guestHeaderActions() button (typically "Sign up") — one class so
   every consumer's guest CTA looks the same instead of each project inventing
   its own. flex-shrink:0 keeps both at natural size in the flex cluster. */
.header-login,
.header-cta {
    display: inline-flex;
    align-items: center;
    height: 32px;
    padding: 0 12px;
    border: 1px solid var(--bspkfw-header-border);
    border-radius: 999px;
    font-size: 13px;
    font-weight: 500;
    line-height: 1;
    text-decoration: none;
    white-space: nowrap;
    flex-shrink: 0;
}
.header-login { color: var(--bspkfw-header-fg); background: transparent; }
/* text-decoration repeated on :hover — the global `a:hover {underline}` rule
   out-specifies the base class selector above, and an underlined pill is not
   a pill. */
.header-login:hover { background: var(--bspkfw-header-hover-bg); color: var(--bspkfw-header-fg-hover); text-decoration: none; }
.header-cta { color: var(--bspkfw-fg-fff); background: var(--bspkfw-accent); border-color: var(--bspkfw-accent-line); }
.header-cta:hover { background: var(--bspkfw-accent-hover); border-color: var(--bspkfw-accent-hover-border); color: var(--bspkfw-fg-fff); text-decoration: none; }
.header-login:focus-visible,
.header-cta:focus-visible { outline: 2px solid var(--bspkfw-header-focus); outline-offset: 2px; }

/* Header user menu — single avatar trigger that opens a dropdown.
   display:flex on the wrapper takes the trigger out of inline line-box
   layout — without it the inherited body line-height (1.5) creates a
   strut around the inline-block button and shifts the avatar a few
   pixels off the .region-trigger sitting next to it. */
.user-menu { position: relative; display: flex; align-items: center; }
.user-menu-trigger {
    background: transparent;
    border: none;
    padding: 0;
    cursor: pointer;
    line-height: 0;
    border-radius: 50%;
    outline-offset: 2px;
}
.user-menu-trigger:focus-visible { outline: 2px solid var(--bspkfw-header-focus); }
.user-menu-photo {
    display: block;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    object-fit: cover;
    border: 1px solid var(--bspkfw-header-border);
}
.user-menu-photo--placeholder {
    background: var(--bspkfw-header-hover-bg) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><circle cx='16' cy='12' r='6' fill='%2394a3b8'/><path d='M4 30c0-7 5-12 12-12s12 5 12 12' fill='%2394a3b8'/></svg>") center/cover no-repeat;
}

.user-menu-dropdown {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    min-width: 180px;
    background: var(--bspkfw-bg-fff);
    border: 1px solid var(--bspkfw-bd-e5e7eb);
    border-radius: 8px;
    box-shadow: 0 6px 20px var(--bspkfw-sh-rgba-0-0-0-12);
    padding: 4px 0;
    z-index: 200;
    /* A super admin's menu outgrows a short window; behind a sticky header the
       page cannot scroll it into view, so the menu scrolls inside itself. */
    max-height: calc(100vh - 76px);
    max-height: calc(100dvh - 76px);
    overflow-y: auto;
    overscroll-behavior: contain;
}
.user-menu-header {
    padding: 8px 14px;
    color: var(--bspkfw-fg-5f6774);
    font-size: 12px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    border-bottom: 1px solid var(--bspkfw-bd-f3f4f6);
    margin-bottom: 4px;
}
.user-menu-item {
    display: block;
    padding: 8px 14px;
    color: var(--bspkfw-fg-374151);
    text-decoration: none;
    font-size: 14px;
}
.user-menu-item:hover { background: var(--bspkfw-bg-f3f4f6); color: var(--bspkfw-fg-111827); }
/* Button-flavoured menu items (theme picker, density toggle) — strip the
   UA button chrome so they read identically to the <a> items. */
button.user-menu-item {
    width: 100%;
    text-align: left;
    background: transparent;
    border: 0;
    cursor: pointer;
    font: inherit;
    font-size: 14px;
}
/* Toggle-style menu items (density) — pressed state gets a checkmark. */
.user-menu-item[aria-pressed="true"]::after {
    content: " ✓";
    color: var(--bspkfw-accent-text);
}
.user-menu-item--danger { color: var(--bspkfw-fg-dc2626); }
.user-menu-item--danger:hover { background: var(--bspkfw-bg-fef2f2); color: var(--bspkfw-fg-b91c1c); }

/* Section divider in the user-menu dropdown — emitted between project-supplied
   items (see PageHandler::renderProjectMenuItems) and the framework's
   Profile/Settings/Logout block, and again before Logout. */
.user-menu-divider {
    height: 1px;
    background: var(--bspkfw-bg-e5e7eb);
    margin: 4px 0;
}

/* Submenu rows (Framework / System / project-defined). Inline expansion —
   clicking the trigger reveals indented items below it without losing the
   parent dropdown. Uses a button (not <a>) because the trigger itself
   doesn't navigate. */
.user-menu-submenu-trigger {
    width: 100%;
    text-align: left;
    background: transparent;
    border: 0;
    cursor: pointer;
    font: inherit;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}
.user-menu-submenu-chevron {
    color: var(--bspkfw-fg-9ca3af);
    font-size: 16px;
    transition: transform 120ms ease;
}
.user-menu-submenu-trigger[aria-expanded="true"] .user-menu-submenu-chevron {
    transform: rotate(90deg);
}
.user-menu-submenu-items {
    background: var(--bspkfw-bg-f9fafb);
    border-top: 1px solid var(--bspkfw-bd-f3f4f6);
    border-bottom: 1px solid var(--bspkfw-bd-f3f4f6);
}
.user-menu-submenu-item {
    padding-left: 28px;
}

/* Notifications bell — top-bar trigger + dropdown.
   Greyed out by default; coloured + red badge when has-unread. */
.notify-bell {
    position: relative;
    background: transparent;
    border: 0;
    padding: 6px 8px;
    color: var(--bspkfw-header-muted);            /* greyed default */
    cursor: pointer;
    font-size: 18px;
    line-height: 1;
}
.notify-bell:hover { color: var(--bspkfw-header-fg); }
.notify-bell.has-unread { color: var(--bspkfw-header-fg-hover); }
.notify-bell-badge {
    position: absolute;
    top: 2px;
    right: 0;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    background: var(--bspkfw-bg-dc2626);       /* red dot */
    color: var(--bspkfw-fg-fff);
    font-size: 10px;
    font-weight: 700;
    line-height: 16px;
    text-align: center;
    border-radius: 10px;
    box-sizing: border-box;
}
.notify-dropdown {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    width: 360px;
    max-width: calc(100vw - 32px);
    background: var(--bspkfw-bg-fff);
    border: 1px solid var(--bspkfw-bd-e5e7eb);
    border-radius: 8px;
    box-shadow: 0 6px 20px var(--bspkfw-sh-rgba-0-0-0-12);
    z-index: 200;
}
.notify-dropdown-header {
    padding: 10px 14px;
    color: var(--bspkfw-fg-5f6774);
    font-size: 12px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    border-bottom: 1px solid var(--bspkfw-bd-f3f4f6);
}
.notify-dropdown-list { max-height: 60vh; overflow-y: auto; }
.notify-dropdown-empty {
    padding: 24px 14px;
    text-align: center;
    color: var(--bspkfw-fg-5f6774);
    font-size: 14px;
}
.notify-item {
    display: block;
    padding: 10px 14px;
    border-bottom: 1px solid var(--bspkfw-bd-f3f4f6);
    color: var(--bspkfw-fg-374151);
    text-decoration: none;
    font-size: 14px;
}
.notify-item:hover { background: var(--bspkfw-bg-f3f4f6); color: var(--bspkfw-fg-111827); }
.notify-item:last-child { border-bottom: 0; }
.notify-item.unread { background: var(--bspkfw-bg-eef2ff); }
.notify-item-meta {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    font-size: 12px;
    color: var(--bspkfw-fg-5f6774);
    margin-top: 2px;
}
.notify-dropdown-footer {
    padding: 10px 14px;
    text-align: center;
    border-top: 1px solid var(--bspkfw-bd-f3f4f6);
}
.notify-dropdown-footer a {
    color: var(--bspkfw-fg-4f46e5);
    font-size: 13px;
    font-weight: 600;
    text-decoration: none;
}

/* User-menu per-object badges (unread counts beside menu entries). */
.user-menu-badge {
    display: inline-block;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    margin-left: 6px;
    background: var(--bspkfw-bg-dc2626);
    color: var(--bspkfw-fg-fff);
    font-size: 11px;
    font-weight: 700;
    line-height: 18px;
    text-align: center;
    border-radius: 9px;
    vertical-align: middle;
}

/* Manage-page banner — "{n} notifications for {plural}" link. */
.notify-manage-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 10px 14px;
    margin-bottom: 16px;
    background: var(--bspkfw-bg-fef3c7);
    border: 1px solid var(--bspkfw-bd-fde68a);
    border-radius: 8px;
    color: var(--bspkfw-fg-78350f);
    font-size: 14px;
}
.notify-manage-banner a {
    color: var(--bspkfw-fg-78350f);
    font-weight: 600;
    text-decoration: underline;
}

main { padding: var(--bspkfw-space-page, 32px 24px); max-width: 1200px; margin: 0 auto; }

/* The project's site footer (Project::footerHtml()), after <main> and
   outside its width cap — the project styles what it returns. Not printed. */
@media print { .bspkfw-site-footer { display: none; } }

/* A page that declares `pageWidth(): 'wide'` (BasePage) gets a BIGGER
   reading measure — deliberately NOT `max-width: none`. The cap above is
   doing two jobs, bounding the LINE LENGTH and bounding the CONTAINER,
   and only the second one is ever in the way. Tables here are
   `width: 100%`, so they fill whatever container they are handed:
   measured in a browser, removing the cap puts a 14-column row across
   3792px on a 4K display — cells padded out with whitespace and the
   reader's eye crossing the entire monitor to follow one line. That is a
   worse problem than the scrollbar it replaces, and it looks like a fix
   in a screenshot taken at 1920. Do not "simplify" this to none.

   Per-page tuning is the token, not a third state: set
   --bspkfw-measure-wide on the page that genuinely needs more. */
main.is-wide { max-width: var(--bspkfw-measure-wide, 1800px); }

/* ============================================================
   Page header
   ============================================================ */
.page-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: var(--bspkfw-space-card, 24px);
}

.page-header h1 { font-size: 22px; font-weight: 600; }

/* The action bar WRAPS when it runs out of room — five controls on a
   phone become two right-anchored rows instead of forcing the page
   wider than the viewport (every manage page carries this markup, so
   without wrap every managed table scrolled sideways on phones even
   with the nav's own overflow treatment engaged). min-width: 0 lets
   the flex item shrink below its content so it never defines the
   header's minimum; justify-content only acts once wrapping makes the
   box wider than a row, so a bar that fits renders byte-identically. */
.header-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 8px;
    min-width: 0;
}

/* ============================================================
   Buttons
   ============================================================ */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;   /* a button wider than its label (a full-width menu, a phone) keeps the label centred */
    text-align: center;        /* …and a label that wraps onto two lines */
    gap: 6px;
    padding: 7px 14px;
    border: 1px solid var(--bspkfw-bd-d1d5db);
    border-radius: 6px;
    background: var(--bspkfw-bg-fff);
    color: var(--bspkfw-fg-374151);
    font-size: 13px;
    font-weight: 500;
    /* Pin line-height + font so `<a class="btn">` and `<button class="btn">`
       compute the same content-box height. Without these, a <button> uses the
       UA control font + line-height:normal (~1.2) and renders a few px shorter
       than an <a> (which inherits the body 1.5), so mixed link/action rows
       don't line up. 1.5 matches the body default the anchors already use, so
       anchor heights are unchanged — only the <button> grows to match. */
    line-height: 1.5;
    font-family: inherit;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
}

.btn:hover { background: var(--bspkfw-bg-f9fafb); border-color: var(--bspkfw-bd-9ca3af); }
.btn:active { background: var(--bspkfw-bg-f3f4f6); }
.btn[hidden] { display: none; }

.btn-primary {
    background: var(--bspkfw-accent);
    border-color: var(--bspkfw-accent-line);
    color: var(--bspkfw-fg-fff);
}
.btn-primary:hover { background: var(--bspkfw-accent-hover); border-color: var(--bspkfw-accent-hover-border); }

.btn-danger {
    background: var(--bspkfw-bg-fff);
    border-color: var(--bspkfw-bd-fca5a5);
    color: var(--bspkfw-fg-dc2626);
}
.btn-danger:hover { background: var(--bspkfw-bg-fef2f2); border-color: var(--bspkfw-bd-f87171); }

.btn-sm { padding: 4px 10px; font-size: 12px; }

/* Form::actionButton() wrapper — a <form> is display:block by default,
   which would break row layouts (a Void button inside a <td> pushing its
   row open); inline-block keeps the whole control sitting in text flow
   exactly like a bare <button> would. Driven by action-form.js. */
.bspkfw-action-form { display: inline-block; margin: 0; }

/* `.is-submitting` is added to a submit button by `Form`'s auto-generated
   script for the duration of the in-flight network call. The button is
   ALSO `disabled` during that window (the actual fix for double-fire);
   this class hides the label + overlays a centered spinner so the
   user gets a non-intrusive "in progress" cue WITHOUT the button's
   box changing size. The spinner colour is `currentColor` by default —
   the glyphs are hidden with `-webkit-text-fill-color`, NOT `color`, so
   the button's text colour survives for the ring to inherit: a project's
   navy button with white text gets a white spinner with no CSS of its
   own. (The old `color: transparent` made currentColor transparent too,
   which forced a medium-grey fallback — a grey ring on a navy fill that
   every project with its own button classes had to hunt for and fix.)
   `--bspkfw-spinner-color` still wins when set — the two framework
   variants below pin it, and a project whose button text colour is not
   the ring colour it wants sets it the same way. */
.is-submitting {
    position: relative;
    -webkit-text-fill-color: transparent !important;
    text-shadow: none !important;
    cursor: wait;
}
.is-submitting > * { visibility: hidden; }      /* hide any nested icons / spans */
.is-submitting::after {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 14px;
    height: 14px;
    margin: -7px 0 0 -7px;                       /* center on the button */
    border: 2px solid var(--bspkfw-spinner-color, currentColor);
    border-top-color: transparent;
    border-radius: 50%;
    opacity: 0.85;
    animation: bspkfw-submitting-spin 0.65s linear infinite;
}
.btn-primary { --bspkfw-spinner-color: var(--bspkfw-fg-fff); }
.btn-danger  { --bspkfw-spinner-color: var(--bspkfw-fg-dc2626); }
@keyframes bspkfw-submitting-spin { to { transform: rotate(360deg); } }

/* A picker while the server searches (BspkfwKselect.remote — kselect-auto.js): a small spinner at the control's
   end. On a linked-record picker (Form\ReferencePicker; the hosted sheet, formwork.css, has the same rule) and on
   any other Kselect it searches for, the people picker among them (`.ks-wrapper`, the default busy element). */
.fw-reference { display: block; position: relative; }
.ks-wrapper.is-searching { position: relative; }
.fw-reference.is-searching::after, .ks-wrapper.is-searching::after {
    content: ''; position: absolute; top: 50%; right: 2.4rem; width: 0.85rem; height: 0.85rem; margin-top: -0.425rem;
    border: 2px solid var(--bspkfw-field-border, var(--bspkfw-bd-d1d5db)); border-top-color: var(--bspkfw-field-accent, var(--bspkfw-accent-text)); border-radius: 50%;
    animation: bspkfw-submitting-spin 0.7s linear infinite; pointer-events: none;
}
@media (prefers-reduced-motion: reduce) { .fw-reference.is-searching::after, .ks-wrapper.is-searching::after { animation-duration: 2s; } }

/* Escalating busy cue for a NAVIGATING POST — busy-nav.js (BspkfwBusy),
   started by action-form.js on every `form.bspkfw-action-form` submit.
   Nothing exists in the DOM before 400 ms; then a slim indeterminate bar
   pinned to the top of the viewport; at ~2.5 s it gains its label; at
   ~8 s the label turns amber and says "Still working. Please don't
   refresh this page." The bar's motion is a CSS transform keyframe on a
   compositor-friendly property — a rAF loop would freeze exactly when the
   main thread is busiest. Above every other layer (the lightbox is 9999)
   because a wait must be visible over whatever is open; click-through, so
   it never blocks the page it is reporting on. Themable via the
   --bspkfw-busy-* properties. */
.bspkfw-busy {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 10000;
    display: flex;
    flex-direction: column;
    align-items: center;
    pointer-events: none;
    animation: bspkfw-busy-in 0.18s ease-out;
}
.bspkfw-busy-bar {
    position: relative;
    width: 100%;
    height: 3px;
    overflow: hidden;
    background: var(--bspkfw-busy-track, var(--bspkfw-accent-tint-strong));
}
.bspkfw-busy-bar::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 40%;
    border-radius: 2px;
    background: var(--bspkfw-busy-color, var(--bspkfw-accent));
    animation: bspkfw-busy-slide 1.4s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
.bspkfw-busy-label {
    margin-top: 8px;
    padding: 6px 14px;
    border-radius: 999px;
    max-width: min(90vw, 480px);
    font-size: 13px;
    line-height: 1.35;
    text-align: center;
    color: var(--bspkfw-busy-label-fg, var(--bspkfw-fg-fff));
    background: var(--bspkfw-busy-label-bg, var(--bspkfw-bg-1f2937));
    /* The faint light ring is what separates the pill from a DARK site
       header — the label lands over the header, whatever colour it is. */
    box-shadow: 0 0 0 1px var(--bspkfw-sh-rgba-255-255-255-35), 0 2px 8px var(--bspkfw-sh-rgba-0-0-0-25);
    animation: bspkfw-busy-in 0.18s ease-out;
}
.bspkfw-busy-label[hidden] { display: none; }
.bspkfw-busy.is-reassuring .bspkfw-busy-label {
    background: var(--bspkfw-busy-reassure-bg, var(--bspkfw-bg-b45309));
    color: var(--bspkfw-busy-reassure-fg, var(--bspkfw-fg-fff));
}
@keyframes bspkfw-busy-slide {
    from { transform: translateX(-100%); }
    to   { transform: translateX(250%); }   /* 250% of its own 40% width = clear of the right edge */
}
@keyframes bspkfw-busy-in { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
    .bspkfw-busy, .bspkfw-busy-label { animation: none; }
    .bspkfw-busy-bar::after { animation: none; width: 100%; opacity: 0.75; }   /* a static bar, not a moving one */
}

/* ============================================================
   Data table
   ============================================================ */
.data-table {
    width: 100%;
    border-collapse: collapse;
    background: var(--bspkfw-bg-fff);
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 1px 3px var(--bspkfw-sh-rgba-0-0-0-08);
}

.data-table th {
    background: var(--bspkfw-bg-f9fafb);
    padding: 10px 14px;
    text-align: left;
    font-weight: 600;
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--bspkfw-fg-5f6774);
    border-bottom: 1px solid var(--bspkfw-bd-e5e7eb);
}

.data-table td {
    padding: 10px 14px;
    border-bottom: 1px solid var(--bspkfw-bd-f3f4f6);
    color: var(--bspkfw-fg-374151);
}

.data-table td:not(.row-actions) {
    max-width: 280px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.data-table tr:last-child td { border-bottom: none; }
.data-table tr:hover td { background: var(--bspkfw-bg-f9fafb); }

/* Table density — the per-user Comfortable / Condensed toggle (manage
   toolbar + user menu) persists to user_settings.tableDensity and is
   server-rendered as data-table-density on <html>, so condensed spacing
   paints on first render (no flash of comfortable). Keyed on the ROOT
   attribute — a public contract — so ANY table opts in with one selector;
   projects add
       :root[data-table-density="condensed"] .my-table th,
       :root[data-table-density="condensed"] .my-table td { padding: …; }
   and inherit the same toggle + persistence for free. Comfortable is the
   default (attribute absent) and needs no rules. The same attribute also
   drives the PAGE-CHROME spacing tokens near the top of this file
   (--bspkfw-space-*), so condensed tightens cards / sections / form rows
   / modals as well as these cells. Spacing only — colours, zebra, hover,
   selection styling all cascade unchanged.
   The condensed values are overridable per project via the --bspkfw-
   custom properties below (set on :root); the var() fallbacks are the
   framework defaults, so unset = unchanged. */
:root[data-table-density="condensed"] .data-table th {
    padding: var(--bspkfw-data-table-condensed-th-padding, 5px 10px);
}
:root[data-table-density="condensed"] .data-table td {
    padding: var(--bspkfw-data-table-condensed-td-padding, 4px 10px);
    line-height: var(--bspkfw-data-table-condensed-line-height, 1.25);
}

.row-actions { width: 1%; white-space: nowrap; }
.row-actions-buttons .btn + .btn { margin-left: 6px; }

/* Project-declared row actions (BaseObject::rowActions()) — LINKS, not
   buttons: a row action goes to a page, so it middle-clicks / opens in a new
   tab like any other link. They wear the built-ins' own classes (.btn.btn-sm
   in the button row, .row-action-item in the ⋮ menu), so the only thing to
   undo is the underline the global `a:hover` would otherwise add. */
.row-actions-buttons a.btn:hover,
a.row-action-item:hover { text-decoration: none; }
.bspkfw-row-action-icon { font-style: normal; line-height: 1; }

/* Bulk-select column — leftmost cell of every manage table. The <th> hosts
   a "select all visible" checkbox plus a 3-way view filter (all / checked /
   unchecked). Selections survive sort / search / pagination because they
   live in JS state (selectedIds Set), not on the DOM. */
.bspkfw-select-col {
    width: 1%;
    white-space: nowrap;
    padding-left: 12px;
    padding-right: 8px;
}
.bspkfw-select-col input[type="checkbox"] {
    cursor: pointer;
    margin: 0;
    vertical-align: middle;
}
.bspkfw-select-header {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.bspkfw-view-filter {
    background: transparent;
    border: 1px solid transparent;
    border-radius: 4px;
    padding: 1px 4px;
    cursor: pointer;
    font-size: 14px;
    line-height: 1;
    color: var(--bspkfw-fg-5f6774);
    transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.bspkfw-view-filter:hover {
    background: var(--bspkfw-bg-f3f4f6);
    border-color: var(--bspkfw-bd-d1d5db);
    color: var(--bspkfw-fg-111827);
}
.bspkfw-view-filter:focus-visible {
    outline: 2px solid var(--bspkfw-accent-bright);
    outline-offset: 1px;
}
.bspkfw-view-filter[data-view-mode="checked"],
.bspkfw-view-filter[data-view-mode="unchecked"] {
    /* Active (non-default) modes get a tinted ring so it's obvious the list
       is filtered — easy to overlook otherwise. */
    color: var(--bspkfw-accent-text);
    border-color: var(--bspkfw-accent-border);
    background: var(--bspkfw-accent-soft);
}

/* Row highlight for ticked rows — subtle so it doesn't fight the hover
   background, but enough to scan a page at a glance. */
.data-table tr.bspkfw-row-selected td       { background: var(--bspkfw-bg-f5f8ff); }
.data-table tr.bspkfw-row-selected:hover td { background: var(--bspkfw-bg-eef2ff); }

/* m2m link columns — comma-separated label list, never sortable. Align
   with neighbour cells; allow the label list to wrap on narrow tables
   instead of forcing horizontal scroll. */
.data-table .bspkfw-link-col {
    white-space: normal;
    max-width: 240px;
}

/* Inline-edit — clicking a data cell turns it into a tiny form input plus
   save/cancel icons (or just an input in auto-save mode). The .data-table
   td:not(.row-actions) rule clamps width + nowrap, which we need to relax
   while editing so the input has room to breathe. */
.data-table td .bspkfw-cell-edit {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    width: 100%;
}
.data-table td:has(> .bspkfw-cell-edit) {
    max-width: none;
    white-space: normal;
}
.bspkfw-cell-edit > .form-control {
    flex: 1 1 auto;
    min-width: 0;
    padding: 3px 6px;
    height: 28px;
    font-size: 13px;
}

/* m2m inline-edit — Kselect-decorated <select multiple>. Two fixes vs the
   default Kselect look:
   - .ks-wrapper inside an inline-flex parent has a baseline of its first
     line, which leaves the control visually anchored at the bottom of
     the cell. align-self: center forces it back to the row's centre.
   - When the dropdown opens, Kselect adds a focus-coloured 1.5px border
     + 3px glow on .ks-control to read as "active". Inside a tight
     table cell that double-bordered look fights the cell's own framing,
     so we hide it — the dropdown's own border carries the visual.
   Note: Kselect *copies* the underlying <select>'s class names onto its
   own .ks-wrapper unless `copyClassName: false` is passed. Every framework
   call site now passes it (core/Form/KselectInit.php for the PHP-rendered
   fields; edit-modal.js and manage.js for the JS-built pickers), so the
   wrapper carries only Kselect's own classes and .form-control no longer
   paints a second box around .ks-control. Two consequences worth keeping
   in mind here: the inline-edit multi-select stays classless anyway (see
   buildInlineLinkEditor — the cell wants the barest chrome), and a class
   selector must still never try to display:none the underlying select,
   because it would also hide the wrapper. Kselect's own inline
   style.display='none' handles hiding the raw <select> just fine. */
.bspkfw-cell-edit .ks-wrapper {
    flex: 1 1 auto;
    min-width: 0;
    align-self: center;
}
.bspkfw-cell-edit .ks-wrapper.ks-open > .ks-control {
    border-color: transparent;
    box-shadow: none;
}

/* Validation state on a Kselect-enhanced field. `.is-invalid` normally styles
   the <select> itself, but Kselect hides that element, so edit-modal.js puts
   the class on the wrapper too (see showFieldError) and the visible chrome is
   restyled here. Kselect ships no .is-invalid styling of its own — it's our
   class — so this rule is the only thing that makes a failed required select
   look failed. Mirrors .form-control.is-invalid's colours. */
.ks-wrapper.is-invalid > .ks-control {
    border-color: var(--bspkfw-field-invalid, var(--bspkfw-bd-f87171));
    box-shadow: 0 0 0 3px var(--bspkfw-field-invalid-ring, var(--bspkfw-sh-rgba-239-68-68-12));
}
.bspkfw-cell-edit > textarea.form-control {
    min-height: 50px;
    resize: vertical;
}
.bspkfw-cell-save, .bspkfw-cell-cancel {
    width: 22px;
    height: 22px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: 3px;
    background: transparent;
    cursor: pointer;
    font-size: 14px;
    line-height: 1;
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.bspkfw-cell-save  { color: var(--bspkfw-fg-16a34a); }
.bspkfw-cell-save:hover  { background: var(--bspkfw-bg-f0fdf4); border-color: var(--bspkfw-bd-86efac); }
.bspkfw-cell-cancel { color: var(--bspkfw-fg-5f6774); }
.bspkfw-cell-cancel:hover { background: var(--bspkfw-bg-f3f4f6); border-color: var(--bspkfw-bd-d1d5db); }
.bspkfw-cell-dirty {
    /* Subtle marker on cells the user just changed but hasn't yet flushed
       through Save All — gone once the list re-fetches. */
    box-shadow: inset 3px 0 0 var(--bspkfw-sh-f59e0b);
}

/* Save all — fixed-position bar at bottom-right, only present when there
   are pending edits. Stays out of the way until the user has something
   to save. */
.bspkfw-save-all-bar {
    position: fixed;
    right: 24px;
    bottom: 24px;
    z-index: 200;
}
.bspkfw-save-all-btn {
    box-shadow: 0 4px 12px var(--bspkfw-sh-rgba-0-0-0-15);
}

/* First-time prompt — small modal asking the user whether to enable
   inline edit on this object, with a sub-checkbox for auto-save and a
   note about the profile-page settings. */
.bspkfw-inline-prompt .modal-box--narrow {
    max-width: 480px;
}
.bspkfw-inline-prompt .checkbox-label {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin: 12px 0 0;
}
.bspkfw-iep-autosave-hint {
    margin: 6px 0 0 24px;
    color: var(--bspkfw-fg-5f6774);
    font-size: 13px;
    line-height: 1.5;
}
.bspkfw-iep-note {
    margin-top: 16px;
    padding-top: 12px;
    border-top: 1px solid var(--bspkfw-bd-f3f4f6);
    color: var(--bspkfw-fg-5f6774);
    font-size: 12px;
}

/* Profile page → Inline edit settings table. Compact like the
   communications-prefs table (same shape, three columns). */
.inline-edit-pref-table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 12px;
}
.inline-edit-pref-table th,
.inline-edit-pref-table td {
    padding: 8px 12px;
    text-align: left;
    border-bottom: 1px solid var(--bspkfw-bd-f3f4f6);
}
.inline-edit-pref-table th {
    font-size: 12px;
    font-weight: 600;
    color: var(--bspkfw-fg-5f6774);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.inline-edit-pref-table th:nth-child(2),
.inline-edit-pref-table th:nth-child(3),
.inline-edit-pref-table td:nth-child(2),
.inline-edit-pref-table td:nth-child(3) {
    text-align: center;
    width: 1%;
    white-space: nowrap;
}

/* "With selected" page-header dropdown. Only rendered when the user has
   bulk-edit or bulk-delete permission and the table isn't readonly.
   Trigger is disabled when the selection is empty so the affordance is
   visible (and discoverable) without ever firing destructively. */
.bspkfw-with-selected {
    position: relative;
    display: inline-block;
}
.bspkfw-with-selected-trigger:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    pointer-events: auto; /* keep tooltip-on-hover working */
}
.bspkfw-with-selected-arrow {
    margin-left: 6px;
    font-size: 10px;
    line-height: 1;
}
.bspkfw-with-selected-menu {
    position: absolute;
    top: calc(100% + 4px);
    right: 0;
    min-width: 160px;
    background: var(--bspkfw-bg-fff);
    border: 1px solid var(--bspkfw-bd-e5e7eb);
    border-radius: 6px;
    box-shadow: 0 4px 12px var(--bspkfw-sh-rgba-0-0-0-1);
    z-index: 100;
    padding: 4px 0;
}
.bspkfw-with-selected-menu[hidden] { display: none; }
.bspkfw-with-selected-item {
    display: block;
    width: 100%;
    text-align: left;
    background: transparent;
    border: none;
    cursor: pointer;
    font-size: 14px;
    padding: 8px 14px;
    color: var(--bspkfw-fg-374151);
}
.bspkfw-with-selected-item:hover { background: var(--bspkfw-bg-f3f4f6); }
.bspkfw-with-selected-item:focus-visible {
    outline: 2px solid var(--bspkfw-accent-bright);
    outline-offset: -2px;
}
.bspkfw-with-selected-item--danger        { color: var(--bspkfw-fg-dc2626); }
.bspkfw-with-selected-item--danger:hover  { background: var(--bspkfw-bg-fef2f2); color: var(--bspkfw-fg-b91c1c); }

/* Compact mode: hide the horizontal button row and show the three-dot menu instead. */
.row-actions-menu { position: relative; display: none; }
.data-table.compact-actions .row-actions-buttons { display: none; }
.data-table.compact-actions .row-actions-menu     { display: inline-block; }
/* Standalone (non-manage-grid) use — the neutral opt-in that makes the menu
   visible WITHOUT a .data-table.compact-actions ancestor. Behaviour comes from
   the always-loaded js/row-actions-menu.js; markup via RowActions::menu(). */
.row-actions-menu--standalone { display: inline-block; }

.row-actions-menu-trigger {
    background: transparent;
    border: 1px solid transparent;
    border-radius: 4px;
    cursor: pointer;
    font-size: 18px;
    line-height: 1;
    padding: 4px 8px;
    color: var(--bspkfw-fg-5f6774);
}
.row-actions-menu-trigger:hover { background: var(--bspkfw-bg-f3f4f6); color: var(--bspkfw-fg-111827); }
.row-actions-menu.open .row-actions-menu-trigger { background: var(--bspkfw-bg-f3f4f6); border-color: var(--bspkfw-bd-d1d5db); }

.row-actions-menu-list {
    /* Positioned via JS at open time using fixed-positioning so it can escape the
       table's `overflow: hidden` (used for rounded corners). */
    position: fixed;
    background: var(--bspkfw-bg-fff);
    border: 1px solid var(--bspkfw-bd-e5e7eb);
    border-radius: 6px;
    box-shadow: 0 4px 12px var(--bspkfw-sh-rgba-0-0-0-1);
    z-index: 100;
    min-width: 130px;
    padding: 4px 0;
}
.row-action-item {
    display: block;
    width: 100%;
    text-align: left;
    background: transparent;
    border: none;
    cursor: pointer;
    font-size: 14px;
    padding: 8px 14px;
    color: var(--bspkfw-fg-374151);
}
.row-action-item:hover { background: var(--bspkfw-bg-f3f4f6); }
.row-action-item--danger        { color: var(--bspkfw-fg-dc2626); }
.row-action-item--danger:hover  { background: var(--bspkfw-bg-fef2f2); }

.badge-super-admin {
    display: inline-block;
    color: var(--bspkfw-fg-d97706);
    font-size: 11px;
    vertical-align: middle;
    margin-left: 4px;
    line-height: 1;
}

/* ============================================================
   Forms
   ============================================================ */
.form-group { margin-bottom: var(--bspkfw-space-row, 16px); }

/* Bspkfw\Core\Form\Fieldset wraps grouped fields in <fieldset>. The
   browser's default styling is fine but a little tight; ease the
   contents off the border so the inner fields don't visually
   collide with the legend or the edge. */
.bspkfw-fieldset {
    padding: 16px 20px 4px;
    margin: 0 0 var(--bspkfw-space-card, 24px);
    border: 1px solid var(--bspkfw-bd-e5e7eb);
    border-radius: 8px;
}
.bspkfw-fieldset > legend {
    padding: 0 8px;
    font-weight: 600;
    color: var(--bspkfw-fg-1f2937);
}

/* A collapsible Fieldset (option `collapsible`): the legend's button looks like
   the legend, with a chevron that turns when open; the hint is muted text
   beside it while folded. Folded, only the legend shows. */
.bspkfw-fieldset.is-collapsible.is-collapsed { padding-bottom: 8px; }
.bspkfw-fieldset-toggle {
    appearance: none; background: none; border: 0; margin: 0; padding: 0;
    font: inherit; font-weight: 600; color: inherit; cursor: pointer;
    display: inline-flex; align-items: center; gap: 6px;
}
.bspkfw-fieldset-toggle::before {
    content: ''; width: 0.45em; height: 0.45em; flex: 0 0 auto;
    border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
    transform: rotate(45deg); transition: transform 0.15s ease; margin: 0 2px 0.2em;
}
.bspkfw-fieldset.is-collapsed > legend > .bspkfw-fieldset-toggle::before { transform: rotate(-45deg); margin-bottom: 0; }
.bspkfw-fieldset-toggle:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; border-radius: 4px; }
.bspkfw-fieldset-hint { margin-left: 8px; font-weight: 400; font-size: 0.875em; color: var(--bspkfw-field-muted, var(--bspkfw-fg-5f6774)); }
.bspkfw-fieldset:not(.is-collapsed) > legend > .bspkfw-fieldset-hint { display: none; }
.bspkfw-fieldset-body[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) { .bspkfw-fieldset-toggle::before { transition: none; } }

/* RepeatableGroup's opt-in rows (2026-10-19 — rowActions / rowLabel / rowBuilder …; CLAUDE/FORMS.md § Repeatable
   groups): each row a light card, its cells beside its tools (move up / down, duplicate, remove). A plain group
   (no opt-in) keeps the browser's own look. */
.bspkfw-repeatable--rich > .bspkfw-repeatable__rows { display: flex; flex-direction: column; gap: 8px; }
.bspkfw-repeatable--rich .bspkfw-repeatable__row {
    display: flex; gap: 8px; align-items: flex-start;
    padding: 10px 12px; border: 1px solid var(--bspkfw-bd-e5e7eb); border-radius: 8px; background: var(--bspkfw-bg-fff);
}
.bspkfw-repeatable--rich .bspkfw-repeatable__cells { flex: 1 1 auto; min-width: 0; }
.bspkfw-repeatable--rich .bspkfw-repeatable__cells > .form-group:last-child { margin-bottom: 0; }
.bspkfw-repeatable--rich .bspkfw-repeatable__tools { flex: 0 0 auto; display: flex; flex-wrap: wrap; gap: 2px; justify-content: flex-end; }
.bspkfw-repeatable--rich .bspkfw-repeatable__tools { position: relative; z-index: 1; }
.bspkfw-repeatable--rich .bspkfw-repeatable__tools > .btn { padding-left: 6px; padding-right: 6px; }
.bspkfw-repeatable--rich .bspkfw-repeatable__row[hidden] { display: none; }   /* a tab per row: the rows not showing (the flex rule above would beat [hidden]) */
.bspkfw-repeatable--rich .bspkfw-repeatable__glyph { display: none; }
.bspkfw-repeatable--rich .bspkfw-repeatable__glyph:only-child { display: inline; }
.bspkfw-repeatable--compact .bspkfw-repeatable__glyph { display: inline; }
.bspkfw-repeatable--compact .bspkfw-repeatable__label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.bspkfw-repeatable--compact .bspkfw-repeatable__tools { flex-wrap: nowrap; width: 7.5rem; }
.bspkfw-repeatable--rich > .bspkfw-repeatable__add { margin-top: 8px; }
@media (max-width: 640px) {
    .bspkfw-repeatable--rich .bspkfw-repeatable__row { flex-direction: column; align-items: stretch; }
    .bspkfw-repeatable--rich .bspkfw-repeatable__tools { justify-content: flex-start; }
}

label {
    display: block;
    font-weight: 500;
    margin-bottom: 5px;
    color: var(--bspkfw-fg-374151);
}

.required { color: var(--bspkfw-fg-dc2626); margin-left: 2px; }

.form-control {
    width: 100%;
    padding: 8px 11px;
    border: 1px solid var(--bspkfw-field-border, var(--bspkfw-bd-d1d5db));
    border-radius: var(--bspkfw-field-radius, 6px);
    font-size: 14px;
    font-family: inherit;
    color: var(--bspkfw-field-text, var(--bspkfw-fg-1f2937));
    background: var(--bspkfw-field-bg, var(--bspkfw-bg-fff));
    transition: border-color 0.15s, box-shadow 0.15s;
}

.form-control:focus {
    outline: none;
    border-color: var(--bspkfw-field-border-focus, var(--bspkfw-focus-ring));
    box-shadow: 0 0 0 3px var(--bspkfw-field-focus-ring, var(--bspkfw-focus-glow));
}

.form-control.is-invalid {
    border-color: var(--bspkfw-field-invalid, var(--bspkfw-bd-f87171));
    box-shadow: 0 0 0 3px var(--bspkfw-field-invalid-ring, var(--bspkfw-sh-rgba-239-68-68-12));
}

/* Number input — full-height custom stepper on the right edge.
   The native browser spinner is hidden; clicks on the buttons call
   input.stepUp() / stepDown() (wired by /js/number-stepper.js). */
.number-input {
    position: relative;
    display: block;
}
.number-input input[type="number"] {
    -moz-appearance: textfield;
    padding-inline-end: 36px;   /* logical: the stepper sits at the inline end, so it follows dir="rtl" */
}
.number-input input[type="number"]::-webkit-inner-spin-button,
.number-input input[type="number"]::-webkit-outer-spin-button {
    -webkit-appearance: none;
    margin: 0;
}
.number-input-stepper {
    position: absolute;
    top:    1px;
    inset-inline-end: 1px;
    bottom: 1px;
    width:  28px;
    display: flex;
    flex-direction: column;
    border-inline-start: 1px solid var(--bspkfw-bd-d1d5db);
    border-start-end-radius: 5px;
    border-end-end-radius: 5px;
    overflow: hidden;
    background: var(--bspkfw-bg-f9fafb);
}
.number-input-up,
.number-input-down {
    flex: 1 1 50%;
    background: transparent;
    border: none;
    cursor: pointer;
    color: var(--bspkfw-fg-5f6774);
    font-size: 9px;
    line-height: 1;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    user-select: none;
}
.number-input-up    { border-bottom: 1px solid var(--bspkfw-bd-d1d5db); }
.number-input-up:hover,
.number-input-down:hover { background: var(--bspkfw-bg-f3f4f6); color: var(--bspkfw-fg-111827); }
.number-input-up:active,
.number-input-down:active { background: var(--bspkfw-bg-e5e7eb); }
/* Hide stepper when the input is disabled. */
.number-input input:disabled ~ .number-input-stepper { opacity: 0.4; pointer-events: none; }

/* MoneyInput — the currency code as a badge joined to the amount box's
   start edge: [USD | 12.34]. Markup: .money-input > .money-input__currency
   + .number-input (the box, with its stepper or text-mode). Logical
   properties, so the badge sits on the right under dir="rtl". Theme it with
   --bspkfw-money-badge-bg / -fg; the defaults derive from the field tokens,
   so a themed or dark form (Formwork's included) gets a matching badge.
   The badge follows the focus border (it reads as one control) but not
   .is-invalid (it is not part of the error). No currency known (no
   `currency` option, no region.default_currency, no region cookie): no
   badge, and the box keeps its own corners. */
.money-input {
    display: flex;
    align-items: stretch;
    width: 100%;
    min-width: 0;
}
.money-input > .number-input {
    flex: 1 1 auto;
    min-width: 0;
}
.money-input:not([data-currency=""]) .form-control {
    border-start-start-radius: 0;
    border-end-start-radius: 0;
}
.money-input[data-currency=""] > .money-input__currency { display: none; }
.money-input__currency {
    display: inline-flex;
    align-items: center;
    flex: 0 0 auto;
    padding: 0 0.6rem;
    border: 1px solid var(--bspkfw-field-border, var(--bspkfw-bd-d1d5db));
    border-inline-end: 0;
    border-start-start-radius: var(--bspkfw-field-radius, 6px);
    border-end-start-radius: var(--bspkfw-field-radius, 6px);
    background: var(--bspkfw-money-badge-bg, color-mix(in srgb, var(--bspkfw-field-muted, var(--bspkfw-bg-5f6774)) 10%, var(--bspkfw-field-bg, var(--bspkfw-bg-fff))));
    color: var(--bspkfw-money-badge-fg, var(--bspkfw-field-muted, var(--bspkfw-fg-5f6774)));
    font-size: 0.875em;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    transition: border-color 0.15s;
}
.money-input:focus-within .money-input__currency { border-color: var(--bspkfw-field-border-focus, var(--bspkfw-focus-ring)); }
.money-input:has(.form-control:disabled) .money-input__currency { color: var(--bspkfw-field-disabled-fg, var(--bspkfw-fg-9ca3af)); }

.field-error { color: var(--bspkfw-fg-dc2626); font-size: 12px; margin-top: 4px; }

/* PasswordInput ['reveal' => true] — the show/hide eye toggle sits inside
   the control (absolutely positioned over the input's right padding).
   NOT the encrypted-column "Reveal" feature — this only flips visibility
   of what the user just typed. */
.form-reveal-wrap { position: relative; }
.form-reveal-wrap .form-control { padding-right: 42px; }
.form-reveal-toggle {
    position: absolute;
    top: 50%;
    right: 6px;
    transform: translateY(-50%);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    padding: 0;
    background: none;
    border: none;
    border-radius: 4px;
    color: var(--bspkfw-fg-5f6774);
    cursor: pointer;
}
.form-reveal-toggle:hover { color: var(--bspkfw-fg-111827); }
.form-reveal-toggle:focus-visible { outline: 2px solid var(--bspkfw-focus-ring); outline-offset: 1px; }
.form-reveal-toggle svg { width: 20px; height: 20px; }
.form-reveal-toggle .reveal-eye-off { display: none; }
.form-reveal-toggle.is-revealed .reveal-eye { display: none; }
.form-reveal-toggle.is-revealed .reveal-eye-off { display: block; }

/* Non-blocking advisory (Field option `asyncAdvisor`) — a live server-checked
   warning the user is free to ignore. Deliberately amber, NOT the red of
   .field-error: the colour is the whole signal that this does not block
   submit. Sized/spaced like .field-error and .form-help so the three
   below-input channels line up. */
.form-advisory {
    color: var(--bspkfw-fg-b45309);
    font-size: 12px;
    margin-top: 4px;
    display: flex;
    align-items: baseline;
    gap: 5px;
}
.form-advisory::before {
    content: "\26A0";     /* ⚠ — decorative; the text carries the meaning */
    flex: none;
}
.form-advisory[hidden] { display: none; }   /* beats the flex display above */

/* Postcode-autofill alternate localities ("Or: Hollywood / Studio City…") —
   rendered by postal-autofill.js under the city input when a postcode maps
   to more than one place. Sized/spaced like .form-help / .field-error so
   the below-input channels line up; the buttons read as inline links. */
.bspkfw-postal-alts {
    font-size: 12px;
    color: var(--bspkfw-fg-5f6774);
    margin-top: 4px;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 6px;
}
.bspkfw-postal-alts button {
    background: none;
    border: none;
    padding: 0;
    font: inherit;
    color: var(--bspkfw-accent-text);
    cursor: pointer;
    text-decoration: underline;
}
.bspkfw-postal-alts button:hover { color: var(--bspkfw-accent-text-hover); }

.form-control--mono { font-family: ui-monospace, monospace; font-size: 12px; }

.form-control--readonly {
    background: var(--bspkfw-field-surface, var(--bspkfw-bg-f9fafb));
    color: var(--bspkfw-field-muted, var(--bspkfw-fg-5f6774));
    cursor: default;
    border-color: var(--bspkfw-field-disabled-border, var(--bspkfw-bd-e5e7eb));
}

.form-control:disabled,
select.form-control:disabled,
input.form-control[readonly] {
    background: var(--bspkfw-field-disabled-bg, var(--bspkfw-bg-f3f4f6));
    color: var(--bspkfw-field-disabled-fg, var(--bspkfw-fg-9ca3af));
    border-color: var(--bspkfw-field-disabled-border, var(--bspkfw-bd-e5e7eb));
    cursor: not-allowed;
    opacity: 1;
}

textarea.form-control { resize: vertical; }

select.form-control { padding-right: 28px; cursor: pointer; }
.form-control--inline { width: auto; }

/* A native colour input: .form-control's padding squashed the swatch to a
   hairline. Room for the swatch, a pointer, never wider than it needs. */
.form-control--color { height: 38px; padding: 3px; max-width: 120px; cursor: pointer; }

/* Clearable colour field (ColorInput 'clearable'): swatch, hex text, Clear.
   An empty value shows the swatch struck through — "no colour". */
.bspkfw-color { display: flex; align-items: center; gap: 6px; }
.bspkfw-color__swatch { flex: 0 0 auto; width: 44px; height: 36px; padding: 2px; cursor: pointer; position: relative; }
.bspkfw-color__hex { flex: 1 1 auto; min-width: 0; max-width: 140px; font-family: ui-monospace, monospace; }
.bspkfw-color__clear { flex: 0 0 auto; }
.bspkfw-color.is-empty .bspkfw-color__swatch {
    background: linear-gradient(to top right, transparent calc(50% - 1px), var(--bspkfw-field-border, var(--bspkfw-bg-d1d5db)) 50%, transparent calc(50% + 1px));
}
.bspkfw-color.is-empty .bspkfw-color__swatch::-webkit-color-swatch { visibility: hidden; }
.bspkfw-color.is-empty .bspkfw-color__swatch::-moz-color-swatch { visibility: hidden; }

.checkbox-label { display: flex; align-items: center; gap: 8px; font-weight: normal; }
.checkbox-label input { width: auto; }

/* Checkbox group (Form\CheckboxGroup) — several boxes, one per choice; the
   multi-select wire shape with every option in view. */
.checkbox-group { display: flex; flex-direction: column; gap: 6px; }
.checkbox-group--inline { flex-direction: row; flex-wrap: wrap; gap: 6px 16px; }
.checkbox-group__option { display: flex; align-items: center; gap: 8px; font-weight: normal; cursor: pointer; }
.checkbox-group__option input { width: auto; margin: 0; }
.checkbox-group.is-invalid { outline: 1px solid var(--bspkfw-field-invalid, var(--bspkfw-bd-f87171)); outline-offset: 4px; border-radius: var(--bspkfw-field-radius, 6px); }

/* RadioGroup options — the same look as CheckboxGroup's: one per line (or wrapped with `inline`), the
   button beside its text with a gap, the text in normal weight. */
.radio-group { display: flex; flex-direction: column; gap: 6px; }
.radio-group--inline { flex-direction: row; flex-wrap: wrap; gap: 6px 16px; }
.radio-group__option { display: flex; align-items: center; gap: 8px; font-weight: normal; cursor: pointer; }
.radio-group__option input { width: auto; margin: 0; flex: none; }

/* The "Other…" free-text box under a Select / RadioGroup with the `other`
   option (form.js shows it when that choice is picked). */
.form-choice-other { margin-top: 6px; }
.radio-group__option--other { display: inline-flex; align-items: center; gap: 8px; }

/* Rich tooltip (Field option `tooltipHtml`): the ⓘ is a <button> that opens a
   native popover (the Popover API — no script, light-dismiss, Esc closes).
   `:popover-open` is unknown to a browser without the API, so the rule is
   dropped there and the content simply stays hidden. */
.form-tooltip--popover { background: none; border: 0; padding: 0; margin: 0; font: inherit; color: inherit; cursor: pointer; line-height: 1; }
.form-popover { display: none; }
.form-popover:popover-open {
    display: block;
    max-width: 320px;
    padding: 10px 12px;
    border: 1px solid var(--bspkfw-field-border, var(--bspkfw-bd-d1d5db));
    border-radius: var(--bspkfw-field-radius, 6px);
    background: var(--bspkfw-field-surface, var(--bspkfw-bg-fff));
    color: var(--bspkfw-field-text, var(--bspkfw-fg-1f2937));
    font-size: 13px;
    font-weight: normal;
    line-height: 1.4;
    box-shadow: 0 6px 24px var(--bspkfw-sh-rgba-0-0-0-14);
}
.form-popover a { color: var(--bspkfw-field-accent, inherit); }

.form-actions {
    display: flex;
    flex-wrap: wrap;   /* Cancel + a second submit (Form::addSubmit) + Submit on a phone: the primary ends its line on the right */
    justify-content: flex-end;
    gap: 8px;
    padding-top: var(--bspkfw-space-row, 16px);
    border-top: 1px solid var(--bspkfw-bd-f3f4f6);
    margin-top: 8px;
}

.form-captcha {
    margin: 16px 0 4px;
    display: flex;
    justify-content: flex-start;
}

/* ============================================================
   Modal
   ============================================================ */
.modal {
    position: fixed;
    inset: 0;
    z-index: var(--bspkfw-z-modal, 100);
    display: flex;
    align-items: center;
    justify-content: center;
}

.modal[hidden] { display: none; }

/* The photo cropper is always opened FROM something — /account today, an
   edit modal's file column via onCropped tomorrow — and there is no flow
   in which it should sit below another layer, so the nested rank is its
   permanent rank, not an option. (At the shared .modal z-index, DOM order
   decided: an edit modal appended at open time rendered ON TOP of the
   cropper it launched, leaving a Save button that couldn't be clicked.)
   Its backdrop stacks with it, so the layer beneath stays inert. */
#bspkfw-photo-cropper-modal { z-index: var(--bspkfw-z-modal-nested, 110); }
/* A confirm / alert / prompt is answered before anything under it: always the top layer (it is created once, so a modal
   opened later — an edit modal, a designer window — used to cover it by DOM order). */
#_dialog { z-index: var(--bspkfw-z-dialog, 120); }

/* A stacked EditModal child (EditModal.open with `stack: true` — the inline
   FK create flow) sits over the parent session it suspended. DOM order
   already paints it on top (child roots are appended last); the explicit
   nested rank makes the layering deliberate rather than incidental. The
   suspended parent stays visible behind the child's backdrop — the user can
   SEE their half-filled form waiting — and is `inert`, so the backdrop's
   dimming is honest: nothing beneath is reachable. */
.bspkfw-edit-modal.bspkfw-em-stacked { z-index: var(--bspkfw-z-modal-nested, 110); }

.modal-backdrop {
    position: fixed;
    inset: 0;
    background: var(--bspkfw-bg-rgba-0-0-0-45);
}

.modal-box {
    position: relative;
    z-index: 1;
    background: var(--bspkfw-bg-fff);
    border-radius: 10px;
    box-shadow: 0 20px 40px var(--bspkfw-sh-rgba-0-0-0-18);
    width: 520px;
    max-width: calc(100vw - 32px);
    max-height: calc(100vh - 64px);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

/* Wide variant — base .modal-box's max-width keeps it inside narrow viewports. */
.modal-box--wide { width: min(85vw, 1000px); }

/* ============================================================
   Permissions matrix
   ============================================================ */
.perm-matrix {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
}

.perm-matrix th,
.perm-matrix td {
    padding: 8px 12px;
    border-bottom: 1px solid var(--bspkfw-bd-e5e7eb);
    text-align: center;
    white-space: nowrap;
}

.perm-matrix th:first-child,
.perm-matrix td:first-child { text-align: left; }

.perm-matrix thead th {
    background: var(--bspkfw-bg-f9fafb);
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--bspkfw-fg-5f6774);
}

.perm-level-name { font-weight: 500; }

.perm-matrix tbody tr:last-child td { border-bottom: none; }

/* Row and column toggle controls */
.perm-row-toggle,
.perm-col-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    margin-right: 8px;
    cursor: pointer;
}

.perm-row-toggle input[type="checkbox"],
.perm-col-toggle input[type="checkbox"] {
    cursor: pointer;
}

.perm-row-toggle input[type="checkbox"]:indeterminate,
.perm-col-toggle input[type="checkbox"]:indeterminate {
    background-color: var(--bspkfw-bg-d1d5db);
}

.perm-col-label {
    display: inline-block;
}

.perm-level-label {
    display: inline-block;
}

.modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px var(--bspkfw-space-card, 24px);
    flex-shrink: 0;
    background: var(--bspkfw-bg-f9fafb);
    border-bottom: 1px solid var(--bspkfw-bd-f3f4f6);   /* mirrors .modal-footer's top border */
    cursor: move;             /* hint that the modal is draggable by its header */
    user-select: none;        /* avoid text selection while grabbing */
}
.modal-header .modal-close,
.modal-header button,
.modal-header a,
.modal-header input,
.modal-header select,
.modal-header label {
    cursor: pointer;          /* keep interactive controls inside the header clickable */
}
.modal-box.is-dragging {
    transition: none;         /* avoid any transition smoothing during the drag */
}

.modal-scroll {
    flex: 1;
    overflow-y: auto;
    padding: 12px var(--bspkfw-space-card, 24px);
    min-height: 0;
}

.modal-footer {
    flex-shrink: 0;
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    padding: var(--bspkfw-space-row, 16px) var(--bspkfw-space-card, 24px);
    border-top: 1px solid var(--bspkfw-bd-f3f4f6);
}

.modal-header h2 { font-size: 18px; font-weight: 600; }

/* EditModal carousel chrome — only visible when openSequence() is driving
   more than one step. The step indicator sits next to the title; the
   Skip-rest button sits at the far left of the footer (margin-right: auto
   pushes Cancel/Save to the right). Both are `[hidden]` by default. */
.bspkfw-em-step {
    margin-left: 12px;
    font-size: 13px;
    color: var(--bspkfw-fg-5f6774);
    font-weight: normal;
    align-self: center;
}
.bspkfw-em-step[hidden] { display: none; }
.bspkfw-em-skip-rest {
    margin-right: auto;
    background: transparent;
    border: 1px dashed var(--bspkfw-bd-d1d5db);
    color: var(--bspkfw-fg-5f6774);
}
.bspkfw-em-skip-rest:hover { background: var(--bspkfw-bg-f9fafb); border-color: var(--bspkfw-bd-9ca3af); color: var(--bspkfw-fg-111827); }
.bspkfw-em-skip-rest[hidden] { display: none; }

/* EditModal bulk mode — "Editing N Plural" title button + expandable
   list of selected records, plus per-field bulk-toggle styling. */
.bspkfw-em-bulk-title-btn {
    background: transparent;
    border: none;
    padding: 0;
    font: inherit;
    color: inherit;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.bspkfw-em-bulk-title-btn:hover { text-decoration: underline; }
.bspkfw-em-bulk-title-btn:focus-visible {
    outline: 2px solid var(--bspkfw-accent-bright);
    outline-offset: 2px;
    border-radius: 2px;
}
.bspkfw-em-bulk-arrow {
    font-size: 12px;
    line-height: 1;
    color: var(--bspkfw-fg-5f6774);
    transition: transform var(--ks-transition, 0.18s ease);
}
.bspkfw-em-bulk-title-btn[aria-expanded="true"] .bspkfw-em-bulk-arrow {
    transform: rotate(180deg);
}
.bspkfw-em-bulk-objects {
    border-bottom: 1px solid var(--bspkfw-bd-f3f4f6);
    background: var(--bspkfw-bg-f9fafb);
    max-height: 200px;
    overflow-y: auto;
    flex-shrink: 0;
}
.bspkfw-em-bulk-objects[hidden] { display: none; }
.bspkfw-em-bulk-objects-list {
    margin: 0;
    padding: 8px 24px;
    list-style: none;
    font-size: 13px;
    color: var(--bspkfw-fg-374151);
}
.bspkfw-em-bulk-objects-list li {
    padding: 3px 0;
    border-bottom: 1px solid var(--bspkfw-bd-e5e7eb);
}
.bspkfw-em-bulk-objects-list li:last-child { border-bottom: none; }

/* Per-row bulk toggle — small checkbox prepended to each editable field
   group + each m2m / tags section heading. The row is greyed out when
   the toggle is off; CSS pointer-events:none handles the Kselect case
   (no runtime disable API). At submit time the toggle's checked state
   is the source of truth — we don't read input.disabled. */
.bspkfw-em-bulk-toggle {
    margin-right: 8px;
    cursor: pointer;
    vertical-align: middle;
    flex-shrink: 0;
}
.bspkfw-em-bulk-row { position: relative; }
.bspkfw-em-bulk-row.form-group { display: flex; flex-wrap: wrap; align-items: baseline; }
.bspkfw-em-bulk-row.form-group > label,
.bspkfw-em-bulk-row.form-group > .form-control,
.bspkfw-em-bulk-row.form-group > .field-error,
.bspkfw-em-bulk-row.form-group > .form-control--readonly {
    flex-basis: 100%;
}
.bspkfw-em-bulk-row.form-group > .bspkfw-em-bulk-toggle { flex-basis: auto; }
.bspkfw-em-bulk-disabled .form-control,
.bspkfw-em-bulk-disabled label,
.bspkfw-em-bulk-disabled .db-link-heading,
.bspkfw-em-bulk-disabled .ks-control,
.bspkfw-em-bulk-disabled .ks-wrapper,
.bspkfw-em-bulk-disabled .file-upload {
    opacity: 0.5;
}
.bspkfw-em-bulk-disabled .ks-wrapper,
.bspkfw-em-bulk-disabled .file-upload {
    pointer-events: none;
}
.bspkfw-em-bulk-disabled .bspkfw-em-bulk-toggle {
    /* The toggle itself stays clickable so the user can re-enable. */
    opacity: 1;
    pointer-events: auto;
}

/* EditModal — per-record readonly (record.__readonly_fields, see
   BaseObject::readOnlyFieldsFor). edit-modal.js locks the inputs
   (readOnly / disabled); this greys the row and inert-ifies the
   file-upload surface, which has no disable API, mirroring the
   bulk-disabled treatment above. Softer opacity than bulk-disabled —
   the value is still meaningful content, just uneditable.

   Kselect is no longer in that "no disable API" category: since 1.7.0 it
   re-reads the <select>'s `disabled` on refresh (via its autoSync observer),
   so lockFieldUi's `inp.disabled = true` genuinely disables the widget —
   .ks-disabled styling, tabindex="-1", aria-disabled. That closed a real
   hole: pointer-events alone blocked the mouse but left the control
   tab-reachable and operable, so "greyed out" was a lie to a keyboard user.
   The .ks-wrapper pointer-events rules below (and in the bulk block above)
   are therefore now belt-and-braces rather than the mechanism, and are kept
   deliberately: they still cover any future path that greys a row without
   setting `disabled`, and they close the async gap between the property
   write and the observer callback. Safe to drop if that ever needs
   simplifying — verify a bulk toggle and a readonly field by keyboard. */
.bspkfw-em-record-readonly label,
.bspkfw-em-record-readonly .ks-control,
.bspkfw-em-record-readonly .ks-wrapper,
.bspkfw-em-record-readonly .file-upload {
    opacity: 0.7;
}
.bspkfw-em-record-readonly .ks-wrapper,
.bspkfw-em-record-readonly .file-upload {
    /* NOT .file-upload-existing — its view links stay clickable; the
       per-file remove buttons are disabled individually by JS. */
    pointer-events: none;
}

/* ------------------------------------------------------------------
   EditModal — field groups + responsive two-column layout.
   Server-rendered by BaseObject::renderGroupedFieldsHtml() when the
   object declares formFieldGroups(); flat (ungrouped) modals never
   carry any of these classes and are untouched. Groups are native
   <details> disclosures (CSP-safe — no JS for the toggle); the
   legend-less --lead section holds fields not claimed by a group.
   ------------------------------------------------------------------ */
.bspkfw-edit-modal.bspkfw-em-grouped .modal-box {
    width: 760px;
    max-width: calc(100vw - 32px);
}
.bspkfw-em-group {
    border: 1px solid var(--bspkfw-bd-e5e7eb);
    border-radius: 8px;
    padding: 12px 16px 0;
    margin: 0 0 var(--bspkfw-space-row, 16px);
}
.bspkfw-em-group[hidden] { display: none; }
.bspkfw-em-group--lead {
    border: none;
    border-radius: 0;
    padding: 0;
}
.bspkfw-em-group > .bspkfw-em-group-legend {
    cursor: pointer;
    font-weight: 600;
    color: var(--bspkfw-fg-1f2937);
    margin: 0 0 12px;
    user-select: none;
}
details.bspkfw-em-group:not([open]) { padding-bottom: 12px; }
details.bspkfw-em-group:not([open]) > .bspkfw-em-group-legend { margin-bottom: 0; }
.bspkfw-em-group-body {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 16px;
    /* Vertical rhythm stays the rows' own .form-group margin-bottom —
       no row-gap, so single-column stacking matches the flat modal. */
}
/* Long inputs span both columns — type-derived (textarea / markdown /
   html / json / file) or an explicit 'width' => 'full' descriptor key. */
.bspkfw-em-group-body > .form-group--full { grid-column: 1 / -1; }
@media (max-width: 700px) {
    .bspkfw-em-group-body { grid-template-columns: 1fr; }
}
/* A grid item's default `min-width: auto` lets a wide MIN-CONTENT child
   (the Trix toolbar's nowrap button row, ~350px+) force the track — and
   with it the modal's scroll container — wider than the modal, which is a
   horizontal scrollbar on a 375px phone. Rows must shrink to their track. */
.bspkfw-em-group-body > .form-group { min-width: 0; }

/* Tabs + "Show advanced settings" (formFieldGroups() 'tab' / 'advanced').
   The strip is the shared modal tab look (.modal-tab-bar / .tab-btn above);
   these rules only add what the edit modal needs on top: wrapping on a
   narrow modal, an error marker on a tab whose panel holds an invalid
   field, the checkbox row, and the heading-less section. */
/* The checkbox and the strip sit in the box's own host, between the header and the scrolling content
   (edit-modal.js moves them there), so neither scrolls away nor moves when a tab is shorter or taller. */
.bspkfw-em-tabs-host { padding: 12px var(--bspkfw-space-card, 24px) 0; flex: none; }
.bspkfw-em-tabs-host .bspkfw-em-tab-bar { margin-bottom: 0; }
.bspkfw-em-tabs-host > .bspkfw-em-advanced:last-child { margin-bottom: 0; }
.bspkfw-em-tab-bar { flex-wrap: wrap; margin-bottom: var(--bspkfw-space-row, 16px); }
.bspkfw-em-tab-bar[hidden] { display: none; }   /* .modal-tab-bar's display:flex would otherwise win */
.bspkfw-em-tab-bar .tab-btn[hidden] { display: none; }
.bspkfw-em-tab-bar .tab-btn.has-error { color: var(--bspkfw-field-invalid, var(--bspkfw-fg-f87171)); }
.bspkfw-em-tab-bar .tab-btn.has-error::after {
    content: '';
    display: inline-block;
    width: 6px;
    height: 6px;
    margin-left: 6px;
    border-radius: 50%;
    background: currentColor;
    vertical-align: middle;
}
.bspkfw-em-tab-panel[hidden] { display: none; }
.bspkfw-em-advanced {
    display: flex;
    justify-content: flex-end;
    margin: 0 0 6px;
    font-size: 13px;
    color: var(--bspkfw-fg-5f6774);
}
.bspkfw-em-advanced[hidden] { display: none; }   /* nothing advanced left to show (edit-modal.js syncGroupVisibility) */
.bspkfw-em-advanced-label { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
/* Textarea "Load from file…" (Form\Textarea `loadFromFile`): the file input sits inside its label, out of
   sight but still reachable by keyboard; the label shows the focus ring. */
.form-load-file { margin-top: 6px; }
.form-load-file__btn { position: relative; cursor: pointer; }
.form-load-file__btn:focus-within { outline: 2px solid var(--bspkfw-focus-ring); outline-offset: 2px; }
.form-load-file__input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
.bspkfw-em-group--plain {
    border: none;
    border-radius: 0;
    padding: 0;
}

/* HtmlInput (Trix) inside any form: the editor and its toolbar are capped
   at the row's width. The button row scrolls INSIDE its own box (never
   widening the row), and on a phone it wraps instead — a scrolling
   toolbar hides exactly the buttons a phone user most needs (bold, list).
   Three-part selectors so these beat the lazily-appended trix.css
   (`trix-toolbar .trix-button-row`) whatever the stylesheet order. */
.bspkfw-html-input,
.bspkfw-html-input trix-editor,
.bspkfw-html-input trix-toolbar,
.bspkfw-html-input .khe-container { max-width: 100%; min-width: 0; }
.bspkfw-html-input trix-toolbar .trix-button-row { max-width: 100%; }
@media (max-width: 480px) {
    .bspkfw-html-input trix-toolbar .trix-button-row {
        flex-wrap: wrap;
        overflow-x: visible;
        justify-content: flex-start;
        gap: 4px 6px;
    }
    .bspkfw-html-input trix-toolbar .trix-button-group { margin-bottom: 4px; }
}

.modal-close {
    background: none;
    border: none;
    font-size: 20px;
    color: var(--bspkfw-fg-9ca3af);
    cursor: pointer;
    line-height: 1;
    padding: 2px 6px;
    border-radius: 4px;
}

.modal-close:hover { color: var(--bspkfw-fg-374151); background: var(--bspkfw-bg-f3f4f6); }

/* ============================================================
   Modal tabs
   ============================================================ */
.modal-tab-bar {
    display: flex;
    gap: 4px;
    border-bottom: 1px solid var(--bspkfw-bd-e5e7eb);
    margin-bottom: var(--bspkfw-space-section-gap, 20px);
}

.tab-btn {
    padding: 6px 14px 10px;
    background: none;
    border: none;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    color: var(--bspkfw-fg-5f6774);
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    transition: color 0.15s, border-color 0.15s;
}

.tab-btn:hover { color: var(--bspkfw-fg-374151); }
.tab-btn.active { color: var(--bspkfw-accent-text); border-bottom-color: var(--bspkfw-accent-line); }
.tab-btn:focus-visible { outline: 2px solid var(--bspkfw-focus-ring); outline-offset: -2px; border-radius: 4px 4px 0 0; }

/* MarkdownInput's Edit / Preview strip (core/Form/MarkdownInput.php): the
   shared tab look above, nearer its field than a modal's strip is, and a
   preview pane boxed like the textarea so switching tabs doesn't jump
   (form-markdown.js copies the textarea's height onto it). The selectors
   carry ONE class of specificity, so a project rule on .bspkfw-md-tabs /
   .bspkfw-md-preview loaded after base.css still wins. */
.modal-tab-bar:where(.bspkfw-md-tabs) { flex-wrap: wrap; margin-bottom: var(--bspkfw-space-row, 16px); }
.bspkfw-md-preview:where(.bspkfw-markdown > *) {
    min-height: 6em;
    padding: 8px 11px;
    border: 1px solid var(--bspkfw-field-border, var(--bspkfw-bd-d1d5db));
    border-radius: var(--bspkfw-field-radius, 6px);
    background: var(--bspkfw-field-bg, var(--bspkfw-bg-fff));
    color: var(--bspkfw-field-text, var(--bspkfw-fg-1f2937));
    font-size: 14px;
    overflow-wrap: anywhere;
    overflow-x: auto;
}
.bspkfw-md-preview:where(.bspkfw-markdown > *) > :first-child { margin-top: 0; }
.bspkfw-md-preview:where(.bspkfw-markdown > *) > :last-child { margin-bottom: 0; }

.tab-hidden { display: none !important; }

.checkbox-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 4px 0;
}

.db-link-section {
    margin-top: var(--bspkfw-space-section-gap, 20px);
    padding-top: var(--bspkfw-space-row, 16px);
    border-top: 1px solid var(--bspkfw-bd-e5e7eb);
}
.db-link-heading {
    font-size: 14px;
    font-weight: 600;
    color: var(--bspkfw-fg-374151);
    margin: 0 0 8px;
}

/* ============================================================
   Alerts & state
   ============================================================ */
.alert {
    padding: 10px 14px;
    border-radius: 6px;
    font-size: 13px;
    margin-bottom: var(--bspkfw-space-row, 16px);
}

.alert-error { background: var(--bspkfw-bg-fef2f2); color: var(--bspkfw-fg-991b1b); border: 1px solid var(--bspkfw-bd-fecaca); }
.alert-success { background: var(--bspkfw-bg-f0fdf4); color: var(--bspkfw-fg-166534); border: 1px solid var(--bspkfw-bd-bbf7d0); }
.alert-warning { background: var(--bspkfw-bg-fffbeb); color: var(--bspkfw-fg-92400e); border: 1px solid var(--bspkfw-bd-fde68a); }

/* A sandbox copy of a site (Site::sandboxBannerHtml()): above the header on
   every page, the warning alert's colours (light and dark), never dismissed. */
.sandbox-banner {
    padding: 8px 16px;
    background: var(--bspkfw-bg-fffbeb);
    color: var(--bspkfw-fg-92400e);
    border-bottom: 1px solid var(--bspkfw-bd-fde68a);
    font-size: 14px;
    font-weight: 600;
    text-align: center;
    overflow-wrap: anywhere;
}

/* StaffAccess::bannerHtml() — a member of staff acting from a control center
   (CLAUDE/STAFF-ACCESS.md): above the header on every page, not dismissible. */
.staff-actor-banner {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 8px 16px;
    padding: 8px 16px;
    background: var(--bspkfw-accent-soft);
    color: var(--bspkfw-accent-text-strong);
    border-bottom: 1px solid var(--bspkfw-accent-border);
    font-size: 14px;
    font-weight: 600;
    overflow-wrap: anywhere;
}
.staff-actor-banner-actions { display: inline-flex; flex-wrap: wrap; gap: 8px; }
.staff-actor-banner-form { display: inline; margin: 0; }
/* /settings → Staff access: the trusted host and the keys (StaffAccessSettings). */
.staff-access-keys { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; margin: 0 0 16px; }
.staff-access-keys dt { font-weight: 600; }
.staff-access-keys dd { margin: 0; word-break: break-all; }
@media (max-width: 600px) { .staff-access-keys { grid-template-columns: 1fr; } }

/* Form::setLocalDraft() — "Restored your unsaved draft … · Discard", above a
   form's fields (form.js). The colours are .alert-warning's. */
.bspkfw-form-draft-notice { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; }
.bspkfw-form-draft-notice__text { flex: 1 1 16em; }

.empty-state, .loading { color: var(--bspkfw-fg-5f6774); padding: 32px 0; text-align: center; }

/* Loading spinner — see core/Spinner.php and public_html/js/spinner.js. The
   overlay variant absolute-positions itself inside any `position: relative`
   host (or one Spinner.show() temporarily makes relative), so callers like
   the manage table can grey out the existing rows while a refresh fetches.
   Project-specific styling can override these rules wholesale by shipping
   a custom `public_html/css/base.css` (or a separate stylesheet); the
   class names are the contract between PHP / JS and CSS. */
.bspkfw-spinner-overlay {
    position: absolute;
    inset: 0;
    background: var(--bspkfw-bg-rgba-255-255-255-65);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 50;
    min-height: 120px;
    /* No `pointer-events:none` — we want the overlay to swallow clicks
       while the underlying content is in a transient loading state. */
}
.bspkfw-spinner { display: inline-block; line-height: 0; }
.bspkfw-spinner-ring {
    width: 32px;
    height: 32px;
    border: 3px solid var(--bspkfw-bd-rgba-99-102-241-2);
    border-top-color: var(--bspkfw-bd-6366f1);
    border-radius: 50%;
    animation: bspkfw-spinner-rotate 0.8s linear infinite;
}
@keyframes bspkfw-spinner-rotate {
    to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
    .bspkfw-spinner-ring { animation-duration: 2.4s; }
}

.spinner {
    display: inline-block;
    width: 18px;
    height: 18px;
    border: 2px solid var(--bspkfw-bd-d1d5db);
    border-top-color: var(--bspkfw-accent-line);
    border-radius: 50%;
    vertical-align: middle;
    margin-left: 10px;
    animation: spinner-rotate 0.8s linear infinite;
}
.spinner[hidden] { display: none; }

/* File upload field — picker, file list, progress bar */

/* Drag-and-drop target. The zone is the `.file-upload` element itself unless
   the field named a bigger one via the `dropZone` option, in which case the
   framework stamps `data-bspkfw-dropzone` on that element instead — so both
   selectors below describe the same state and a project's own panel picks up
   the treatment for free.

   Field TOKENS, not hardcoded blues: a per-business theme that recolours its
   inputs recolours the drop cue with them, and the dashed outline still reads
   correctly against a dark surface. */
.file-upload.is-dragging,
[data-bspkfw-dropzone].is-dragging {
    outline: 2px dashed var(--bspkfw-field-border-focus, var(--bspkfw-focus-ring));
    outline-offset: 2px;
    background: var(--bspkfw-field-focus-ring, var(--bspkfw-accent-tint));
    border-radius: 6px;
}
/* Children must not swallow the drag: `pointer-events: none` on the contents
   would break the picker button, so instead the enter/leave COUNTER in
   file-upload.js absorbs the crossings. Nothing to do here — this comment
   exists so the next person doesn't "fix" the flicker in CSS. */

/* "or drop files here" — a picker button alone does not advertise the
   gesture. Server-rendered, so it is in the markup for a no-JS reader; hidden
   once the field holds anything, because by then the user has found it and
   the row list is the more useful thing to look at. */
.file-upload-drop-hint {
    margin: 6px 0 0;
    font-size: 12px;
    color: var(--bspkfw-field-muted, var(--bspkfw-fg-5f6774));
}
.file-upload:has(.file-upload-list li) .file-upload-drop-hint {
    display: none;
}
/* A disabled picker means the field is at its cap — the hint would be a lie. */
.file-upload:has(.file-upload-picker:disabled) .file-upload-drop-hint {
    display: none;
}

/* A native file input's intrinsic width is its "Choose files … No file
   chosen" label (~250px), so as a grid item it forces the same
   min-content overflow as the Trix toolbar on a 320px phone. */
.file-upload .file-upload-picker { max-width: 100%; min-width: 0; }
.file-upload-list {
    list-style: none;
    padding: 0;
    margin: 8px 0 0;
}
.file-upload-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 6px 10px;
    background: var(--bspkfw-bg-f9fafb);
    border: 1px solid var(--bspkfw-bd-e5e7eb);
    border-radius: 6px;
    margin-top: 6px;
    font-size: 13px;
}
.file-upload-name {
    flex: 1 1 auto;
    word-break: break-all;
    color: var(--bspkfw-fg-374151);
}
.file-upload-meta {
    color: var(--bspkfw-fg-5f6774);
    font-size: 12px;
    flex-shrink: 0;
}
.file-upload-remove {
    background: transparent;
    border: none;
    cursor: pointer;
    color: var(--bspkfw-fg-5f6774);
    font-size: 18px;
    line-height: 1;
    padding: 0 6px;
    flex-shrink: 0;
}
.file-upload-remove:hover { color: var(--bspkfw-fg-dc2626); }
.file-upload-progress {
    flex: 1 1 50%;
    height: 6px;
    background: var(--bspkfw-bg-e5e7eb);
    border-radius: 3px;
    overflow: hidden;
}
.file-upload-progress-bar {
    height: 100%;
    width: 0;
    background: var(--bspkfw-accent);
    transition: width 0.15s linear;
}
.file-upload-item--uploading .file-upload-name {
    flex: 0 1 30%;
    color: var(--bspkfw-fg-5f6774);
    font-style: italic;
}

/* "Current files" block rendered above the picker on the edit modal — already-
   saved basenames with download links and ✕ buttons that hit the framework's
   /files/ DELETE endpoint. */
.file-upload-existing {
    margin-bottom: 8px;
}
.file-upload-existing-list {
    list-style: none;
    padding: 0;
    margin: 0;
}
.file-upload-item--existing {
    background: var(--bspkfw-bg-f3f6fa);
    border-color: var(--bspkfw-bd-d6deeb);
}
.file-upload-item--existing .file-upload-name {
    color: var(--bspkfw-accent-text-strong);
    text-decoration: none;
}
.file-upload-item--existing .file-upload-name:hover {
    text-decoration: underline;
}
/* Stored file a queued upload will displace on save (maxFiles cap with
   onExceed 'replace' — EditModal marks the oldest overflow rows so the swap
   is visible before Save, not discovered after). */
.file-upload-item--replacing {
    opacity: 0.6;
}
.file-upload-item--replacing .file-upload-name {
    text-decoration: line-through;
    color: var(--bspkfw-fg-5f6774);
}
.file-upload-item--replacing .file-upload-name:hover {
    text-decoration: line-through underline;
}
.file-upload-replacing-tag {
    flex-shrink: 0;
    font-size: 12px;
    color: var(--bspkfw-fg-92400e);
    background: var(--bspkfw-bg-fef3c7);
    border: 1px solid var(--bspkfw-bd-fde68a);
    border-radius: 4px;
    padding: 1px 6px;
}

/* Thumbnail preview for an already-stored image in a managed-edit FileUpload. */
.file-upload-thumb {
    width: 40px;
    height: 40px;
    object-fit: cover;
    border-radius: 4px;
    border: 1px solid var(--bspkfw-bd-d6deeb);
    flex-shrink: 0;
    background: var(--bspkfw-bg-fff);
}

/* View modal — file column download list. */
.view-file-list {
    list-style: none;
    padding: 0;
    margin: 0;
}
.view-file-list li {
    padding: 2px 0;
}
.view-file-list a {
    color: var(--bspkfw-accent-text-strong);
    text-decoration: none;
}
.view-file-list a:hover {
    text-decoration: underline;
}

/* Gallery layout — used when one or more files in a list are images. Thumbs
   wrap into rows; non-image items still flow as text links inside the same
   list (occupying their natural width). */
.view-file-list--gallery {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
}
.view-file-list--gallery .view-file-item {
    padding: 0;
}
.view-file-list--gallery .view-file-item--thumb {
    flex: 0 0 auto;
}

.view-file-thumb {
    display: block;
    width: 88px;
    height: 88px;
    padding: 0;
    border: 1px solid var(--bspkfw-bd-d6deeb);
    border-radius: 6px;
    background: var(--bspkfw-bg-f3f6fa);
    cursor: zoom-in;
    overflow: hidden;
    transition: transform .12s ease, box-shadow .12s ease, border-color .12s ease;
}
.view-file-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.view-file-thumb:hover {
    transform: translateY(-1px);
    border-color: var(--bspkfw-bd-94a3b8);
    box-shadow: 0 4px 10px var(--bspkfw-sh-rgba-0-0-0-08);
}
.view-file-thumb:focus-visible {
    outline: 2px solid var(--bspkfw-focus-ring);
    outline-offset: 2px;
}

/* Lightbox — fullscreen overlay shown when a thumbnail is clicked. Lives
   directly under <body>; ignores the rest of the page's stacking. */
body.image-lightbox-open { overflow: hidden; }

.image-lightbox {
    position: fixed;
    inset: 0;
    z-index: 9999;
    display: flex;
    align-items: center;
    justify-content: center;
}
.image-lightbox[hidden] { display: none; }
.image-lightbox-backdrop {
    position: absolute;
    inset: 0;
    background: var(--bspkfw-bg-rgba-15-23-42-92);
    cursor: zoom-out;
}
.image-lightbox-stage {
    position: relative;
    z-index: 1;
    max-width: 92vw;
    max-height: 92vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 12px;
    padding: 0 64px;
}
.image-lightbox-image {
    max-width: 100%;
    max-height: 80vh;
    object-fit: contain;
    border-radius: 6px;
    box-shadow: 0 12px 40px var(--bspkfw-sh-rgba-0-0-0-5);
    background: var(--bspkfw-bg-fff);
}
.image-lightbox-caption {
    color: var(--bspkfw-fg-f1f5f9);
    font-size: 13px;
    display: flex;
    gap: 12px;
    align-items: baseline;
}
.image-lightbox-name    { font-weight: 500; word-break: break-all; }
.image-lightbox-text    { color: var(--bspkfw-fg-cbd5e1); }
.image-lightbox-text:empty, .image-lightbox-name:empty { display: none; }
.image-lightbox-counter { color: var(--bspkfw-fg-94a3b8); font-variant-numeric: tabular-nums; }

.image-lightbox-close,
.image-lightbox-prev,
.image-lightbox-next {
    position: absolute;
    z-index: 2;
    background: var(--bspkfw-bg-rgba-255-255-255-12);
    color: var(--bspkfw-fg-f8fafc);
    border: 1px solid var(--bspkfw-bd-rgba-255-255-255-18);
    border-radius: 50%;
    width: 44px;
    height: 44px;
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background .12s ease;
}
.image-lightbox-close:hover,
.image-lightbox-prev:hover,
.image-lightbox-next:hover { background: var(--bspkfw-bg-rgba-255-255-255-24); }
.image-lightbox-close { top: 18px; right: 18px; font-size: 26px; }
.image-lightbox-prev  { left: 18px; top: 50%; transform: translateY(-50%); }
.image-lightbox-next  { right: 18px; top: 50%; transform: translateY(-50%); }
.image-lightbox-prev[hidden], .image-lightbox-next[hidden] { display: none; }

@media (max-width: 540px) {
    .image-lightbox-stage { padding: 0 12px; }
    .image-lightbox-prev, .image-lightbox-next { width: 36px; height: 36px; font-size: 20px; }
    .image-lightbox-close { top: 12px; right: 12px; }
}

.spinner-inline {
    width: 14px;
    height: 14px;
    border-width: 2px;
    margin: 0;
    vertical-align: -3px;
}
@keyframes spinner-rotate { to { transform: rotate(360deg); } }

/* ============================================================
   Login page
   ============================================================ */
.login-wrap {
    /* 52px = .site-header height, 64px = <main>'s 32px top + 32px bottom
       padding. Without both subtractions, the inner box overflows by
       exactly main's vertical padding and the page grows a scrollbar
       even when there's nothing to scroll to. */
    min-height: calc(100vh - 52px - 64px);
    display: flex;
    align-items: center;
    justify-content: center;
}

.login-box {
    background: var(--bspkfw-bg-fff);
    border-radius: 10px;
    box-shadow: 0 4px 16px var(--bspkfw-sh-rgba-0-0-0-1);
    padding: 36px;
    width: 360px;
    max-width: calc(100vw - 48px);
}

.login-box h1 { font-size: 20px; font-weight: 600; margin-bottom: 24px; text-align: center; }

/* /login's side panel (Login::panel()): a picture and a welcome text. The
   panel follows the form in the document (keyboard and screen readers
   reach the form first); where it shows is this. Phones: one column, the
   picture, heading and text above the form. 900px and up: beside the box,
   on the start or the end side. The picture's box has a fixed shape, so a
   large image never shifts the layout, and text never sits on the picture. */
.login-wrap--panel {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-content: center;
    justify-items: center;
    gap: 24px;
    padding: 16px 0;
}
.login-wrap--panel > .login-panel { order: -1; }
.login-panel {
    width: 100%;
    max-width: 560px;
    background: var(--bspkfw-bg-fff);
    border-radius: 10px;
    box-shadow: 0 4px 16px var(--bspkfw-sh-rgba-0-0-0-1);
    overflow: hidden;
    color: var(--bspkfw-fg-374151);
}
.login-panel-img {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    max-height: 60vh;
    object-fit: cover;
}
.login-panel-heading {
    margin: 20px 24px 8px;
    font-size: 20px;
    font-weight: 600;
    color: var(--bspkfw-fg-111827);
}
.login-panel-text { margin: 0 24px 24px; line-height: 1.55; }
.login-panel-img + .login-panel-text { margin-top: 20px; }
.login-panel:not(:has(.login-panel-heading, .login-panel-text)) .login-panel-img { aspect-ratio: auto; }
@media (min-width: 900px) {
    .login-wrap--panel {
        grid-template-columns: minmax(0, 1fr) 420px;
        align-items: center;
        max-width: 1100px;
        margin: 0 auto;
        gap: 40px;
    }
    .login-wrap--panel-end { grid-template-columns: 420px minmax(0, 1fr); }
    .login-wrap--panel-end > .login-panel { order: 1; }
    .login-panel { max-width: none; }
    .login-panel-img { aspect-ratio: 4 / 3; max-height: none; }
}
/* The install's name above the sign-in heading (Login::titleHtml()). */
.login-brand {
    margin: 0 0 8px;
    text-align: center;
    font-size: 15px;
    font-weight: 600;
    color: var(--bspkfw-fg-374151);
}

.btn-block { width: 100%; justify-content: center; margin-top: 8px; }

.login-footer { margin-top: 20px; text-align: center; font-size: 13px; color: var(--bspkfw-fg-5f6774); }

/* A person's own invite link (InviteCodes::personalBlockHtml(): /account, a project dashboard). */
.personal-invite__body { display: flex; gap: 24px; align-items: flex-start; flex-wrap: wrap; }
.personal-invite__main { flex: 1 1 280px; min-width: 0; }
.personal-invite__row { display: flex; gap: 8px; }
.personal-invite__link { flex: 1 1 auto; min-width: 0; }
.personal-invite__row .btn { flex: 0 0 auto; white-space: nowrap; }
.personal-invite__meta { margin: 8px 0 12px; font-size: 13px; color: var(--bspkfw-fg-5f6774); }
.personal-invite__actions { display: flex; gap: 8px; flex-wrap: wrap; }
.personal-invite__qr { flex: 0 0 132px; width: 132px; height: 132px; }
.personal-invite__qr svg { width: 100%; height: 100%; display: block; }

/* "Invited by Sam" above /signup's form (a personal invite link). */
.signup-invited-by { margin: -12px 0 20px; text-align: center; font-size: 14px; color: var(--bspkfw-fg-374151); }

/* Auth boxes one above the other (/signup's waitlist + "Got an invitation?"). */
.login-wrap--stack { flex-direction: column; gap: 20px; padding: 16px 0; }
.signup-unlock h2 { font-size: 16px; font-weight: 600; margin-bottom: 8px; text-align: center; }

/* The launch waitlist (Waitlist::formHtml(), /waitlist/*, /waitlist). */
.waitlist-join__intro { font-size: 14px; color: var(--bspkfw-fg-5f6774); margin-bottom: 16px; text-align: center; }
.waitlist-join__done { font-size: 14px; }
.waitlist-page .stat-grid { margin-bottom: 16px; }
.waitlist-toolbar { justify-content: space-between; }
.waitlist-toolbar .table-quick-filters { margin-bottom: 0; }
.waitlist-toolbar__actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.waitlist-toolbar__actions .btn:disabled { opacity: 0.55; cursor: not-allowed; }
.waitlist-select { width: 32px; }
.waitlist-actions { white-space: nowrap; text-align: right; }
.forgot-link { text-align: right; font-size: 13px; margin-top: -4px; margin-bottom: 4px; }

.alert-list { margin: 0; padding-left: 20px; }
.alert-list li + li { margin-top: 4px; }

/* /settings → WebSockets test: the step list + live connection-state cell. */
.ws-test-table th { width: 260px; white-space: nowrap; }
.ws-test-table td { word-break: break-all; }
.ws-test-steps { list-style: none; margin: 12px 0; padding: 0; }
.ws-test-steps li { display: flex; gap: 12px; padding: 6px 0; border-bottom: 1px solid var(--border, var(--bspkfw-bd-e5e7eb)); }
.ws-step-label { flex: 0 0 320px; }
.ws-step-value { flex: 1 1 auto; word-break: break-all; }
.ws-step-wait { color: var(--bspkfw-fg-5f6774); }
.ws-step-ok, .ws-state-ok { color: var(--bspkfw-fg-166534); }
.ws-step-warn, .ws-state-warn { color: var(--bspkfw-fg-92400e); }
.ws-step-bad, .ws-state-bad { color: var(--bspkfw-fg-991b1b); }
@media (max-width: 640px) {
    .ws-test-table th { width: auto; white-space: normal; }
    .ws-test-steps li { flex-direction: column; gap: 2px; }
    .ws-step-label { flex-basis: auto; }
}

.field-hint { font-weight: normal; color: var(--bspkfw-fg-5f6774); font-size: 12px; }

/* ============================================================
   Landing hero
   ============================================================ */
.hero {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    /* Account for <main>'s 64px vertical padding the same way .login-wrap
       does — without that, the page grows a 64px-tall scrollbar tail. */
    min-height: calc(100vh - 52px - 64px);
    text-align: center;
    padding: 48px 24px;
}

.hero h1 { font-size: 42px; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 16px; }

.hero-sub { font-size: 18px; color: var(--bspkfw-fg-5f6774); max-width: 480px; margin-bottom: 36px; line-height: 1.6; }

.hero-actions { display: flex; gap: 12px; }

/* ============================================================
   Dialog (alert / confirm / prompt)
   ============================================================ */
.dialog-box {
    max-width: 400px;
    padding: 28px 24px 20px;
}

.dialog-msg {
    margin-bottom: 20px;
    line-height: 1.6;
    color: var(--bspkfw-fg-1f2937);
    font-size: 14px;
    white-space: pre-wrap;
}

.dialog-prompt-input {
    margin-bottom: 16px;
}

.dialog-btns {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
}

/* Structured item list under the dialog message (the `items` option on
   Dialog.confirm / confirmAsync — see CLAUDE/MODALS.md). Scrolls inside
   itself so twenty attachments can never push the buttons off-screen. */
.dialog-items {
    margin: -8px 0 20px;
    padding-left: 22px;
    max-height: 168px;          /* ~7 rows, then the list scrolls */
    overflow-y: auto;
    font-size: 14px;
    line-height: 1.6;
    color: var(--bspkfw-fg-1f2937);
}

.dialog-items li { margin: 2px 0; overflow-wrap: anywhere; }

/* confirmAsync's busy window: links are inert (the JS also swallows
   clicks and drops them from the tab order). */
.dialog-items a[aria-disabled="true"] {
    pointer-events: none;
    opacity: 0.5;
}

/* ============================================================
   Notifications
   ============================================================ */
#notify-container {
    position: fixed;
    top: 16px;
    right: 16px;
    /* Above the user-menu dropdown (200) and the nav drawer (301): a toast
       answering a click inside a menu must never be hidden by that menu. */
    z-index: 400;
    display: flex;
    flex-direction: column;
    gap: 8px;
    pointer-events: none;
}

/* Scoped to #notify-container so the toast's `opacity: 0` (slide-in
   start state) doesn't bleed onto the bell dropdown's `.notify-item`
   rows — they share the class name but live in `.notify-dropdown-list`,
   not here, and never get the `.notify-visible` toggle. */
#notify-container .notify-item {
    pointer-events: all;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 16px;
    border-radius: 8px;
    box-shadow: 0 4px 12px var(--bspkfw-sh-rgba-0-0-0-12);
    font-size: 14px;
    font-weight: 500;
    max-width: 320px;
    opacity: 0;
    transform: translateX(20px);
    transition: opacity 0.2s ease, transform 0.2s ease;
}

#notify-container .notify-item.notify-visible { opacity: 1; transform: translateX(0); }

.notify-success { background: var(--bspkfw-bg-f0fdf4); color: var(--bspkfw-fg-166534); border: 1px solid var(--bspkfw-bd-bbf7d0); }
.notify-error   { background: var(--bspkfw-bg-fef2f2); color: var(--bspkfw-fg-991b1b); border: 1px solid var(--bspkfw-bd-fecaca); }
.notify-info    { background: var(--bspkfw-accent-soft); color: var(--bspkfw-accent-text-strong); border: 1px solid var(--bspkfw-accent-border); }
.notify-warning { background: var(--bspkfw-bg-fffbeb); color: var(--bspkfw-fg-92400e); border: 1px solid var(--bspkfw-bd-fde68a); }

.notify-msg { flex: 1; }

.notify-close {
    background: none;
    border: none;
    cursor: pointer;
    font-size: 18px;
    line-height: 1;
    color: inherit;
    opacity: 0.5;
    padding: 0 2px;
    flex-shrink: 0;
}

.notify-close:hover { opacity: 1; }

/* One action button inside a toast (notify(msg, type, {action})): "Undo",
   "Reload this page" — stacked UNDER the message in a `.notify-body`
   column, so the text keeps the toast's full width. Inherits the toast's
   colour so it reads as part of it. */
.notify-body {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
    min-width: 0;
}
.notify-action {
    background: none;
    border: 1px solid currentColor;
    border-radius: 4px;
    color: inherit;
    cursor: pointer;
    font: inherit;
    font-size: 13px;
    line-height: 1.2;
    padding: 3px 8px;
    white-space: nowrap;
}
.notify-action:hover { background: var(--bspkfw-bg-rgba-0-0-0-06); }

/* Visually-hidden but still in the accessibility tree — the standard
   "screen-reader only" clip recipe. Foundational a11y primitive: skip
   links, icon-only control names, live-region labels (notify.js's mirrors
   use .sr-only), etc. `.sr-only` is always hidden; `.sr-only-focusable`
   is hidden until focused (or it contains focus), then paints normally.
   NOT display:none / visibility:hidden / width:0 — those drop the node
   from the a11y tree or truncate the announcement. */
.sr-only,
.sr-only-focusable:not(:focus):not(:focus-within) {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
}

/* The focusable variant, while focused: a normal, clearly-visible control.
   Sensible defaults (surface bg, dark text, the framework's primary-blue
   :focus-visible outline) so a bare `<a class="sr-only-focusable">` looks
   right with no per-project styling; consumers add positioning (e.g. a
   skip link pins itself to the top-left). */
.sr-only-focusable:focus,
.sr-only-focusable:focus-within {
    padding: 8px 16px;
    background: var(--bspkfw-bg-fff);
    color: var(--bspkfw-fg-1f2937);
    border-radius: 6px;
    outline: 2px solid var(--bspkfw-focus-ring);
    outline-offset: 2px;
    box-shadow: 0 4px 12px var(--bspkfw-sh-rgba-0-0-0-15);
    z-index: 1000;
}

/* ============================================================
   Enhanced table (search, sort, responsive)
   ============================================================ */

/* width: auto lets the table overflow its container when content is too wide,
   which is required for the JS overflow check to work. min-width: 100% ensures
   it still fills the container when there is enough space. */
table.enhanced-table { width: auto; min-width: 100%; }

.enhanced-table-toolbar { margin-bottom: 12px; }

.enhanced-table-search { max-width: 280px; }

.col-sortable { cursor: pointer; user-select: none; }
.col-sortable:hover { background: var(--bspkfw-bg-f3f4f6); color: var(--bspkfw-fg-374151); }

.col-sortable::after { content: '\00a0\2195'; opacity: 0.35; font-size: 0.85em; }
.col-sortable[aria-sort="ascending"]::after  { content: '\00a0\2191'; opacity: 1; color: var(--bspkfw-accent-text); }
.col-sortable[aria-sort="descending"]::after { content: '\00a0\2193'; opacity: 1; color: var(--bspkfw-accent-text); }

/* Expand button — prepended to the first cell when columns are hidden */
.et-expand-btn {
    position: relative;
    display: inline-block;
    width: 18px;
    height: 18px;
    flex-shrink: 0;
    vertical-align: middle;
    /* 10px, not less: the button (18px) and the row's checkbox (~13px) are
       both under WCAG 2.5.8's 24px, so their centres must sit >= 24px apart
       (axe `target-size`, tests/Browser/specs/a11y.spec.js). */
    margin-right: 10px;
    padding: 0;
    border: 1px solid var(--bspkfw-bd-d1d5db);
    border-radius: 4px;
    background: var(--bspkfw-bg-fff);
    /* hide the + / − character so we can draw a perfectly centered glyph instead */
    font-size: 0;
    color: transparent;
    cursor: pointer;
}
.et-expand-btn::before,
.et-expand-btn::after {
    content: '';
    position: absolute;
    left: 50%;
    top: 50%;
    background: var(--bspkfw-bg-5f6774);
    transform: translate(-50%, -50%);
}
.et-expand-btn::before { width: 8px; height: 2px; }
.et-expand-btn::after  { width: 2px; height: 8px; }
.et-expand-btn[aria-expanded="true"]::after { display: none; }
.et-expand-btn:hover { background: var(--bspkfw-bg-f3f4f6); }
.et-expand-btn:hover::before,
.et-expand-btn:hover::after { background: var(--bspkfw-bg-374151); }

/* Detail row — the temporary row injected below an expanded row */
.et-detail-cell {
    background: var(--bspkfw-bg-f8fafc);
    padding: 10px 14px 12px 32px;
    border-bottom: 1px solid var(--bspkfw-bd-e5e7eb);
}

.et-detail-list {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 6px 16px;
    margin: 0;
}

.et-detail-list dt {
    font-size: 11px;
    font-weight: 600;
    color: var(--bspkfw-fg-5f6774);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    white-space: nowrap;
    align-self: baseline;
}

.et-detail-list dd { margin: 0; color: var(--bspkfw-fg-374151); font-size: 13px; }

/* ============================================================
   View modal
   ============================================================ */
.view-field-list {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin: 0;
    padding: 4px 0;
}

.view-field {
    display: grid;
    grid-template-columns: 160px 1fr;
    gap: 4px 16px;
    align-items: baseline;
}

.view-field dt {
    font-size: 11px;
    font-weight: 600;
    color: var(--bspkfw-fg-5f6774);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    align-self: baseline;
    padding-top: 2px;
}

.view-field dd {
    margin: 0;
    color: var(--bspkfw-fg-111827);
    font-size: 14px;
    word-break: break-word;
}

.view-null { color: var(--bspkfw-fg-5f6774); font-style: italic; }

/* View-modal field groups (BaseObject::viewFieldGroups) — the read-only
   sibling of .bspkfw-em-group. Native <details> so the toggle needs no JS
   (CSP-safe); ungrouped fields render above in a bare .view-field-list. */
.view-field-group {
    border: 1px solid var(--bspkfw-bd-e5e7eb);
    border-radius: 8px;
    padding: 10px 16px;
    margin: 12px 0 0;
}
.view-field-group > .view-field-group-legend {
    cursor: pointer;
    font-weight: 600;
    color: var(--bspkfw-fg-1f2937);
    user-select: none;
}
.view-field-group[open] > .view-field-group-legend { margin-bottom: 8px; }

/* Manage-grid file-cell thumbnail (fileColumns() 'grid' => 'thumb…').
   Hard height cap so a register's rows stay rows; object-fit crops
   rather than distorts when the declared variant isn't square. */
.bspkfw-grid-thumb {
    display: block;
    height: 44px;
    max-width: 88px;
    object-fit: cover;
    border-radius: 4px;
}

/* Boolean (checkbox-typed) cell values — manage list + view modal.
   The "off" state reuses .view-null so it matches empty cells. */
.bspkfw-bool { font-style: normal; }
.bspkfw-bool-yes { color: var(--bspkfw-fg-16a34a); font-weight: 600; }

/* Cell decorations (BaseObject::cellDecorations) — a small mark rendered
   BESIDE a grid cell's value. Deliberately colourless: the object's own
   `class` decides how the mark looks, and inheriting currentColor means an
   undressed mark still reads correctly on every theme. */
.bspkfw-cell-mark {
    display: inline-block;
    font-style: normal;
    font-size: 0.95em;
    line-height: 1;
    white-space: nowrap;
}
.bspkfw-cell-mark--after  { margin-left: 0.35em; }
.bspkfw-cell-mark--before { margin-right: 0.35em; }

.view-link-list {
    margin: 2px 0 0;
    padding-left: 18px;
    color: var(--bspkfw-fg-111827);
}
.view-link-list li { padding: 1px 0; }

.duration-ago { color: var(--bspkfw-fg-5f6774); font-size: 0.85em; }

.view-field--block {
    grid-template-columns: 1fr;
}

.view-json-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
    margin-top: 2px;
}

.view-json-table th,
.view-json-table td {
    padding: 5px 8px;
    border: 1px solid var(--bspkfw-bd-e5e7eb);
    text-align: left;
    vertical-align: top;
}

.view-json-table thead th {
    background: var(--bspkfw-bg-f3f4f6);
    font-weight: 600;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--bspkfw-fg-5f6774);
}

.view-json-table tbody th {
    background: var(--bspkfw-bg-f9fafb);
    font-weight: 600;
    color: var(--bspkfw-fg-374151);
    width: 140px;
    white-space: nowrap;
}

@media (max-width: 480px) {
    .view-field {
        grid-template-columns: 1fr;
    }
}

/* ============================================================
   Home page
   ============================================================ */
.home-list {
    list-style: none;
    margin-top: var(--bspkfw-space-section-gap, 20px);
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.home-list a {
    display: inline-block;
    padding: 10px 16px;
    background: var(--bspkfw-bg-fff);
    border: 1px solid var(--bspkfw-bd-e5e7eb);
    border-radius: 6px;
    font-weight: 500;
}

.home-list a:hover { border-color: var(--bspkfw-accent-line); background: var(--bspkfw-accent-soft); text-decoration: none; }

/* ============================================================
   View modal user manager
   ============================================================ */
.vm-user-manager {
    border-top: 1px solid var(--bspkfw-bd-e5e7eb);
    padding-top: 16px;
    margin-top: 16px;
}

.vm-user-heading {
    font-size: 12px;
    font-weight: 600;
    color: var(--bspkfw-fg-5f6774);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin-bottom: 8px;
}

.vm-user-list {
    list-style: none;
    margin-bottom: 12px;
}

.vm-user-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 6px 0;
    border-bottom: 1px solid var(--bspkfw-bd-f3f4f6);
}

.vm-user-row:last-child { border-bottom: none; }

.vm-add-user {
    display: flex;
    gap: 8px;
    margin-top: 8px;
}

.vm-add-user .form-control { flex: 1; }

/* ============================================================
   Profile page
   ============================================================ */
.profile-page { max-width: 680px; }

/* ============================================================
   Settings page (/settings) — the page's width for its tables, a readable
   column for everything you read or fill in (since 2026-10-28)
   ============================================================

   /settings was a .profile-page: every section held to 680px, so the Site
   Settings table hid its Delete column behind a sideways scroll on every
   row. Removing the cap alone is worse: inputs and paragraphs run 1290px,
   and .form-actions (a right-aligned flex row) puts each Save ~600px from
   the fields it saves. So the SECTION takes the width and the tables fill
   it, while prose keeps the reading line and each form — and a Save after
   a lone field — keeps the form measure, Save flush with its fields.
   Actions that follow a table belong to the table and end where it does
   (`.settings-table-actions`).
   A two-column list (the schema object list: a name and its Edit) keeps
   the form measure too: at full width the button leaves its row. */
.settings-page .page-section p,
.settings-page .page-section-desc { max-width: 75ch; }

.settings-page .page-section form,
.settings-page .page-section .form-group,
.settings-page .page-section .form-row,
.settings-page .page-section .form-actions:not(.settings-table-actions),
.settings-page #schema-object-section { max-width: var(--bspkfw-measure-form, 560px); }
/* A fold under a button row (Backups' "Data directories") is a touch target
   of its own: at least 24px tall and clear of the buttons above it (WCAG
   2.5.8, the a11y sweep's target-size). */
.settings-page .page-section > details { margin-top: 12px; }
.settings-page .page-section > details > summary { min-height: 24px; display: flex; align-items: center; cursor: pointer; }

/* Profile photo widget — sits above the email/display-name form */
.profile-photo-widget {
    display: flex;
    align-items: center;
    gap: 16px;
    margin-bottom: 16px;
}
.profile-photo-preview {
    width: 96px;
    height: 96px;
    border-radius: 50%;
    overflow: hidden;
    background: var(--bspkfw-bg-e5e7eb) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 96 96'><circle cx='48' cy='36' r='18' fill='%239ca3af'/><path d='M16 88c0-18 14-32 32-32s32 14 32 32' fill='%239ca3af'/></svg>") center/cover no-repeat;
    flex-shrink: 0;
    border: 1px solid var(--bspkfw-bd-e5e7eb);
}
.profile-photo-preview img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.profile-photo-widget.has-no-photo .profile-photo-preview img { display: none; }
.profile-photo-actions { flex: 1; }

/* Cropper modal */
.cropper-stage {
    padding: 20px;
    display: flex;
    justify-content: center;
}
.cropper-viewport {
    width: 320px;
    max-width: 100%;       /* shrink to fit a narrow modal on small screens */
    aspect-ratio: 1 / 1;   /* stay square (circular) at any rendered width */
    overflow: hidden;      /* belt-and-braces; clip-path does the real work */
    position: relative;
    background: var(--bspkfw-bg-111827);
    border-radius: 50%;    /* shapes the 1px border */
    /* The preview <img> is driven by an inline transform (photo-cropper.js),
       which promotes it to its own compositing layer and escapes the parent's
       rounded-corner overflow clip in Chromium/WebKit — especially once the
       box is sized by aspect-ratio rather than a fixed height. clip-path
       clips composited/transformed descendants reliably, keeping the circle. */
    clip-path: circle(50%);
    cursor: grab;
    touch-action: none;
    user-select: none;
}
.cropper-viewport:active { cursor: grabbing; }
.cropper-image {
    position: absolute;
    top: 0;
    left: 0;
    transform-origin: 0 0;
    pointer-events: none;
    user-select: none;
    max-width: none;
    max-height: none;
}
.cropper-controls {
    padding: 0 24px 12px;
}
.cropper-toolbar {
    display: flex;
    align-items: center;
    gap: 12px;
}
.cropper-toolbar .cropper-zoom-label { flex: 1; }
.cropper-rotate {
    display: flex;
    gap: 6px;
}
/* When JS hides the rotate group (rotate: false), the zoom label's
   flex: 1 reclaims the row — layout identical to the pre-rotate modal. */
.cropper-rotate[hidden] { display: none; }
.cropper-zoom-label {
    display: flex;
    align-items: center;
    gap: 12px;
    font-size: 13px;
    color: var(--bspkfw-fg-374151);
}
.cropper-zoom-label input[type="range"] { flex: 1; }

.page-sections {
    display: flex;
    flex-direction: column;
    gap: var(--bspkfw-space-section-gap, 20px);
}

.page-section {
    background: var(--bspkfw-bg-fff);
    border: 1px solid var(--bspkfw-bd-e5e7eb);
    border-radius: 8px;
    padding: var(--bspkfw-space-card, 24px);
}

.page-section-title {
    font-size: 15px;
    font-weight: 600;
    margin-bottom: 4px;
}

.page-section-desc {
    font-size: 13px;
    color: var(--bspkfw-fg-5f6774);
    margin-bottom: var(--bspkfw-space-section-gap, 20px);
}

.page-section-desc:last-child { margin-bottom: 0; }

.page-section--danger { border-color: var(--bspkfw-bd-fca5a5); }
.page-section--danger .page-section-title { color: var(--bspkfw-fg-dc2626); }

.page-section.collapsible:not(.bspkfw-fold) > .page-section-title {
    cursor: pointer;
    user-select: none;
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 4px;
}

.page-section.collapsible:not(.bspkfw-fold) > .page-section-title::after {
    content: '▾';
    display: inline-block;
    color: var(--bspkfw-fg-9ca3af);
    font-size: 12px;
    flex-shrink: 0;
    transition: transform 0.15s;
}

.page-section.collapsible.collapsed:not(.bspkfw-fold) { cursor: pointer; }
.page-section.collapsible.collapsed:not(.bspkfw-fold) > .page-section-title { margin-bottom: 0; }
.page-section.collapsible.collapsed:not(.bspkfw-fold) > .page-section-title::after { transform: rotate(-90deg); }
.page-section.collapsible.collapsed:not(.bspkfw-fold) > *:not(.page-section-title) { display: none; }

/* Html::collapsibleSection() — keyed on .bspkfw-fold, not .page-section, so a
   project card class (`class => 'wsk-card'`) folds the same. The title row is
   a flex row: the toggle (looks like the heading) and the header extras at
   the right. The chevron is CSS-drawn: right when folded, down when open;
   before the title with `chevron => 'start'`, at the row's end otherwise. */
.bspkfw-fold > .page-section-title { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.bspkfw-fold.collapsed > .page-section-title { margin-bottom: 0; }
.bspkfw-fold-heading { font: inherit; margin: 0; flex: 1 1 auto; min-width: 0; }
.bspkfw-fold-toggle {
    appearance: none; background: none; border: 0; margin: 0; padding: 0;
    font: inherit; color: inherit; text-align: start; cursor: pointer;
    display: flex; align-items: center; gap: 8px; width: 100%;
}
.bspkfw-fold:not(.is-chevron-start) .bspkfw-fold-toggle { justify-content: space-between; }
.bspkfw-fold-toggle:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; border-radius: 4px; }
.bspkfw-fold-chevron {
    flex: 0 0 auto; width: 0.45em; height: 0.45em; margin: 0 3px 0.2em;
    border-right: 2px solid var(--bspkfw-bd-9ca3af); border-bottom: 2px solid var(--bspkfw-bd-9ca3af);
    transform: rotate(45deg); transition: transform 0.15s ease;
}
.bspkfw-fold.collapsed .bspkfw-fold-chevron { transform: rotate(-45deg); margin-bottom: 0; }
.bspkfw-fold-extras { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; }
.bspkfw-fold > .page-section-body[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) { .bspkfw-fold-chevron { transition: none; } }

/* ============================================================
   Communications preferences table
   ============================================================ */
.comms-pref-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 14px;
    margin-bottom: 20px;
}

.comms-pref-table th,
.comms-pref-table td {
    padding: 10px 12px;
    border-bottom: 1px solid var(--bspkfw-bd-e5e7eb);
    vertical-align: middle;
}

.comms-pref-table thead th {
    background: var(--bspkfw-bg-f9fafb);
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--bspkfw-fg-5f6774);
    text-align: center;
}

.comms-pref-table thead th:first-child { text-align: left; }

.comms-pref-table tbody tr:last-child td { border-bottom: none; }

.comms-pref-check { text-align: center; width: 100px; }

.comms-pref-name { text-align: left; }

.comms-pref-label {
    display: block;
    font-weight: 500;
}

.comms-pref-desc {
    display: block;
    font-size: 12px;
    color: var(--bspkfw-fg-5f6774);
    margin-top: 2px;
}

.comms-channels-bar { margin-bottom: 16px; }

.comms-ch-edit {
    cursor: pointer;
    user-select: none;
}
.comms-ch-edit:hover { background: var(--bspkfw-bg-f3f4f6); }

.comms-pref-head-label { display: block; }

/* Per-channel "select / unselect all" checkbox in the comms-prefs header. */
.comms-select-all { cursor: pointer; margin: 4px 0 0; }

/* Inline field help (Field option `help`, and the descriptor key of the same
   name). `display: block` is load-bearing, not cosmetic: this is a <small>,
   and vertical margins are inert on a non-replaced inline element, so without
   it the 4px below rounds to nothing and the help sits flush against the
   input. Every sibling below-input channel already sidesteps this by being a
   block/flex node (.field-error, .form-advisory, .bspkfw-postal-alts). Colour
   routes through the shared field palette — the token's :root default IS this
   rule's former literal, so a light theme is byte-identical while midnight /
   sepia stop rendering grey-on-dark. */
.form-help {
    display: block;
    font-size: 12px;
    color: var(--bspkfw-field-muted, var(--bspkfw-fg-5f6774));
    margin-top: 4px;
}
/* A note the field adds itself (DurationInput's "Rounded up from 4:33"): help-quiet, never an error; wraps under the field. */
.form-help--note { font-style: italic; overflow-wrap: anywhere; }

/* "Show all / Show fewer" toggle under a default-group-filtered select
   (optgroup-filter.js — see Form\Select's defaultGroups option). */
.bspkfw-og-toggle {
    display: block;
    background: none;
    border: none;
    padding: 0;
    margin-top: 4px;
    font-size: 12px;
    color: var(--bspkfw-accent-text);
    cursor: pointer;
}
.bspkfw-og-toggle:hover { text-decoration: underline; }

/* ============================================================
   Badges
   ============================================================ */
.badge {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 10px;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.03em;
}
.badge-success { background: var(--bspkfw-bg-dcfce7); color: var(--bspkfw-fg-166534); }
.badge-info    { background: var(--bspkfw-accent-soft-2); color: var(--bspkfw-accent-text-strong); }
.badge-warning { background: var(--bspkfw-bg-fef9c3); color: var(--bspkfw-fg-854d0e); }
.badge-danger  { background: var(--bspkfw-bg-fee2e2); color: var(--bspkfw-fg-991b1b); }
.badge-neutral { background: var(--bspkfw-bg-f3f4f6); color: var(--bspkfw-fg-374151); }

/* API tokens section (ApiTokensSection): the Replace panel under the list. */
.api-tokens-replace { border: 1px solid var(--bspkfw-border); border-radius: 8px; padding: 12px 16px; margin: 12px 0; }
.api-tokens-replace h3 { margin-top: 0; }

/* /invite-codes: the code made just now, shown once with its sign-up link (pages/invite-codes.js). */
.invite-code-made__row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 8px; }
.invite-code-made__code { font-size: 1.25rem; letter-spacing: 0.08em; font-weight: 600; }
.invite-code-made__link { flex: 1 1 18rem; min-width: 0; }

/* ------------------------------------------------------------------
   Stat tiles — a responsive grid of labelled count cards. Shared so
   any dashboard-style page (jobs queue, reports summaries, …) renders
   the same treatment. Tones reuse the .badge colour palette.
   ------------------------------------------------------------------ */
.stat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: 12px;
}
.stat-card {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 14px 16px;
    border: 1px solid var(--bspkfw-bd-e5e7eb);
    border-left-width: 4px;
    border-radius: 8px;
    background: var(--bspkfw-bg-fff);
}
.stat-card-value { font-size: 26px; font-weight: 700; line-height: 1; }
.stat-card-label { font-size: 12px; color: var(--bspkfw-fg-5f6774); text-transform: uppercase; letter-spacing: 0.03em; }
.stat-card--success { border-left-color: var(--bspkfw-bd-16a34a); }
.stat-card--info    { border-left-color: var(--bspkfw-accent-line); }
.stat-card--warning { border-left-color: var(--bspkfw-bd-ca8a04); }
.stat-card--danger  { border-left-color: var(--bspkfw-bd-dc2626); }
.stat-card--neutral { border-left-color: var(--bspkfw-bd-9ca3af); }
.stat-oldest { margin-top: 12px; font-size: 14px; }
.stat-oldest .stat-card-label { display: inline; }
.stat-oldest--stale { color: var(--bspkfw-fg-dc2626); }

/* Monospace error cell — wraps long messages, preserves whitespace,
   tinted so a failed-job error reads as "this is the problem". */
.job-error {
    display: block;
    max-width: 480px;
    white-space: pre-wrap;
    word-break: break-word;
    color: var(--bspkfw-fg-991b1b);
    font-size: 12px;
}
.job-error em { color: var(--bspkfw-fg-5f6774); font-style: italic; }

/* ============================================================
   Table toolbar, sortable headers, and pagination
   ============================================================ */
.table-toolbar { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; flex-wrap: wrap; }
.table-search  { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; }
.table-search input { flex: 1; min-width: 0; }
.table-page-size { display: flex; align-items: center; gap: 6px; font-size: 0.875rem; white-space: nowrap; margin-bottom: 0; }

/* Density toggle button (manage toolbar) — a two-state aria-pressed
   toggle; pressed = condensed. Mirrors the .bspkfw-view-filter treatment:
   quiet at rest, tinted ring when the non-default state is active. */
.table-density-toggle {
    display: inline-flex;
    align-items: center;
    background: transparent;
    border: 1px solid var(--bspkfw-bd-d1d5db);
    border-radius: 4px;
    padding: 6px 8px;
    cursor: pointer;
    color: var(--bspkfw-fg-5f6774);
    transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.table-density-toggle:hover {
    background: var(--bspkfw-bg-f3f4f6);
    color: var(--bspkfw-fg-111827);
}
.table-density-toggle:focus-visible {
    outline: 2px solid var(--bspkfw-accent-bright);
    outline-offset: 1px;
}
.table-density-toggle[aria-pressed="true"] {
    color: var(--bspkfw-accent-text);
    border-color: var(--bspkfw-accent-border);
    background: var(--bspkfw-accent-soft);
}

/* "Show deleted" (soft-deleting objects) — same tinted-ring treatment as
   the density toggle while the deleted rows are on screen. */
.table-show-deleted[aria-pressed="true"] {
    color: var(--bspkfw-accent-text);
    border-color: var(--bspkfw-accent-border);
    background: var(--bspkfw-accent-soft);
}

/* Quick-filter chip row (object_config.quick_filters) — one group per
   declared column, rendered by manage.js between the toolbar and the
   table. Chips are aria-pressed toggles; active = the same tinted-ring
   treatment as the density toggle / view-filter buttons. */
.table-quick-filters {
    display: flex;
    align-items: center;
    gap: 6px 16px;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.table-qf-group {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
}
.table-qf-label {
    font-size: 0.8125rem;
    color: var(--bspkfw-fg-5f6774);
    white-space: nowrap;
}
.table-qf-chip {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    background: transparent;
    border: 1px solid var(--bspkfw-bd-d1d5db);
    border-radius: 999px;
    padding: 3px 12px;
    font-size: 0.8125rem;
    line-height: 1.4;
    color: var(--bspkfw-fg-374151);
    cursor: pointer;
    white-space: nowrap;
    transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.table-qf-chip:hover {
    background: var(--bspkfw-bg-f3f4f6);
    color: var(--bspkfw-fg-111827);
}
.table-qf-chip:focus-visible {
    outline: 2px solid var(--bspkfw-accent-bright);
    outline-offset: 1px;
}
.table-qf-chip.is-active {
    color: var(--bspkfw-accent-text);
    border-color: var(--bspkfw-accent-border);
    background: var(--bspkfw-accent-soft);
}
.table-qf-count {
    font-size: 0.75rem;
    color: var(--bspkfw-fg-5f6774);
}
.table-qf-chip.is-active .table-qf-count { color: var(--bspkfw-accent-text); opacity: 0.75; }

th.sortable { cursor: pointer; user-select: none; white-space: nowrap; }
th.sortable:hover { background: var(--bspkfw-bg-rgba-0-0-0-04); }
.sort-icon { opacity: 0.3; font-size: 0.8em; margin-left: 4px; }
th.sort-asc .sort-icon, th.sort-desc .sort-icon { opacity: 1; }
th.sort-asc .sort-icon::before  { content: '↑'; }
th.sort-desc .sort-icon::before { content: '↓'; }
th.sortable:not(.sort-asc):not(.sort-desc) .sort-icon::before { content: '↕'; }

/* Resizable columns — table-resize.js (Html::resizableTableAttrs(), the
   manage grid). The handle is an 8px hit area on the header's trailing edge;
   a sized table switches to fixed layout so one column moves at a time. */
table[data-bspkfw-resize] > thead > tr > th[data-col] { position: relative; }
.bspkfw-col-resize {
    position: absolute; top: 0; bottom: 0; inset-inline-end: 0; width: 8px;
    cursor: col-resize; touch-action: none; z-index: 1;
}
.bspkfw-col-resize::after {
    content: ''; position: absolute; top: 20%; bottom: 20%; inset-inline-end: 3px;
    width: 2px; border-radius: 1px; background: transparent;
}
th[data-col]:hover > .bspkfw-col-resize::after { background: var(--bspkfw-col-resize-idle, var(--bspkfw-bg-d1d5db)); }
.bspkfw-col-resize:hover::after,
.bspkfw-col-resize:focus-visible::after,
.bspkfw-col-resize.is-dragging::after { background: var(--bspkfw-col-resize-active, var(--bspkfw-accent)); }
.bspkfw-col-resize:focus-visible { outline: none; }
table.is-col-sized { table-layout: fixed; }
table.is-col-sized > thead > tr > th { overflow: hidden; text-overflow: ellipsis; }
table.is-col-sized > tbody > tr > td { overflow-wrap: anywhere; }
html.is-col-resizing, html.is-col-resizing * { cursor: col-resize !important; user-select: none; }

.table-pagination { display: flex; align-items: center; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.pg-info  { flex: 1; text-align: center; color: var(--bspkfw-fg-5f6774); font-size: 0.875rem; }
.pg-count { font-size: 0.875rem; color: var(--bspkfw-fg-5f6774); }

/* ============================================================
   Schema modal
   ============================================================ */
.sm-hint { font-weight: normal; font-size: 0.8em; color: var(--bspkfw-fg-5f6774); }

.sm-meta {
    flex-shrink: 0;
    padding: 0 24px;
}

.sm-field-actions {
    flex-shrink: 0;
    display: flex;
    gap: 6px;
    padding: 10px 24px;
    border-top: 1px solid var(--bspkfw-bd-f3f4f6);
}

.sm-row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }

.sm-separator { border: none; border-top: 1px solid var(--bspkfw-bd-e5e7eb); margin: 16px 0; }

.sm-fields-header { font-size: 0.875rem; font-weight: 600; margin-bottom: 8px; color: var(--bspkfw-fg-374151); }

.sm-fields-table { table-layout: fixed; }
.sm-fields-table .sm-col-drag   { width: 32px; text-align: center; }
.sm-fields-table .sm-col-null   { width: 80px; text-align: center; }
.sm-fields-table .sm-col-vis    { width: 52px; text-align: center; }
.sm-fields-table .sm-col-remove { width: 36px; text-align: center; }
.sm-remove-btn {
    width: 26px;
    height: 26px;
    padding: 0;
    line-height: 1;
    font-size: 16px;
    color: var(--bspkfw-fg-ef4444);
    background: none;
    border: 1px solid transparent;
    border-radius: 4px;
    /* `.btn` is inline-flex with align-items:center but no horizontal
       centering — the × sits flush-left on hover, looking off. Override
       to centre both axes inside the 26×26 box. */
    justify-content: center;
}
.sm-remove-btn:hover { background: var(--bspkfw-bg-fee2e2); border-color: var(--bspkfw-bd-fca5a5); }
.sm-fields-table td { vertical-align: middle; padding: 5px 6px; }

.sm-field-row:has(input:disabled) { background: var(--bspkfw-bg-f9fafb); color: var(--bspkfw-fg-9ca3af); }
.sm-field-row:has(input:disabled) td { color: var(--bspkfw-fg-9ca3af); }
.sm-drag-handle { cursor: grab; color: var(--bspkfw-fg-9ca3af); font-size: 1.1rem; display: inline-block; padding: 0 4px; }
.sm-drag-row { cursor: default; }
.sm-dragging  { opacity: 0.4; }
.sm-drag-over { outline: 2px dashed var(--bspkfw-focus-ring); outline-offset: -2px; }
.sm-fk-type { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; padding: 2px 0; }
.sm-fk-badge  { background: var(--bspkfw-bg-ede9fe); color: var(--bspkfw-fg-7c3aed); border-radius: 4px; padding: 1px 7px; font-size: 12px; font-weight: 500; }
.sm-m2m-badge { background: var(--bspkfw-bg-d1fae5); color: var(--bspkfw-fg-065f46); border-radius: 4px; padding: 1px 7px; font-size: 12px; font-weight: 500; }
.sm-virtual-name { font-weight: 500; color: var(--bspkfw-fg-374151); padding: 6px 0; display: inline-block; }
.sm-links-note { font-size: 0.78rem; color: var(--bspkfw-fg-5f6774); margin: 6px 0 0; }

/* Communication message preview modal */
.comm-tabs { }
.comm-tab-nav { display: flex; gap: 2px; border-bottom: 1px solid var(--bspkfw-bd-e5e7eb); margin-bottom: 16px; }
.comm-tab-btn { background: none; border: none; border-bottom: 2px solid transparent; padding: 8px 16px; cursor: pointer; font-size: 14px; color: var(--bspkfw-fg-5f6774); margin-bottom: -1px; border-radius: 4px 4px 0 0; transition: color .15s; }
.comm-tab-btn:hover { color: var(--bspkfw-fg-111827); background: var(--bspkfw-bg-f9fafb); }
.comm-tab-btn.active { color: var(--bspkfw-fg-4f46e5); border-bottom-color: var(--bspkfw-bd-4f46e5); font-weight: 500; }
.comm-tab-panel { display: none; }
.comm-tab-panel.active { display: block; }
.cmm-meta { border: 1px solid var(--bspkfw-bd-e5e7eb); border-radius: 6px; overflow: hidden; margin-bottom: 12px; }
.cmm-meta-row { display: flex; border-bottom: 1px solid var(--bspkfw-bd-e5e7eb); }
.cmm-meta-row:last-child { border-bottom: none; }
.cmm-meta-label { width: 80px; padding: 7px 12px; font-size: 13px; font-weight: 600; color: var(--bspkfw-fg-5f6774); background: var(--bspkfw-bg-f9fafb); flex-shrink: 0; }
.cmm-meta-value { padding: 7px 12px; font-size: 13px; }
.cmm-email-body { border: 1px solid var(--bspkfw-bd-e5e7eb); border-radius: 6px; overflow: hidden; }
.cmm-iframe { width: 100%; min-height: 200px; height: 360px; border: none; display: block; background: var(--bspkfw-bg-fff); }
.cmm-slack-msg { background: var(--bspkfw-bg-f9fafb); border: 1px solid var(--bspkfw-bd-e5e7eb); border-radius: 6px; padding: 12px 16px; font-size: 14px; white-space: pre-wrap; word-break: break-word; }
.comm-sd-section { border-radius: 6px; overflow: hidden; margin-bottom: 8px; border: 1px solid var(--bspkfw-bd-e5e7eb); }
.comm-sd-section:last-child { margin-bottom: 0; }
.comm-sd-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; cursor: pointer; user-select: none; background: var(--bspkfw-bg-f9fafb); }
.comm-sd-header:hover { filter: brightness(0.97); }
.comm-sd-title { font-size: 13px; font-weight: 500; }
.comm-sd-chevron { font-size: 11px; color: var(--bspkfw-fg-9ca3af); transition: transform .15s; }
.comm-sd-section.open .comm-sd-chevron { transform: rotate(180deg); }
.comm-sd-body { padding: 10px 12px; }
.comm-sd-rows { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 13px; margin: 0; }
.comm-sd-row { display: contents; }
.comm-sd-rows dt { font-weight: 600; color: var(--bspkfw-fg-5f6774); }
.comm-sd-rows dd { margin: 0; }
.comm-sd-success { border-color: var(--bspkfw-bd-bbf7d0); }
.comm-sd-success .comm-sd-header { background: var(--bspkfw-bg-f0fdf4); }
.comm-sd-failed { border-color: var(--bspkfw-bd-fecaca); }
.comm-sd-failed .comm-sd-header { background: var(--bspkfw-bg-fef2f2); }
.comm-sd-sending { border-color: var(--bspkfw-accent-border); }
.comm-sd-sending .comm-sd-header { background: var(--bspkfw-accent-soft); }
.comm-sd-neutral .comm-sd-header { background: var(--bspkfw-bg-f9fafb); }

/* ============================================================
   Tag chips — styled by per-category `style` blob (inline). The
   chip class only sets layout / size; colour comes from inline
   style on each chip so each tag-category can theme its own.
   ============================================================ */
.bspkfw-tag-chip {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 999px;
    background: var(--bspkfw-bg-e5e7eb);
    color: var(--bspkfw-fg-374151);
    font-size: 12px;
    line-height: 1.5;
    white-space: nowrap;
    vertical-align: baseline;
}
.bspkfw-tag-chip + .bspkfw-tag-chip { margin-left: 4px; }

/* Manage-table cell: "n tags" trigger + popover. position:fixed on the
   popover so the table's overflow:hidden doesn't clip it (mirrors the
   existing row-actions-menu pattern). */
.bspkfw-tag-multi {
    background: transparent;
    border: 1px solid var(--bspkfw-bd-d1d5db);
    border-radius: 999px;
    padding: 1px 9px;
    font-size: 12px;
    color: var(--bspkfw-fg-374151);
    cursor: pointer;
}
.bspkfw-tag-multi:hover { background: var(--bspkfw-bg-f3f4f6); }
.bspkfw-tag-popover {
    position: fixed;
    background: var(--bspkfw-bg-fff);
    border: 1px solid var(--bspkfw-bd-e5e7eb);
    border-radius: 6px;
    box-shadow: 0 4px 12px var(--bspkfw-sh-rgba-0-0-0-12);
    padding: 8px 10px;
    z-index: 250;
    min-width: 160px;
    max-width: 360px;
    line-height: 1.9;
}
.bspkfw-tag-popover[hidden] { display: none; }

/* Hidden select options (Form\Select `hiddenOptions`): the native <option>
   carries `hidden disabled` (the placeholder pattern — absent from the open
   list, shown in the closed control when it's the stored value). Kselect's
   _carryAttrs copies the option's className onto its dropdown row, so this
   one rule removes the row from the Kselect dropdown / mobile sheet too —
   no vendored-widget change. */
.ks-option.bspkfw-option-hidden { display: none; }

/* Edit-modal picker: a Kselect <select multiple>. Each option is rendered by
   Kselect as ".ks-option-text > .bspkfw-tag-chip" (Kselect runs innerHTML on
   our chip-HTML option text, allowHtml: true). The chip already carries its
   own colour via the per-category inline style; we just nudge spacing here. */
.bspkfw-tag-picker-select { /* native select stays display:none — Kselect handles UI */ }
.tags-picker-container .ks-option-text .bspkfw-tag-chip { vertical-align: middle; }

/* Sticky "+ Add new tag" footer mounted inside the open Kselect dropdown.
   `position: sticky` + `bottom: 0` pins it to the viewport of the existing
   .ks-dropdown / .ks-mobile-options-wrap scroll container so it stays
   visible while the user scrolls the option list. No Kselect-file edits.
   The generic inline-FK-create footer (create-inline.js) shares the exact
   treatment — one design, two mounters. */
.bspkfw-ci-footer,
.bspkfw-tag-add-footer {
    position: sticky;
    bottom: 0;
    background: var(--ks-color-bg, var(--bspkfw-bg-fff));
    border-top: 1px solid var(--bspkfw-bd-e5e7eb);
    padding: 8px 10px;
    z-index: 1;
}
.bspkfw-ci-btn,
.bspkfw-tag-add-btn {
    width: 100%;
    padding: 6px 10px;
    background: transparent;
    border: 1px dashed var(--bspkfw-bd-c7d2fe);
    border-radius: 6px;
    color: var(--bspkfw-fg-4f46e5);
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
}
.bspkfw-ci-btn:hover,
.bspkfw-tag-add-btn:hover  { background: var(--bspkfw-bg-eef2ff); border-color: var(--bspkfw-bd-818cf8); }
.bspkfw-ci-btn:focus-visible,
.bspkfw-tag-add-btn:focus-visible {
    outline: 2px solid var(--bspkfw-bd-6366f1);
    outline-offset: 2px;
}
.bspkfw-tag-add-form {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 6px;
}
.bspkfw-tag-add-form .bspkfw-tag-add-cat,
.bspkfw-tag-add-form .bspkfw-tag-add-name {
    height: 32px;
    padding: 4px 8px;
    font-size: 13px;
}
.bspkfw-tag-add-form .bspkfw-tag-add-actions {
    grid-column: 1 / span 2;
    display: flex;
    justify-content: flex-end;
    gap: 6px;
}
.bspkfw-tag-add-form .bspkfw-tag-add-error {
    grid-column: 1 / span 2;
    margin: 0;
    font-size: 12px;
    padding: 6px 8px;
}
.bspkfw-tag-add-empty {
    color: var(--bspkfw-fg-5f6774);
    font-size: 12px;
    text-align: center;
    padding: 4px 0;
}

/* -----------------------------------------------------------------
   Import / Export wizard (import-export.js)
   Reuses .modal / .modal-box / .form-group / .data-table from the
   shared modal styles above; only the wizard-specific bits live here.
   ----------------------------------------------------------------- */
.bspkfw-iew-modal .modal-box {
    width: 720px;
    max-width: 95vw;
}
.bspkfw-iew-form  { display: flex; flex-direction: column; gap: 12px; }
.bspkfw-iew-help  { color: var(--bspkfw-fg-5f6774); font-size: 13px; margin: 6px 0 12px; }
.bspkfw-iew-template { margin: 12px 0; }
.bspkfw-iew-template-help { color: var(--bspkfw-fg-5f6774); font-size: 12px; }
.bspkfw-iew-error {
    color: var(--bspkfw-fg-b91c1c);
    background: var(--bspkfw-bg-fef2f2);
    border: 1px solid var(--bspkfw-bd-fecaca);
    padding: 8px 10px;
    border-radius: 4px;
    margin-top: 8px;
}
.bspkfw-iew-preview-wrap {
    overflow-x: auto;
    border: 1px solid var(--bspkfw-bd-e5e7eb);
    border-radius: 4px;
    margin-top: 8px;
}
.bspkfw-iew-preview { min-width: 100%; }
.bspkfw-iew-preview th, .bspkfw-iew-preview td {
    padding: 6px 8px;
    font-size: 12px;
    white-space: nowrap;
}
.bspkfw-iew-preview thead tr:first-child th {
    background: var(--bspkfw-bg-f3f4f6);
    font-weight: 600;
}
.bspkfw-iew-preview thead tr:last-child th {
    background: var(--bspkfw-bg-f9fafb);
    border-top: 1px solid var(--bspkfw-bd-e5e7eb);
}
.bspkfw-iew-preview thead tr:last-child .form-control {
    width: 100%;
    font-size: 12px;
    padding: 4px 6px;
}
.bspkfw-iew-summary dl {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 4px 16px;
    margin: 8px 0;
}
.bspkfw-iew-summary dt { font-weight: 600; color: var(--bspkfw-fg-374151); }
.bspkfw-iew-summary dd { margin: 0; color: var(--bspkfw-fg-1f2937); }
.bspkfw-iew-summary-final dt { color: var(--bspkfw-fg-065f46); }
.bspkfw-iew-ok { color: var(--bspkfw-fg-065f46); }
.bspkfw-iew-errors {
    background: var(--bspkfw-bg-fef2f2);
    border: 1px solid var(--bspkfw-bd-fecaca);
    border-radius: 4px;
    padding: 10px 12px;
    margin-top: 12px;
}
.bspkfw-iew-errors h3 { font-size: 14px; margin: 0 0 8px; color: var(--bspkfw-fg-991b1b); }
.bspkfw-iew-errors ul { margin: 0; padding-left: 18px; color: var(--bspkfw-fg-7f1d1d); font-size: 12px; }
/* Discard warnings (Validate step) — amber, never blocks Apply. */
.bspkfw-iew-warnings {
    background: var(--bspkfw-bg-fffbeb);
    border: 1px solid var(--bspkfw-bd-fde68a);
    border-radius: 4px;
    padding: 10px 12px;
    margin-top: 12px;
}
.bspkfw-iew-warnings h3 { font-size: 14px; margin: 0 0 8px; color: var(--bspkfw-fg-92400e); }
.bspkfw-iew-warnings ul { margin: 0; padding-left: 18px; color: var(--bspkfw-fg-78350f); font-size: 12px; }
.bspkfw-iew-fkpreview { margin-top: 12px; }
.bspkfw-iew-fkpreview h3 { font-size: 14px; margin: 0 0 8px; }
.bspkfw-iew-fkpreview .data-table th,
.bspkfw-iew-fkpreview .data-table td { font-size: 12px; padding: 4px 8px; }
.bspkfw-iew-fkpreview-bad { color: var(--bspkfw-fg-991b1b); font-style: italic; }

/* -----------------------------------------------------------------
   Theme picker (theme-switcher.js)
   Skeleton classes on .theme-picker-* — themes can override colours
   without touching layout.
   ----------------------------------------------------------------- */
.theme-picker .modal-box { width: 480px; max-width: 95vw; }
.theme-picker-list {
    list-style: none;
    margin: 0;
    padding: 0;
}
.theme-picker-item {
    display: grid;
    grid-template-columns: 24px 1fr;
    align-items: baseline;
    gap: 4px 12px;
    padding: 12px 14px;
    border: 1px solid var(--bspkfw-bd-e5e7eb);
    border-radius: 6px;
    margin-bottom: 8px;
    cursor: pointer;
    background: var(--bspkfw-bg-fff);
    transition: background 0.12s, border-color 0.12s;
}
.theme-picker-item:hover { background: var(--bspkfw-bg-f9fafb); border-color: var(--bspkfw-bd-d1d5db); }
.theme-picker-item--active { border-color: var(--bspkfw-bd-6366f1); background: var(--bspkfw-bg-eef2ff); }
.theme-picker-item--active:hover { background: var(--bspkfw-bg-e0e7ff); }
.theme-picker-item--applying { opacity: 0.5; pointer-events: none; }
.theme-picker-check {
    grid-column: 1 / 2;
    grid-row: 1 / 3;
    text-align: center;
    color: var(--bspkfw-fg-6366f1);
    font-weight: 700;
    font-size: 16px;
}
.theme-picker-name  { grid-column: 2 / 3; font-weight: 600; color: var(--bspkfw-fg-111827); }
.theme-picker-desc  { grid-column: 2 / 3; color: var(--bspkfw-fg-5f6774); font-size: 12px; }
.theme-picker-empty,
.theme-picker-error,
.theme-picker-loading {
    padding: 16px;
    text-align: center;
    color: var(--bspkfw-fg-5f6774);
    font-size: 13px;
}
.theme-picker-error { color: var(--bspkfw-fg-b91c1c); }

/* -----------------------------------------------------------------
   File-upload parser hint (edit-modal.js setParsingHint)
   Inline "Analyzing…" indicator inside a parser-enabled .file-upload
   container while POST /api/{table}/_parse_file is in flight. Themes
   re-skin via the same selectors.
   ----------------------------------------------------------------- */
.file-upload .bspkfw-parsing-hint {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin-top: 6px;
    color: var(--bspkfw-fg-5f6774);
    font-size: 12px;
    font-style: italic;
}
.bspkfw-parsing-spinner {
    width: 12px;
    height: 12px;
    border: 2px solid var(--bspkfw-bd-d1d5db);
    border-top-color: var(--bspkfw-bd-6366f1);
    border-radius: 50%;
    animation: bspkfw-parsing-spin 0.8s linear infinite;
}
@keyframes bspkfw-parsing-spin { to { transform: rotate(360deg); } }

/* --- Encrypted fields (FieldEncryption helper UI) --- */
.bspkfw-encrypted-field { position: relative; }
.bspkfw-enc-helper {
    margin-top: 0.4em;
    padding: 0.5em 0.7em;
    background: var(--bspkfw-bg-f7f8fa);
    border: 1px solid var(--bspkfw-bd-e3e6eb);
    border-radius: 4px;
    font-size: 0.9em;
    display: flex;
    flex-wrap: wrap;
    gap: 0.5em;
    align-items: center;
}
.bspkfw-enc-badge {
    display: inline-block;
    padding: 0.1em 0.5em;
    background: var(--bspkfw-bg-e3e6eb);
    color: var(--bspkfw-fg-555);
    border-radius: 3px;
    font-size: 0.85em;
}
.bspkfw-enc-warning {
    flex-basis: 100%;
    padding: 0.4em 0.6em;
    background: var(--bspkfw-bg-fff7e6);
    border: 1px solid var(--bspkfw-bd-ffd591);
    border-radius: 3px;
    color: var(--bspkfw-fg-8a5a00);
    display: flex;
    gap: 0.4em;
    align-items: flex-start;
}
.bspkfw-enc-warning-icon { flex-shrink: 0; }
.bspkfw-enc-warning-text { flex: 1; }
.bspkfw-enc-info-icon {
    background: none;
    border: none;
    color: var(--bspkfw-accent-text-hover);
    cursor: pointer;
    font-size: 1em;
    padding: 0 0.3em;
    flex-shrink: 0;
}
.bspkfw-enc-info-icon:hover { text-decoration: underline; }
.bspkfw-enc-password-row {
    flex-basis: 100%;
    display: flex;
    gap: 0.5em;
    align-items: center;
}
.bspkfw-enc-password-label { flex-shrink: 0; font-weight: 500; }
.bspkfw-enc-password { flex: 1; min-width: 8em; }
.bspkfw-enc-decrypt-btn { flex-shrink: 0; }
.bspkfw-enc-decrypted-hint {
    flex-basis: 100%;
    color: var(--bspkfw-fg-047857);
    font-size: 0.85em;
}
.bspkfw-enc-reveal-denied {
    flex-basis: 100%;
    color: var(--bspkfw-fg-888);
    font-size: 0.85em;
    font-style: italic;
}
.bspkfw-enc-locked .form-control { background: var(--bspkfw-bg-f0f0f0); color: var(--bspkfw-fg-888); font-style: italic; }

/* --- Async (server-callback) field validators --- */
/* Spinner glyph over the input's right edge while the validator
   request is in flight. Inputs can't host CSS pseudo-elements, so the
   spinner is a sibling <span> injected by the form JS into the
   .form-group (which carries position: relative) and positioned
   absolutely from the input's measured offset. CSS owns the visual +
   the animation; JS owns placement and lifecycle. */
.form-group.bspkfw-async-validating { position: relative; }
.form-group.bspkfw-async-validating > .form-control { padding-right: 2.4em; }
.bspkfw-async-spinner {
    position: absolute;
    width: 1em;
    height: 1em;
    box-sizing: border-box;
    border: 0.15em solid var(--bspkfw-bd-rgba-99-102-241-25);
    border-top-color: var(--bspkfw-bd-6366f1);
    border-radius: 50%;
    pointer-events: none;
    animation: bspkfw-async-spin 0.8s linear infinite;
}
@keyframes bspkfw-async-spin { to { transform: rotate(360deg); } }
/* --- asyncSuggester in-flight cue (edit-modal.js) --- */
/* Same sibling-spinner glyph as the validator above, hung off the TARGET
   field's .form-group while /_field_suggest is in flight. The extra
   padding keeps the glyph clear of the value; a .money-input badge sits
   on the left, so it is untouched. Class-driven so a project can restyle
   it in one place. */
.form-group.is-suggesting { position: relative; }
.form-group.is-suggesting .form-control { padding-right: 2.4em; }

/* ↻ "Work it out again" beside every suggester-backed input (edit-modal.js
   adds it; the label is manage.suggestAgain). Shares the spinner's slot at
   the input's right edge and yields to it while a request is out. */
.form-group.has-suggester { position: relative; }
.form-group.has-suggester .form-control { padding-right: 2.4em; }
.bspkfw-suggest-again {
    position: absolute;
    right: 0.35em;
    width: 1.7em;
    height: 1.7em;
    padding: 0;
    border: 0;
    border-radius: 4px;
    background: transparent;
    color: var(--bspkfw-field-muted, var(--bspkfw-fg-5f6774));
    font: inherit;
    line-height: 1;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
}
.bspkfw-suggest-again:hover,
.bspkfw-suggest-again:focus-visible { color: inherit; background: var(--bspkfw-bg-rgba-0-0-0-06); }
.form-group.is-suggesting .bspkfw-suggest-again { visibility: hidden; }

/* Optional fading green ✓ on validated success (showValidatedTick). */
.form-group.bspkfw-async-ok > .form-control {
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path d='M3 8 L7 12 L13 4' fill='none' stroke='%23047857' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
    background-repeat: no-repeat;
    background-position: right 0.6em center;
    background-size: 1em 1em;
    padding-right: 2.4em;
    transition: background-image 0.2s ease-out;
}

/* ============================================================================
 * CSP Phase 2 — classes extracted from inline style= attributes so the policy
 * can run a strict `style-src 'self'` (no 'unsafe-inline'). Each utility maps
 * to exactly the property it replaced, so the sweep is visually a no-op.
 * Add new utilities here rather than reaching for an inline style.
 * ==========================================================================*/

/* Text */
.u-muted  { color: var(--bspkfw-fg-5f6774); }
.u-warn   { color: var(--bspkfw-fg-b45309); }
.u-fs-13  { font-size: 13px; }
.u-fs-14  { font-size: 14px; }

/* Margin — only the values actually used across framework pages. */
.u-m-0     { margin: 0; }
.u-my-16   { margin: 16px 0; }
.u-mt-8    { margin-top: 8px; }
.u-mt-12   { margin-top: 12px; }
.u-mt-16   { margin-top: 16px; }
.u-mt-24   { margin-top: 24px; }
.u-mb-0    { margin-bottom: 0; }
.u-mb-8    { margin-bottom: 8px; }
.u-mb-12   { margin-bottom: 12px; }
.u-mb-14   { margin-bottom: 14px; }
.u-mb-16   { margin-bottom: 16px; }
.u-mb-20   { margin-bottom: 20px; }
.u-mb-24   { margin-bottom: 24px; }
.u-mr-8    { margin-right: 8px; }
.u-mr-auto { margin-right: auto; }

/* Padding / layout */
.u-p-16        { padding: 16px; }
.u-flex        { display: flex; }
.u-flex-col    { display: flex; flex-direction: column; }
.u-flex-center { display: flex; justify-content: center; }
.u-gap-8       { gap: 8px; }
.u-hidden      { display: none; }
.u-grow        { flex: 1; }
.u-pointer     { cursor: pointer; }

/* Compound / semantic */
.bspkfw-divider     { margin-top: 32px; border-top: 1px solid var(--bspkfw-bd-e5e7eb); padding-top: 24px; }
.bspkfw-divider-sm  { margin-top: 16px; border-top: 1px solid var(--bspkfw-bd-e5e7eb); padding-top: 16px; }
.auth-lead        { font-size: 14px; color: var(--bspkfw-fg-5f6774); margin-bottom: 20px; }

/* CSS-hidden honeypot (anti-spam). Was an inline style on .bspkfw-honeypot.
 * Form.php ALSO emits this rule inline with every anti-spam form
 * (Form::HONEYPOT_STYLE) so custom-chrome pages that skip base.css still
 * hide the field — keep the two copies in sync. This one stays so
 * framework-chrome pages don't depend on the inline emission. */
.bspkfw-honeypot { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }

/* The raw multi-select behind the tag picker stays hidden; Kselect renders
 * its own widget over it. Was an inline `style="display:none"`. */
select[data-bspkfw-tag-picker] { display: none; }

/* MFA setup — QR frame + manual-entry secret block. */
.mfa-qr-frame { width: 240px; height: 240px; padding: 8px; background: var(--bspkfw-bg-fff); border: 1px solid var(--bspkfw-bd-e5e7eb); border-radius: 8px; }
.mfa-secret   { display: block; margin-top: 8px; padding: 10px; background: var(--bspkfw-bg-f9fafb); border: 1px solid var(--bspkfw-bd-e5e7eb); border-radius: 6px; font-family: monospace; font-size: 15px; letter-spacing: 2px; word-break: break-all; }

/* MFA backup codes — the printable code list. Was an inline style on the
 * existing .bspkfw-backup-codes element. */
.bspkfw-backup-codes { font-family: ui-monospace, SFMono-Regular, monospace; font-size: 16px; line-height: 1.8; background: var(--bspkfw-bg-f9fafb); border: 1px solid var(--bspkfw-bd-e5e7eb); border-radius: 8px; padding: 16px 16px 16px 36px; }

/* Schema modal — narrow dialog box. */
.modal-box--narrow { max-width: 380px; }

/* ============================================================
   BspkfwChart (bspkfw-chart.js) — SVG time-series charts.
   Every colour routes through a --chart-* custom property so
   themes restyle charts by overriding the variables on
   .bspkfw-chart — no JS changes. Series cycle through the 6-slot
   palette via .bspkfw-chart-series-N classes.
   ============================================================ */
.bspkfw-chart {
    --chart-series-1: var(--bspkfw-accent-text);
    --chart-series-2: var(--bspkfw-fg-16a34a);
    --chart-series-3: var(--bspkfw-fg-d97706);
    --chart-series-4: var(--bspkfw-fg-dc2626);
    --chart-series-5: var(--bspkfw-fg-7c3aed);
    --chart-series-6: var(--bspkfw-fg-0d9488);
    --chart-grid: var(--bspkfw-bd-e5e7eb);
    --chart-axis-text: var(--bspkfw-fg-5f6774);
    --chart-tooltip-bg: var(--bspkfw-bg-rgba-17-24-39-92);
    --chart-tooltip-color: var(--bspkfw-fg-f9fafb);
    position: relative;
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 260px;   /* default — consumers override on their own container */
}
.bspkfw-chart svg { display: block; flex: 1 1 auto; min-height: 0; width: 100%; }

/* Per-series colour binding — the JS assigns these classes cyclically. */
.bspkfw-chart-series-1 { --chart-series-color: var(--chart-series-1); }
.bspkfw-chart-series-2 { --chart-series-color: var(--chart-series-2); }
.bspkfw-chart-series-3 { --chart-series-color: var(--chart-series-3); }
.bspkfw-chart-series-4 { --chart-series-color: var(--chart-series-4); }
.bspkfw-chart-series-5 { --chart-series-color: var(--chart-series-5); }
.bspkfw-chart-series-6 { --chart-series-color: var(--chart-series-6); }

.bspkfw-chart .bspkfw-chart-line {
    fill: none;
    stroke: var(--chart-series-color);
    stroke-width: 2;
    stroke-linejoin: round;
    stroke-linecap: round;
}
.bspkfw-chart .bspkfw-chart-area { fill: var(--chart-series-color); opacity: 0.12; stroke: none; }
.bspkfw-chart .bspkfw-chart-bar  { fill: var(--chart-series-color); }
.bspkfw-chart .bspkfw-chart-dot  { fill: var(--chart-series-color); }
.bspkfw-chart .bspkfw-chart-grid-line { stroke: var(--chart-grid); stroke-width: 1; }
.bspkfw-chart text { fill: var(--chart-axis-text); font-size: 11px; }
.bspkfw-chart .bspkfw-chart-empty { font-size: 13px; }

.bspkfw-chart-tooltip {
    position: absolute;
    z-index: 5;
    pointer-events: none;
    background: var(--chart-tooltip-bg);
    color: var(--chart-tooltip-color);
    padding: 6px 9px;
    border-radius: 6px;
    font-size: 12px;
    line-height: 1.5;
    white-space: nowrap;
    box-shadow: 0 2px 8px var(--bspkfw-sh-rgba-0-0-0-25);
}
.bspkfw-chart-tooltip-time { font-weight: 600; }
.bspkfw-chart-tooltip-row  { display: flex; align-items: center; gap: 6px; }

.bspkfw-chart-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 14px;
    padding: 2px 0 6px;
    font-size: 12px;
    color: var(--chart-axis-text);
}
.bspkfw-chart-legend-item { display: inline-flex; align-items: center; gap: 6px; }
.bspkfw-chart-swatch {
    flex: 0 0 auto;
    width: 10px;
    height: 10px;
    border-radius: 2px;
    background: var(--chart-series-color);
}

/* ── Development history page (/dev-history) ──────────────────────────
   Bespoke presentation for the super-admin git-history browser
   (pages/dev_history.php + js/pages/dev_history.js). Wider than the
   680px profile column — commit summaries are prose. */
.devh-page { max-width: 900px; margin: 0 auto; }

.devh-toolbar {
    display: flex;
    gap: 10px;
    align-items: center;
    margin-bottom: var(--bspkfw-space-row, 16px);
}
.devh-search { flex: 1 1 auto; }
.devh-filter { flex: 0 0 auto; width: auto; }

.devh-list { display: flex; flex-direction: column; gap: 12px; }

.devh-row {
    background: var(--bspkfw-bg-fff);
    border: 1px solid var(--bspkfw-bd-e5e7eb);
    border-radius: 8px;
    padding: var(--bspkfw-space-row, 16px);
}
.devh-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 4px 12px;
    margin-bottom: 8px;
}
.devh-badge--framework { background: var(--bspkfw-accent-soft-2); color: var(--bspkfw-accent-text-strong); }
.devh-badge--project   { background: var(--bspkfw-bg-dcfce7); color: var(--bspkfw-fg-166534); }
.devh-hash   { font-size: 12px; color: var(--bspkfw-fg-5f6774); }
.devh-author { font-size: 13px; color: var(--bspkfw-fg-374151); font-weight: 600; }
.devh-when   { font-size: 13px; color: var(--bspkfw-fg-5f6774); margin-left: auto; }

.devh-text {
    margin: 0;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    font-size: 14px;
    line-height: 1.55;
}
.devh-text--summary { font-style: italic; color: var(--bspkfw-fg-374151); }
.devh-toggle { margin-top: 8px; }
.devh-full {
    margin: 10px 0 0;
    padding: 12px;
    background: var(--bspkfw-bg-f9fafb);
    border: 1px solid var(--bspkfw-bd-e5e7eb);
    border-radius: 6px;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    font-size: 13px;
    line-height: 1.5;
    max-height: 70vh;
    overflow-y: auto;
}

.devh-pager {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 14px;
    margin-top: var(--bspkfw-space-section-gap, 20px);
}
.devh-pager-label { font-size: 13px; color: var(--bspkfw-fg-5f6774); }

.devh-unavailable code { user-select: all; }

/* ── Logs page (/logs) ───────────────────────────────────────────────
   The named activity logs (Logger::channel()) read in the app:
   pages/logs.php + js/pages/logs.js. Monospace lines, newest first;
   the page runs at the wide measure (pageWidth 'wide'). */
.logs-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 10px;
    margin-bottom: var(--bspkfw-space-row, 16px);
}
.logs-field { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--bspkfw-fg-5f6774); }
.logs-field--grow { flex: 1 1 240px; }
.logs-field select.form-control { width: auto; }

.logs-summary { font-size: 13px; color: var(--bspkfw-fg-5f6774); margin: 0 0 10px; }

.logs-list {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12.5px;
    line-height: 1.5;
    background: var(--bspkfw-bg-f9fafb);
    border: 1px solid var(--bspkfw-bd-e5e7eb);
    border-radius: 8px;
    padding: 8px 0;
    overflow-x: auto;
}
.logs-list:empty { display: none; }
.logs-line {
    display: grid;
    grid-template-columns: 19ch 5ch 6ch minmax(0, 1fr);
    column-gap: 12px;
    padding: 2px 12px;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
.logs-line:nth-child(even) { background: var(--bspkfw-bg-rgba-0-0-0-02); }
.logs-ts    { color: var(--bspkfw-fg-5f6774); }
.logs-level { font-weight: 700; }
.logs-proc  { color: var(--bspkfw-fg-5f6774); }
.logs-line--warn  .logs-level { color: var(--bspkfw-fg-b45309); }
.logs-line--error .logs-level { color: var(--bspkfw-fg-b91c1c); }
.logs-line--error { background: var(--bspkfw-bg-fef2f2); }
.logs-line--raw   .logs-level { color: var(--bspkfw-fg-5f6774); }
.logs-ctx { color: var(--bspkfw-fg-374151); }

.logs-pager {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 14px;
    margin-top: var(--bspkfw-space-section-gap, 20px);
}
.logs-pager-label { font-size: 13px; color: var(--bspkfw-fg-5f6774); }

/* ============================================================
   BspkfwScanner (bspkfw-scanner.js) — camera barcode / QR
   reader modal. Standard .modal chrome; these rules style only
   the camera viewport inside it.
   ============================================================ */
.bspkfw-scanner-modal .modal-box { width: 440px; }

.bspkfw-scanner-viewport {
    position: relative;
    background: var(--bspkfw-bg-000);
    border-radius: 8px;
    overflow: hidden;
    aspect-ratio: 4 / 3;
}
.bspkfw-scanner-viewport video {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* Aiming frame — flashes green (.is-hit on the viewport) when a scan
   is accepted, the "that one counted" cue. */
.bspkfw-scanner-reticle {
    position: absolute;
    inset: 14% 10%;
    border: 2px solid var(--bspkfw-bd-rgba-255-255-255-8);
    border-radius: 10px;
    pointer-events: none;
    /* dark halo either side of the white line so the frame reads on
       bright scenes (a white shelf label) and dark ones alike */
    box-shadow: 0 0 0 1px var(--bspkfw-sh-rgba-0-0-0-4), inset 0 0 0 1px var(--bspkfw-sh-rgba-0-0-0-4);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.bspkfw-scanner-viewport.is-hit .bspkfw-scanner-reticle {
    border-color: var(--bspkfw-bd-22c55e);
    box-shadow: 0 0 0 4px var(--bspkfw-sh-rgba-34-197-94-35);
}

/* Torch toggle — capability-gated by the JS (hidden unless the active
   camera track reports torch support). */
.bspkfw-scanner-torch {
    position: absolute;
    right: 10px;
    bottom: 10px;
    background: var(--bspkfw-bg-rgba-17-24-39-65);
    border-color: var(--bspkfw-bd-rgba-255-255-255-5);
    color: var(--bspkfw-fg-f9fafb);
}
.bspkfw-scanner-torch.is-active {
    background: var(--bspkfw-bg-f9fafb);
    color: var(--bspkfw-fg-111827);
}

.bspkfw-scanner-status {
    margin: 12px 0 0;
    min-height: 1.4em;
    text-align: center;
    font-size: 14px;
    color: var(--bspkfw-fg-5f6774);
}
.bspkfw-scanner-error { margin: 12px 0 0; }

/* ============================================================
   Manage grid — card layout on narrow viewports (manage.js)
   ============================================================
   Below 640px manage.js renders each row as a .manage-card (label / value
   pairs, full-width action buttons, select checkbox in the corner) instead
   of the horizontally-scrolling table. The breakpoint lives in manage.js
   (CARD_BREAKPOINT) — keep this media query equal to it. Desktop is
   untouched: none of these rules apply above the breakpoint. */
@media (max-width: 640px) {
    .manage-cards-bar {
        display: flex;
        align-items: center;
        gap: 10px;
        margin-bottom: 10px;
    }
    .manage-cards-select .bspkfw-select-header { display: inline-flex; align-items: center; gap: 6px; }
    .manage-cards-select input[type="checkbox"] { width: 22px; height: 22px; }
    .manage-card-sort { flex: 1; min-width: 0; min-height: 44px; }

    .manage-cards { display: flex; flex-direction: column; gap: 10px; }
    .manage-card {
        position: relative;
        background: var(--bspkfw-bg-fff);
        border-radius: 8px;
        box-shadow: 0 1px 3px var(--bspkfw-sh-rgba-0-0-0-08);
        padding: 12px 14px;
    }
    .manage-card.bspkfw-row-selected { background: var(--bspkfw-bg-f5f8ff); box-shadow: 0 0 0 2px var(--bspkfw-accent-border); }
    .manage-card-select { position: absolute; top: 10px; right: 12px; margin: 0; line-height: 0; }
    .manage-card-select input[type="checkbox"] { width: 22px; height: 22px; }
    .manage-card:has(> .manage-card-select) .manage-card-fields { padding-right: 34px; }

    .manage-card-fields { margin: 0; display: grid; gap: 6px; }
    .manage-card-field {
        display: grid;
        grid-template-columns: minmax(6em, 38%) minmax(0, 1fr);
        gap: 8px;
        align-items: baseline;
    }
    .manage-card-field dt {
        font-size: 12px;
        font-weight: 600;
        text-transform: uppercase;
        letter-spacing: 0.04em;
        color: var(--bspkfw-fg-5f6774);
    }
    .manage-card-field dd { margin: 0; color: var(--bspkfw-fg-374151); min-width: 0; overflow-wrap: anywhere; }
    .manage-card-field dd img { max-width: 100%; height: auto; }

    .manage-card-actions {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
        gap: 8px;
        margin-top: 12px;
    }
    .manage-card-actions .btn { width: 100%; min-height: 44px; margin: 0; text-align: center; }
    .manage-cards-empty { margin: 0; }

    /* "With selected" becomes a sticky footer while anything is selected —
       the page-header trigger is off-screen once the user has scrolled the
       cards; the menu opens UPWARD from it. Nothing changes while the
       trigger is disabled (no selection). */
    body.manage-cards-active .manage-page { padding-bottom: 72px; }
    body.manage-cards-active .bspkfw-with-selected:has(> .bspkfw-with-selected-trigger:not(:disabled)) {
        position: fixed;
        left: 12px;
        right: 12px;
        bottom: 12px;
        z-index: 60;
    }
    body.manage-cards-active .bspkfw-with-selected:has(> .bspkfw-with-selected-trigger:not(:disabled)) > .bspkfw-with-selected-trigger {
        width: 100%;
        min-height: 48px;
        box-shadow: 0 6px 20px var(--bspkfw-sh-rgba-0-0-0-18);
    }
    body.manage-cards-active .bspkfw-with-selected:has(> .bspkfw-with-selected-trigger:not(:disabled)) > .bspkfw-with-selected-menu {
        top: auto;
        bottom: calc(100% + 6px);
    }
}


/* ---------------------------------------------------------------------
   Translation mode (translate-mode.js) + /translation-review
   --------------------------------------------------------------------- */
/* Wrapper the scanner puts around a matched text node that shares its
   parent with other content; inline so layout never moves. */
.tm-wrap { display: inline; }
/* Same-text-several-keys chooser: a framework modal, classes only. */
.tm-chooser-list { display: flex; flex-direction: column; gap: 8px; }
.tm-chooser-item { text-align: left; display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
.tm-chooser-item code { font-size: 0.95em; }
.tm-chooser-item small { color: var(--bspkfw-fg-5f6774); }
/* Review page: one card per proposal — source + context on the left, the
   editable proposal, note and decision on the right; stacks on a phone.
   A seven-column table put the note box off screen. */
.tr-card { border: 1px solid var(--bspkfw-bd-e5e7eb); border-radius: 8px; padding: 14px 16px; margin-bottom: 14px; background: var(--bspkfw-bg-fff); }
.tr-card-head { display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; margin-bottom: 10px; }
.tr-card-head code { font-size: 0.95em; }
.tr-card-lang { color: var(--bspkfw-fg-5f6774); }
.tr-card-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px 24px; }
@media (max-width: 800px) { .tr-card-grid { grid-template-columns: minmax(0, 1fr); } }
.tr-card h4 { margin: 0 0 4px; font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.02em; color: var(--bspkfw-fg-5f6774); }
.tr-card h4 + * { margin-bottom: 12px; }
.tr-text-cell { white-space: pre-wrap; word-break: break-word; }
.tr-card label { display: block; font-weight: 600; margin-bottom: 4px; }
.tr-card textarea.tr-text { width: 100%; margin-bottom: 12px; }
.tr-card .tr-note { width: 100%; }
.tr-card .form-help { display: block; margin: 4px 0 12px; }
.tr-stale { color: var(--bspkfw-fg-b45309); font-size: 0.9em; }
.tr-meta { font-size: 0.9em; color: var(--bspkfw-fg-5f6774); }
.tr-meta > div { margin-top: 4px; }
.tr-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.tr-chips { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 12px; }

/* ===========================================================================
   User manual — the header `?` button, the in-page modal (manual.js), the
   callouts the renderer emits, and the /manual page (sidebar + prose).
   Scoped `.manual-*` throughout; nothing here touches another component.
   =========================================================================== */
.manual-help {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: 1px solid var(--bspkfw-header-border);
    border-radius: 999px;
    width: 32px;
    height: 32px;
    color: var(--bspkfw-header-fg);
    font-family: inherit;
    font-weight: 700;
    cursor: pointer;
    line-height: 1;
    padding: 0;
    flex-shrink: 0;
}
.manual-help:hover { background: var(--bspkfw-header-hover-bg); color: var(--bspkfw-header-fg-hover); }
.manual-help-icon { font-size: 16px; line-height: 1; }
/* `Project::manual()['iconStyle' => 'plain']` — no ring, sized like the
   notifications bell beside it (an emoji glyph such as 📖 reads as an icon on
   its own). Re-style either variant from a project sheet via these classes. */
.manual-help--plain {
    border-color: transparent;
    width: auto;
    min-width: 32px;
    padding: 6px 8px;
    color: var(--bspkfw-header-muted);
}
.manual-help--plain:hover { background: transparent; color: var(--bspkfw-header-fg); }
.manual-help--plain .manual-help-icon { font-size: 18px; }

/* The modal — may open OVER an EditModal, so the nested z token. */
.manual-modal { z-index: var(--bspkfw-z-modal-nested, 110); }
.manual-modal[hidden] { display: none; }
.manual-modal-box { max-height: calc(100vh - 48px); }
.manual-modal-header { gap: 10px; }
.manual-modal-header .manual-modal-title { flex: 1; min-width: 0; margin: 0; font-size: 1.05rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.manual-back[hidden] { display: none; }
.manual-modal-search { padding: 10px var(--bspkfw-space-card, 24px); border-bottom: 1px solid var(--bspkfw-bd-f3f4f6); flex-shrink: 0; }
.manual-modal-search .form-control { width: 100%; }
.manual-modal-body { padding: 16px var(--bspkfw-space-card, 24px); line-height: 1.55; }
.manual-modal-body:focus { outline: none; }
.manual-modal-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.manual-modal-crumbs { font-size: 0.85em; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (max-width: 640px) {
    /* Full-screen on a phone — reading help should not happen in a letterbox. */
    .manual-modal .manual-modal-box { width: 100vw; max-width: 100vw; height: 100dvh; max-height: 100dvh; border-radius: 0; }
    .manual-modal-crumbs { display: none; }
}

/* Prose — base.css zeroes <p> margins, so give the manual's running text its own rhythm. */
.manual-modal-body p, .manual-body p { margin: 0 0 0.8em; }
.manual-modal-body ul, .manual-modal-body ol, .manual-body ul, .manual-body ol { margin: 0 0 0.8em; padding-left: 1.4em; }
.manual-modal-body li, .manual-body li { margin-bottom: 0.3em; }
.manual-modal-body pre, .manual-body pre { margin: 0 0 0.8em; padding: 10px 12px; background: var(--bspkfw-bg-f3f4f6); border-radius: 6px; overflow-x: auto; }
.manual-modal-body code, .manual-body code { font-size: 0.92em; }
.manual-modal-body .manual-heading, .manual-body .manual-heading { margin: 1.4em 0 0.5em; line-height: 1.25; }
.manual-modal-body .manual-section:first-child > .manual-heading { margin-top: 0; }
/* The modal's title IS the section's heading — don't print it twice. */
.manual-modal-body > .manual-section:first-child > .manual-heading { display: none; }
.manual-body h2.manual-heading { font-size: 1.5rem; padding-top: 0.2em; }
.manual-body h3.manual-heading { font-size: 1.2rem; }
.manual-body h4.manual-heading { font-size: 1.05rem; }
.manual-shot { display: block; max-width: 100%; height: auto; margin: 12px 0; border: 1px solid var(--bspkfw-bd-e5e7eb); border-radius: 6px; }
.manual-callout { margin: 12px 0; padding: 10px 14px; border-left: 4px solid var(--bspkfw-accent-bright); background: var(--bspkfw-accent-soft); border-radius: 4px; }
.manual-callout--otherwise { border-left-color: var(--bspkfw-bd-9ca3af); background: var(--bspkfw-bg-f3f4f6); }
.manual-callout-label { font-size: 0.8em; font-weight: 600; text-transform: uppercase; letter-spacing: 0.02em; color: var(--bspkfw-accent-text-strong); margin-bottom: 4px; }
.manual-callout--otherwise .manual-callout-label { color: var(--bspkfw-fg-374151); }
.manual-callout > :last-child { margin-bottom: 0; }
.manual-notice { font-size: 0.9em; color: var(--bspkfw-fg-5f6774); border: 1px dashed var(--bspkfw-bd-d1d5db); border-radius: 4px; padding: 6px 10px; margin: 0 0 12px; }
.manual-notice--unbound { color: var(--bspkfw-fg-92400e); border-color: var(--bspkfw-bd-fcd34d); background: var(--bspkfw-bg-fffbeb); }
.manual-audience { vertical-align: middle; }

/* Contents (modal + page) */
.manual-toc ul { list-style: none; margin: 0; padding: 0; }
.manual-toc > ul > li { margin-bottom: 10px; }
.manual-toc a { text-decoration: none; color: inherit; display: block; padding: 2px 0; border-left: 2px solid transparent; padding-left: 8px; min-height: 24px; box-sizing: border-box; }   /* 24px: WCAG 2.5.8 target size, stacked links */
.manual-toc a:hover { color: var(--bspkfw-accent-text); }
.manual-toc .manual-toc-topic > a { font-weight: 600; }
.manual-toc .manual-toc-l3 a { padding-left: 20px; font-size: 0.92em; }
.manual-toc a.is-current { border-left-color: var(--bspkfw-accent-line); color: var(--bspkfw-accent-text); }
.manual-toc a.is-current-topic { color: var(--bspkfw-accent-text); }
.manual-toc li.is-filtered-out { display: none; }
.manual-toc--modal > li { margin-bottom: 14px; }

/* Search results */
.manual-results { list-style: none; margin: 0; padding: 0; }
.manual-result { padding: 10px 0; border-bottom: 1px solid var(--bspkfw-bd-f3f4f6); }
.manual-result-title { font-weight: 600; text-decoration: none; margin-right: 8px; }
.manual-result-topic { font-size: 0.85em; }
.manual-result-snippet { margin: 4px 0 0; font-size: 0.92em; color: var(--bspkfw-fg-374151); }
.manual-result-snippet mark { background: var(--bspkfw-bg-fef08a); padding: 0 1px; }

/* /manual page */
.manual-page { display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: 32px; align-items: start; }
.manual-contents { position: sticky; top: 16px; max-height: calc(100vh - 32px); overflow-y: auto; padding-right: 8px; }
.manual-contents-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
.manual-contents-title { margin: 0; font-size: 0.9em; text-transform: uppercase; letter-spacing: 0.04em; color: var(--bspkfw-fg-5f6774); }
.manual-toc-filter { display: block; margin-bottom: 10px; }
.manual-toc-filter .form-control { width: 100%; }
.manual-toc-nomatch { font-size: 0.9em; }
.manual-toc-nomatch[hidden] { display: none; }
.manual-body { max-width: 76ch; line-height: 1.6; }
.manual-body .manual-topic { margin-bottom: 48px; padding-bottom: 24px; border-bottom: 1px solid var(--bspkfw-bd-e5e7eb); }
.manual-body .manual-heading { scroll-margin-top: 16px; }
@media (max-width: 900px) {
    .manual-page { grid-template-columns: minmax(0, 1fr); }
    .manual-contents { position: static; max-height: none; }
}
.manual-page--print .manual-contents { position: static; max-height: none; }
@media print {
    .manual-help, .manual-contents-head .btn, .manual-toc-filter { display: none !important; }
    .manual-page { display: block; }
    .manual-body { max-width: none; }
    .manual-callout { break-inside: avoid; }
    .manual-section { break-inside: avoid-page; }
}

/* =============================================================================
   Document layouts + designer (core/Documents, /document-designer, the
   layouts panel). A three-column workspace — palette, page, inspector — that
   stacks on a narrow screen. The page is a white sheet at the paper's aspect
   ratio (set by the script through the CSSOM); its elements are positioned in
   percentages and their type is sized from --bspkfw-pt, the screen size of
   one printed point, so the canvas previews the PDF at any width.
   Theme hooks: --bspkfw-designer-accent / -accent-soft / -accent-ink /
   -line / -muted / -surface (defaults follow the field tokens).
   ============================================================================= */

.bspkfw-designer, .bspkfw-layouts {
    --bspkfw-designer-accent:      var(--bspkfw-field-accent, var(--bspkfw-accent-text));
    --bspkfw-designer-accent-soft: var(--bspkfw-field-accent-soft, var(--bspkfw-accent-soft-2));
    --bspkfw-designer-accent-ink:  var(--bspkfw-fg-fff);
    --bspkfw-designer-line:        var(--bspkfw-field-border, var(--bspkfw-bd-d1d5db));
    --bspkfw-designer-muted:       var(--bspkfw-field-muted, var(--bspkfw-fg-5f6774));
    --bspkfw-designer-surface:     var(--bspkfw-field-surface, var(--bspkfw-bg-fff));
    --bspkfw-designer-guide:       var(--bspkfw-bd-e11d48);
}

/* --- Layouts panel ---------------------------------------------------------- */
.bspkfw-layouts__title { margin: 0 0 0.25rem; font-size: 1.1rem; }
.bspkfw-layouts__intro { margin: 0 0 0.75rem; color: var(--bspkfw-designer-muted); }
.bspkfw-layouts__list { list-style: none; margin: 0 0 0.75rem; padding: 0; }
.bspkfw-layouts__item {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.35rem 0.5rem;
    padding: 0.5rem 0;
    border-bottom: 1px solid var(--bspkfw-designer-line);
}
.bspkfw-layouts__name { font-weight: 600; }
.bspkfw-layouts__paper { color: var(--bspkfw-designer-muted); }
.bspkfw-layouts__actions { margin-left: auto; display: inline-flex; flex-wrap: wrap; gap: 0.25rem; }
.bspkfw-layouts__archived { margin-top: 1rem; }
.bspkfw-layouts__fonts { margin-top: 1.75rem; padding-top: 1rem; border-top: 1px solid var(--bspkfw-designer-line); }
.bspkfw-layouts__item--image { align-items: center; }
/* Pictures dragged over the Images section (a drop zone for whoever may add them). */
.bspkfw-layouts__images[data-bspkfw-images-drop] { border-radius: 8px; transition: background-color 0.1s; }
.bspkfw-layouts__images.is-dragover {
    outline: 2px dashed var(--bspkfw-accent);
    outline-offset: 6px;
    background: var(--bspkfw-accent-soft);
}
.bspkfw-layouts__images.is-dragover .bspkfw-layouts__drop-hint { font-weight: 600; color: inherit; }
.bspkfw-layouts__item.is-uploading { opacity: 0.7; }
.bspkfw-layouts__thumb {
    flex: 0 0 auto;
    width: 40px;
    height: 40px;
    object-fit: contain;
    background: repeating-linear-gradient(45deg, var(--bspkfw-bg-f1f5f9), var(--bspkfw-bg-f1f5f9) 4px, var(--bspkfw-bg-e2e8f0) 4px, var(--bspkfw-bg-e2e8f0) 8px);
    border: 1px solid var(--bspkfw-designer-line);
}
.bspkfw-layouts__archived summary { cursor: pointer; color: var(--bspkfw-designer-muted); }
.bspkfw-layouts__badge {
    display: inline-block;
    padding: 0 0.45rem;
    border-radius: 999px;
    font-size: 0.75rem;
    line-height: 1.5;
    background: var(--bspkfw-designer-accent-soft);
    color: var(--bspkfw-designer-accent);
}
.bspkfw-layouts .btn-link, .bspkfw-designer .btn-link {
    background: none; border-color: transparent; color: var(--bspkfw-designer-accent); padding-left: 0.4rem; padding-right: 0.4rem;
}

/* --- Designer chrome -------------------------------------------------------- */
.bspkfw-designer__head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: space-between;
    gap: 0.5rem 1rem;
    margin-bottom: 1rem;
}
.bspkfw-designer__head h1 { margin: 0; }
.bspkfw-designer__head p { margin: 0.25rem 0 0; }
.bspkfw-designer__tools { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.bspkfw-designer__muted { color: var(--bspkfw-designer-muted); font-size: 0.9rem; }
.bspkfw-designer__link { background: none; border-color: transparent; color: var(--bspkfw-designer-accent); }
.bspkfw-designer__panel {
    border: 1px solid var(--bspkfw-designer-line);
    border-radius: 8px;
    padding: 0.9rem 1rem;
    background: var(--bspkfw-designer-surface);
}
.bspkfw-designer__panel-title { margin: 0 0 0.35rem; font-size: 1rem; }

.bspkfw-designer__body {
    display: grid;
    grid-template-columns: minmax(10rem, 13rem) minmax(0, 1fr) minmax(13rem, 17rem);
    gap: 1rem;
    align-items: start;
}
@media (max-width: 1000px) {
    .bspkfw-designer__body { grid-template-columns: minmax(0, 1fr); }
}

.bspkfw-designer__sub { font-size: 0.9rem; margin: 1rem 0 0.4rem; }
.bspkfw-designer__chips { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.bspkfw-designer__chip {
    padding: 0.25rem 0.6rem;
    border: 1px dashed var(--bspkfw-designer-accent);
    border-radius: 999px;
    background: var(--bspkfw-designer-accent-soft);
    color: var(--bspkfw-designer-accent);
    font: inherit;
    font-size: 0.85rem;
    cursor: grab;
    touch-action: none;
}
.bspkfw-designer__chip:active { cursor: grabbing; }
.bspkfw-designer__ghost {
    position: fixed;
    z-index: 10000;
    transform: translate(-50%, -50%);
    padding: 0.25rem 0.6rem;
    border-radius: 999px;
    background: var(--bspkfw-designer-accent);
    color: var(--bspkfw-designer-accent-ink);
    font-size: 0.85rem;
    pointer-events: none;
    box-shadow: 0 4px 14px var(--bspkfw-sh-rgba-0-0-0-2);
}

.bspkfw-designer__stage { min-width: 0; }
.bspkfw-designer__page {
    position: relative;
    width: 100%;
    margin: 0 auto;
    background: var(--bspkfw-bg-fff) center / 100% 100% no-repeat;
    box-shadow: 0 1px 3px var(--bspkfw-sh-rgba-0-0-0-15), 0 8px 24px var(--bspkfw-sh-rgba-0-0-0-08);
    color: var(--bspkfw-fg-111);
    overflow: hidden;
    touch-action: none;
    user-select: none;
    -webkit-user-select: none;
}
.bspkfw-designer__page.is-drop-target,
.bspkfw-designer__page.is-file-drop { outline: 3px dashed var(--bspkfw-designer-accent); outline-offset: 4px; }
.bspkfw-designer__page.has-pdf-background::before {
    content: "PDF";
    position: absolute;
    inset: auto 0.5rem 0.5rem auto;
    font-size: 0.7rem;
    color: var(--bspkfw-fg-9ca3af);
}
.bspkfw-designer__hint { margin-top: 0.5rem; font-size: 0.85rem; }

.bspkfw-designer__guides { position: absolute; inset: 0; pointer-events: none; z-index: 5; }
.bspkfw-designer__guide { position: absolute; background: var(--bspkfw-designer-guide); }
.bspkfw-designer__guide--x { top: 0; bottom: 0; width: 1px; }
.bspkfw-designer__guide--y { left: 0; right: 0; height: 1px; }

/* --- Canvas elements -------------------------------------------------------- */
.bspkfw-de {
    position: absolute;
    box-sizing: border-box;
    outline: 1px dashed var(--bspkfw-bd-rgba-100-116-139-45);
    cursor: move;
    /* The body clips the preview; the box itself lets its badges sit outside it. */
    overflow: visible;
}
.bspkfw-de:hover { outline-color: var(--bspkfw-designer-accent); }
.bspkfw-de:focus { outline: 2px solid var(--bspkfw-designer-accent); }
.bspkfw-de.is-selected { outline: 2px solid var(--bspkfw-designer-accent); overflow: visible; z-index: 4; }
.bspkfw-de.is-anchored { outline-style: dotted; }
/* The badges sit just ABOVE the box, so they never cover a value printed
   flush to its top corner ("$1,000.00" read "$1,000." under the ↧). */
.bspkfw-de.is-anchored::after, .bspkfw-de.is-repeated::before {
    position: absolute;
    bottom: 100%;
    right: 0;
    z-index: 1;
    padding: 0 0.3em;
    font-size: 0.65rem;
    line-height: 1.4;
    background: var(--bspkfw-designer-accent-soft);
    color: var(--bspkfw-designer-accent);
    pointer-events: none;
}
.bspkfw-de.is-anchored::after { content: "\21A7"; }
.bspkfw-de.is-repeated::before { content: "\21BB"; right: auto; left: 0; }
.bspkfw-de__body { width: 100%; height: 100%; overflow: hidden; }
.bspkfw-de__handle {
    position: absolute;
    display: none;
    width: 10px;
    height: 10px;
    background: var(--bspkfw-designer-accent);
    border: 1px solid var(--bspkfw-bd-fff);
}
.bspkfw-de.is-selected .bspkfw-de__handle { display: block; }
.bspkfw-designer__page.is-multi .bspkfw-de__handle { display: none; }   /* resize is one element's */
/* The rubber band drawn on empty page to select several elements. */
.bspkfw-designer__marquee {
    position: absolute;
    border: 1px dashed var(--bspkfw-designer-accent);
    background: var(--bspkfw-designer-accent-soft);
    opacity: 0.5;
    pointer-events: none;
    z-index: 5;
}
.bspkfw-de__handle--e  { right: -6px; top: calc(50% - 5px); cursor: ew-resize; }
.bspkfw-de__handle--s  { bottom: -6px; left: calc(50% - 5px); cursor: ns-resize; }
.bspkfw-de__handle--se { right: -6px; bottom: -6px; cursor: nwse-resize; }

/* The sheet is set in the layout's default font by the script (the PDF core
   stacks, or an uploaded face registered through the FontFace API); an element
   that chose its own carries it inline. No synthesised bold: a face with no
   bold file prints REGULAR in the PDF, and the canvas says the same. */
.bspkfw-designer__page { font-family: Helvetica, Arial, sans-serif; }
.bspkfw-de { font-synthesis: none; }
/* Pdf::textAt()'s pitch: 7 mm a line at 11 pt = 1.8038 em of the canvas's
   point at any size (document-designer.js textLineHeight() for a spacing). */
.bspkfw-de__text { line-height: 1.8038; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bspkfw-de__text.is-wrapping { white-space: pre-wrap; }
.bspkfw-de__text.is-muted, .bspkfw-de__label { color: var(--bspkfw-fg-6e6e6e); }
.bspkfw-de__img { display: block; max-width: 100%; max-height: 100%; object-fit: contain; }
.bspkfw-de__img.is-align-C { margin: 0 auto; }
.bspkfw-de__img.is-align-R { margin-left: auto; }
.bspkfw-de__placeholder, .bspkfw-de__qr {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
    aspect-ratio: 1;
    max-width: 100%;
    background: repeating-linear-gradient(45deg, var(--bspkfw-bg-f1f5f9), var(--bspkfw-bg-f1f5f9) 4px, var(--bspkfw-bg-e2e8f0) 4px, var(--bspkfw-bg-e2e8f0) 8px);
    color: var(--bspkfw-fg-64748b);
    font-size: 0.7rem;
}
.bspkfw-de__placeholder { aspect-ratio: auto; width: 100%; }
/* An Image box: empty = a drop / click target; uploading = the file, dimmed, under a progress badge. */
.bspkfw-de__placeholder--image { box-sizing: border-box; padding: 0.25em; text-align: center; border: 1px dashed var(--bspkfw-designer-accent); }
.bspkfw-de.is-file-over { outline: 3px solid var(--bspkfw-designer-accent); }
.bspkfw-de__upload { position: relative; height: 100%; }
.bspkfw-de__upload .bspkfw-de__img { opacity: 0.6; }
.bspkfw-de__upload-badge {
    position: absolute;
    left: 0;
    bottom: 0;
    padding: 0 0.3em;
    font-size: 0.65rem;
    line-height: 1.4;
    background: var(--bspkfw-designer-accent);
    color: var(--bspkfw-designer-accent-ink);
    pointer-events: none;
}
.bspkfw-de__body.is-align-C .bspkfw-de__qr { margin: 0 auto; }
.bspkfw-de__body.is-align-R .bspkfw-de__qr { margin-left: auto; }
.bspkfw-de__rule { border-top: 1px solid var(--bspkfw-bd-969696); }
.bspkfw-de--rule .bspkfw-de__body { display: flex; align-items: center; }
.bspkfw-de--rule .bspkfw-de__rule { width: 100%; }

.bspkfw-de__details { display: grid; gap: 0 0.5em; line-height: 1.5; }
.bspkfw-de__pair { display: flex; gap: 0.4em; min-width: 0; white-space: nowrap; overflow: hidden; }
.bspkfw-de__details.is-above .bspkfw-de__pair { flex-direction: column; gap: 0; }
.bspkfw-de__label { flex: 0 0 auto; max-width: 45%; overflow: hidden; text-overflow: ellipsis; font-size: 0.94em; }
.bspkfw-de__details.is-above .bspkfw-de__label { max-width: none; }
.bspkfw-de__value { overflow: hidden; text-overflow: ellipsis; }

.bspkfw-de__table { display: grid; line-height: 1.45; align-content: start; }
.bspkfw-de__th, .bspkfw-de__td { padding: 0 0.3em; border: 1px solid var(--bspkfw-bd-969696); min-width: 0; overflow: hidden; }
.bspkfw-de__th { font-weight: 700; color: var(--bspkfw-designer-accent); }
.bspkfw-de__td.is-nowrap { white-space: nowrap; text-overflow: ellipsis; }
.bspkfw-de__th.is-align-R, .bspkfw-de__td.is-align-R { text-align: right; }
.bspkfw-de__th.is-align-C, .bspkfw-de__td.is-align-C { text-align: center; }

.bspkfw-de__ruled { display: flex; flex-direction: column; height: 100%; }
.bspkfw-de__over { flex: 1; display: flex; align-items: flex-end; min-height: 0; }
.bspkfw-de__over.is-align-C { justify-content: center; }
.bspkfw-de__over.is-align-R { justify-content: flex-end; }
.bspkfw-de__over .bspkfw-de__img { max-height: 100%; }

/* --- Inspector + field lists ------------------------------------------------ */
.bspkfw-designer__selected { font-weight: 600; margin: 0 0 0.5rem; }
.bspkfw-designer__arrange { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; margin-bottom: 0.75rem; }
.bspkfw-designer__label { flex-basis: 100%; font-size: 0.85rem; font-weight: 600; margin: 0.5rem 0 0.25rem; }
.bspkfw-designer__arrange .btn.is-active {
    background: var(--bspkfw-designer-accent);
    border-color: var(--bspkfw-designer-accent);
    color: var(--bspkfw-designer-accent-ink);
}
.bspkfw-designer__remove { color: var(--bspkfw-field-invalid, var(--bspkfw-fg-f87171)); background: none; border-color: transparent; margin-bottom: 0.75rem; }
.bspkfw-designer__image-preview { display: flex; align-items: center; gap: 0.5rem; min-height: 2rem; margin-bottom: 0.5rem; }
.bspkfw-designer__image-preview img {
    width: 64px;
    height: 64px;
    object-fit: contain;
    background: repeating-linear-gradient(45deg, var(--bspkfw-bg-f1f5f9), var(--bspkfw-bg-f1f5f9) 4px, var(--bspkfw-bg-e2e8f0) 4px, var(--bspkfw-bg-e2e8f0) 8px);
    border: 1px solid var(--bspkfw-designer-line);
}

.bspkfw-designer__fields {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
    gap: 1rem;
    margin-top: 1rem;
}
.bspkfw-designer__rows { list-style: none; margin: 0; padding: 0; }
.bspkfw-designer__anchors { list-style: none; margin: 0 0 8px; padding: 0; max-height: 12em; overflow-y: auto; }
/* A kind's options under a list's rows (the framework field rows, spaced as the inspector's). */
.bspkfw-designer__list-options { margin-top: 0.75rem; padding-top: 0.5rem; border-top: 1px solid var(--bspkfw-designer-line); }
.bspkfw-designer__list-options .form-group { margin: 0 0 0.5rem; }
.bspkfw-designer__row {
    display: grid;
    grid-template-columns: auto minmax(8rem, 1fr) minmax(8rem, 1.2fr);
    align-items: center;
    gap: 0.5rem;
    padding: 0.25rem 0;
    border-bottom: 1px solid var(--bspkfw-designer-line);
    background: var(--bspkfw-designer-surface);
}
.bspkfw-designer__rows--columns .bspkfw-designer__row { grid-template-columns: auto minmax(5rem, 1fr) minmax(5rem, 1.2fr); }
.bspkfw-designer__row.is-dragging { opacity: 0.6; box-shadow: 0 4px 12px var(--bspkfw-sh-rgba-0-0-0-15); }
.bspkfw-designer__row.is-off .bspkfw-designer__labelrow,
.bspkfw-designer__row.is-off input[type="text"] { opacity: 0.45; }
/* "Placed on its own" — the particular has a detail element of its own, so the
   tick does nothing to the block; the note sits under the tick + label. */
.bspkfw-designer__placed { grid-column: 2 / -1; font-size: 0.75rem; color: var(--bspkfw-designer-muted); }
.bspkfw-designer__placed[hidden] { display: none; }
.bspkfw-designer__row .form-group { margin: 0; }
.bspkfw-designer__tick { display: flex; align-items: center; gap: 0.4rem; margin: 0; min-width: 0; overflow: hidden; }
.bspkfw-designer__labelrow > label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
}
.bspkfw-designer__handle {
    cursor: grab;
    padding: 0 0.35rem;
    color: var(--bspkfw-designer-muted);
    font-size: 1.1rem;
    touch-action: none;
    user-select: none;
}
.bspkfw-designer__handle:focus { outline: 2px solid var(--bspkfw-designer-accent); border-radius: 3px; }
.bspkfw-designer__save { margin-top: 1rem; }

/* ============================================================
   Board drag — BspkfwDesignerUtil.bindBoardDrag() (/js/designer-util.js)
   A card carried between the columns of a board (kanban). The ghost is
   a clone on <body>, sized and moved by the script through the CSSOM;
   the source card stays put, dimmed; the column under the pointer lights
   up. Rules are one class deep so a project's own card / column styles
   win where they say more.
   ============================================================ */
.bspkfw-board { -webkit-touch-callout: none; }
.bspkfw-board-dragging,
.bspkfw-board-dragging * { cursor: grabbing !important; user-select: none !important; -webkit-user-select: none !important; }
body > .bspkfw-board-drag-ghost {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 10000;
    margin: 0;
    box-sizing: border-box;
    pointer-events: none;
    opacity: 0.95;
    rotate: 1.5deg;
    box-shadow: 0 10px 28px var(--bspkfw-sh-rgba-15-23-42-18), 0 2px 6px var(--bspkfw-sh-rgba-0-0-0-15);
    will-change: transform;
}
.is-board-dragging .is-dragging { opacity: 0.4; }
.is-board-dragging .is-drop-target {
    outline: 2px dashed var(--bspkfw-accent);
    outline-offset: calc(var(--bspkfw-space-row) / -4);
    background-color: var(--bspkfw-accent-soft);
}
/* Opted-in ordering: a column's ghost is cut short and fades, so it never
   covers the board; the insertion bar (a card's position, or the gap a
   column will take) is fixed on <body>, so the page's lists never reflow. */
body > .bspkfw-board-drag-ghost.is-column {
    overflow: hidden;
    -webkit-mask-image: linear-gradient(to bottom, currentColor 70%, transparent);
    mask-image: linear-gradient(to bottom, currentColor 70%, transparent);
}
body > .bspkfw-board-drop-indicator {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 9999;
    height: 3px;
    margin-top: -1.5px;
    border-radius: 2px;
    background: var(--bspkfw-accent);
    box-shadow: 0 0 0 2px var(--bspkfw-accent-soft);
    pointer-events: none;
}
body > .bspkfw-board-drop-indicator.is-vertical { width: 3px; margin: 0 0 0 -1.5px; }
.bspkfw-board .is-drop-pending { opacity: 0.6; cursor: progress; }
@media (prefers-reduced-motion: reduce) { body > .bspkfw-board-drag-ghost { rotate: none; } }

/* ============================================================
   PDF viewer modal — window.BspkfwPdfViewer (/js/bspkfw-pdf-viewer.js)
   Reuses .modal / .modal-box / .modal-header / .modal-close; this block
   is the viewer's own chrome. Pages are <canvas>es sized by the script
   through the CSSOM (no style= attributes).
   ============================================================ */
.bspkfw-pdfv .modal-box {
    width: min(96vw, 1100px);
    height: min(94vh, 1400px);
    max-height: calc(100vh - 24px);
}
.bspkfw-pdfv__header { gap: 12px; }
.bspkfw-pdfv__title {
    font-size: 16px;
    font-weight: 600;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.bspkfw-pdfv__tools {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-shrink: 0;
}
.bspkfw-pdfv__counter {
    font-size: 12px;
    color: var(--bspkfw-fg-5f6774);
    min-width: 7em;
    text-align: center;
    font-variant-numeric: tabular-nums;
}
.bspkfw-pdfv__counter:empty { display: none; }
.bspkfw-pdfv__body {
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
    background: var(--bspkfw-bg-e5e7eb);
    padding: 16px;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
}
.bspkfw-pdfv__body:focus { outline: none; }
.bspkfw-pdfv__body:focus-visible { outline: 2px solid var(--bspkfw-focus-ring); outline-offset: -2px; }
.bspkfw-pdfv__status,
.bspkfw-pdfv__error {
    min-height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    gap: 8px;
    color: var(--bspkfw-fg-374151);
}
.bspkfw-pdfv__status[hidden] { display: none; }   /* display:flex above would beat the UA's [hidden] */
.bspkfw-pdfv__status .bspkfw-spinner { margin-bottom: 8px; }
.bspkfw-pdfv__status-label,
.bspkfw-pdfv__error-title { font-size: 15px; font-weight: 600; margin: 0; }
.bspkfw-pdfv__status-hint,
.bspkfw-pdfv__error-message { font-size: 13px; color: var(--bspkfw-fg-5f6774); margin: 0; max-width: 36em; }
.bspkfw-pdfv__error .btn { margin-top: 8px; }
.bspkfw-pdfv__pages {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
}
.bspkfw-pdfv__pages[hidden] { display: none; }
.bspkfw-pdfv__page {
    position: relative;
    flex-shrink: 0;
    background: var(--bspkfw-bg-fff);
    box-shadow: 0 1px 3px var(--bspkfw-sh-rgba-0-0-0-18), 0 6px 18px var(--bspkfw-sh-rgba-0-0-0-10);
}
.bspkfw-pdfv__page canvas { display: block; }

@media (max-width: 640px) {
    /* A phone: the viewer takes the screen. The title drops under the tools
       rather than squeezing them; zoom stays reachable with a thumb. */
    .bspkfw-pdfv .modal-box {
        width: 100vw;
        height: 100dvh;
        max-width: none;
        max-height: none;
        border-radius: 0;
    }
    .bspkfw-pdfv__header { flex-wrap: wrap; padding: 10px 12px; }
    .bspkfw-pdfv__title { order: 2; flex: 1 1 100%; font-size: 14px; }
    .bspkfw-pdfv__tools { order: 1; margin-left: auto; }
    .bspkfw-pdfv__body { padding: 8px; }
}

@media (prefers-reduced-motion: no-preference) {
    .bspkfw-pdfv__body { scroll-behavior: smooth; }
}

/* ── System health › Concerning logs (/system-health/logs) ─────────────── */
.hl-page .page-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.hl-sink { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px; padding: 8px 12px; margin: 0 0 12px; border: 1px solid var(--bspkfw-bd-e5e7eb); border-radius: 6px; font-size: 13px; }
.hl-sink--off { border-color: var(--bspkfw-bd-dc2626); background: var(--bspkfw-bg-fef2f2); }
.hl-sink-on  { color: var(--bspkfw-fg-166534); }
.hl-sink-off { color: var(--bspkfw-fg-991b1b); }
.hl-sink-fact { color: var(--bspkfw-fg-5f6774); }
.hl-probe-result { flex-basis: 100%; margin: 4px 0 0; font-size: 13px; }
.hl-probe-result--ok  { color: var(--bspkfw-fg-166534); }
.hl-probe-result--bad { color: var(--bspkfw-fg-991b1b); }
.hl-filters { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px 14px; margin: 0 0 10px; }
.hl-field { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--bspkfw-fg-5f6774); }
.hl-field select.form-control { width: auto; }
.hl-tabs { display: flex; gap: 4px; margin: 0 0 8px; border-bottom: 1px solid var(--bspkfw-bd-e5e7eb); }
.hl-tab { padding: 6px 12px; border-bottom: 2px solid transparent; color: var(--bspkfw-fg-5f6774); text-decoration: none; }
.hl-tab--active { color: inherit; border-bottom-color: var(--bspkfw-accent-line); font-weight: 600; }
.hl-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 12px; }
.hl-chip { display: inline-flex; align-items: center; gap: 4px; padding: 2px 10px; border: 1px solid var(--bspkfw-bd-e5e7eb); border-radius: 999px; font-size: 12px; color: inherit; text-decoration: none; }
.hl-chip--active { border-color: var(--bspkfw-accent-line); background: var(--bspkfw-accent-soft-2); }
.hl-chip-n { color: var(--bspkfw-fg-5f6774); }
.hl-showing { font-size: 13px; color: var(--bspkfw-fg-5f6774); margin: 0 0 8px; }
.hl-num { text-align: right; white-space: nowrap; }
.hl-sample { max-width: 48ch; overflow-wrap: anywhere; }
.hl-muted { color: var(--bspkfw-fg-5f6774); }
.hl-actions { white-space: nowrap; }
.hl-actions .btn + .btn { margin-left: 4px; }
.hl-status { font-size: 12px; font-weight: 600; }
.hl-status--open     { color: var(--bspkfw-fg-991b1b); }
.hl-status--reopened { color: var(--bspkfw-fg-b45309); }
.hl-status--acked    { color: var(--bspkfw-fg-166534); }
.hl-h2 { font-size: 16px; margin: 0 0 8px; }
.hl-row { border: 1px solid var(--bspkfw-bd-e5e7eb); border-radius: 6px; padding: 6px 10px; margin: 0 0 6px; }
.hl-row--error { border-left: 3px solid var(--bspkfw-bd-dc2626); }
.hl-row--warn  { border-left: 3px solid var(--bspkfw-bd-ca8a04); }
.hl-row summary { cursor: pointer; overflow-wrap: anywhere; }
.hl-msg { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; }
.hl-meta { display: grid; grid-template-columns: max-content 1fr; gap: 4px 12px; margin: 8px 0 0; font-size: 13px; }
.hl-meta > div { display: contents; }
.hl-meta dt { color: var(--bspkfw-fg-5f6774); }
.hl-meta dd { margin: 0; overflow-wrap: anywhere; }
.hl-context pre { margin: 0; padding: 6px 8px; background: var(--bspkfw-bg-rgba-0-0-0-04); border-radius: 4px; font-size: 12px; max-height: 320px; overflow: auto; }
.hl-sort { display: inline-flex; align-items: center; color: inherit; text-decoration: none; }
.hl-sort:hover { text-decoration: underline; }
table.is-col-sized .hl-sample { max-width: none; }
.hl-user { white-space: nowrap; }
.hl-row-user { font-size: 12px; color: var(--bspkfw-fg-4b5563); }
.hl-bulk { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 8px; }
.modal-box.hl-rows-modal { width: min(1100px, calc(100vw - 32px)); max-width: none; }
@media (max-width: 480px) {
    .hl-meta { grid-template-columns: 1fr; }
    .hl-actions { white-space: normal; }
}

/* ── Snapshots & exports (/data-archives) ───────────────────────────────── */
.da-actions { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }
.da-upload-btn { position: relative; cursor: pointer; }
.da-upload-progress { font-size: 13px; color: var(--bspkfw-fg-5f6774); margin: 0 0 8px; }
.da-table .da-note { max-width: 32ch; overflow-wrap: anywhere; }
.da-actions-cell { white-space: nowrap; }
.da-actions-cell .btn + .btn { margin-left: 4px; }
/* The recovery code of a wrapped download: shown once, read aloud or typed back, so large and spaced. */
.da-recovery-code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 18px; letter-spacing: 0.06em; overflow-wrap: anywhere; padding: 12px; border: 1px dashed currentColor; border-radius: 6px; text-align: center; user-select: all; }
/* Print: the code's sheet alone, never the page behind it. */
.da-print-sheet { display: none; }
@media print {
    body.da-printing > :not(.da-print-sheet) { display: none !important; }
    body.da-printing > .da-print-sheet { display: block; }
}
.da-muted { color: var(--bspkfw-fg-5f6774); font-size: 12px; }
.da-state { font-size: 12px; font-weight: 600; }
.da-state--busy  { color: var(--bspkfw-accent-text-hover); }
.da-state--ready { color: var(--bspkfw-fg-166534); }
.da-state--failed { color: var(--bspkfw-fg-991b1b); }
.da-error { color: var(--bspkfw-fg-991b1b); font-size: 12px; margin: 4px 0 0; padding-left: 16px; overflow-wrap: anywhere; }
.da-restore { font-size: 12px; margin-top: 4px; }
.da-restore--done { color: var(--bspkfw-fg-166534); }
.da-restore--failed { color: var(--bspkfw-fg-991b1b); }
.da-restore--queued, .da-restore--running { color: var(--bspkfw-accent-text-hover); }
.da-list { margin: 6px 0 0; padding-left: 18px; }
.da-notes { margin: 8px 0; }
.da-confirm { display: block; margin: 12px 0 8px; }
.da-confirm > span { display: block; margin-bottom: 4px; }
.modal-box.da-restore-box { width: min(900px, calc(100vw - 32px)); max-width: none; }
.da-restore-box [data-da-modal-body] { max-height: 75vh; overflow: auto; }

/* ── System health dashboard (/system-health) ──────────────────────────── */
.health-page .page-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.health-actions { display: flex; gap: 6px; flex-wrap: wrap; }
.health-intro { color: var(--bspkfw-fg-5f6774); font-size: 14px; margin: 0 0 14px; }
.health-concerns { padding: 8px 12px; margin: 0 0 14px; border: 1px solid var(--bspkfw-bd-e5e7eb); border-left-width: 4px; border-radius: 6px; font-size: 14px; }
.health-concerns a { color: inherit; }
.health-concerns--ok      { border-left-color: var(--bspkfw-bd-16a34a); }
.health-concerns--alert   { border-left-color: var(--bspkfw-bd-dc2626); }
.health-concerns--unknown { border-left-color: var(--bspkfw-bd-9ca3af); border-style: dashed; }
.health-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; margin: 0 0 16px; }
.health-card { display: flex; flex-direction: column; gap: 4px; text-align: left; text-decoration: none; color: inherit; font: inherit; background: var(--bspkfw-card-bg, var(--bspkfw-bg-fff)); cursor: default; }
a.health-card, button.health-card { cursor: pointer; }
a.health-card:hover, button.health-card:hover, a.health-card:focus-visible, button.health-card:focus-visible { box-shadow: 0 0 0 2px var(--bspkfw-focus-glow-strong); outline: none; }
.health-card .stat-card-value { font-size: 22px; }
.health-card-sub { font-size: 12px; color: var(--bspkfw-fg-5f6774); overflow-wrap: anywhere; }
.health-card-tone { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; font-weight: 600; }
.health-card--ok      { border-left-color: var(--bspkfw-bd-16a34a); } .health-card--ok .health-card-tone      { color: var(--bspkfw-fg-166534); }
.health-card--warn    { border-left-color: var(--bspkfw-bd-ca8a04); } .health-card--warn .health-card-tone    { color: var(--bspkfw-fg-854d0e); }
.health-card--alert   { border-left-color: var(--bspkfw-bd-dc2626); } .health-card--alert .health-card-tone   { color: var(--bspkfw-fg-991b1b); }
.health-card--muted   { border-left-color: var(--bspkfw-bd-9ca3af); } .health-card--muted .health-card-tone   { color: var(--bspkfw-fg-5f6774); }
.health-card--unknown { border-left-color: var(--bspkfw-bd-9ca3af); border-style: dashed; } .health-card--unknown .health-card-tone { color: var(--bspkfw-fg-5f6774); }
.health-card--unknown .stat-card-value::after { content: ' ?'; color: var(--bspkfw-fg-5f6774); }
.health-card[aria-busy="true"] .stat-card-value { opacity: 0.6; }
.health-asof { color: var(--bspkfw-fg-5f6774); }
.health-muted { color: var(--bspkfw-fg-5f6774); }
.health-kv { display: grid; grid-template-columns: max-content 1fr; gap: 4px 12px; margin: 0 0 12px; font-size: 13px; }
.health-kv > div { display: contents; }
.health-kv dt { color: var(--bspkfw-fg-5f6774); }
.health-kv dd { margin: 0; }
.health-detail-body { max-height: 70vh; overflow: auto; }
.health-detail-table { font-size: 13px; }
.health-detail-heading { font-size: 14px; margin: 16px 0 6px; }
@media (max-width: 390px) {
    .health-grid { grid-template-columns: 1fr; }
    .health-kv { grid-template-columns: 1fr; }
}

/* Stored file the user marked for removal in the edit modal: struck through,
   with Undo; it goes when the record is saved (never before). */
.file-upload-item--removing {
    opacity: 0.7;
}
.file-upload-item--removing .file-upload-name {
    text-decoration: line-through;
    color: var(--bspkfw-fg-5f6774);
}
.file-upload-item--removing .file-upload-name:hover {
    text-decoration: line-through underline;
}
.file-upload-removing-tag {
    flex-shrink: 0;
    font-size: 12px;
    color: var(--bspkfw-fg-991b1b);
    background: var(--bspkfw-bg-fee2e2);
    border: 1px solid var(--bspkfw-bd-fecaca);
    border-radius: 4px;
    padding: 1px 6px;
}
.file-upload-remove.file-upload-undo {
    font-size: 12px;
    color: var(--bspkfw-accent-text-strong);
    text-decoration: underline;
}
.file-upload-remove.file-upload-undo:hover { color: var(--bspkfw-accent-text-deep); }

/* ── Bug reports (CLAUDE/BUG-REPORTS.md) ─────────────────────────────── */
/* Header button — dev/test installs by default; same frame as .manual-help. */
.bug-report-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: 1px solid var(--bspkfw-header-border);
    border-radius: 999px;
    width: 32px;
    height: 32px;
    color: var(--bspkfw-header-fg);
    font-family: inherit;
    cursor: pointer;
    line-height: 1;
    padding: 0;
    flex-shrink: 0;
}
.bug-report-btn:hover { background: var(--bspkfw-header-hover-bg); color: var(--bspkfw-header-fg-hover); }
.bug-report-btn-icon { font-size: 16px; line-height: 1; }
.bug-report-btn.is-submitting .bug-report-btn-icon { visibility: hidden; }

.bug-report-page { max-width: 720px; }

/* The report window's own rules (modal, screenshots, details) are in
   /css/bug-report.css — its own file so a bespoke page shell that skips
   base.css can link it (BugReporting::shellHeadHtml()). The triage page's
   `.bug-detail pre` shares the details rule there. */

.bug-severity-mark--blocking { color: var(--bspkfw-fg-b91c1c); }
.bug-severity-mark--high { color: var(--bspkfw-fg-b45309); }

/* Triage page /bug-reports/{guid}. */
.bug-detail-grid {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    gap: 24px;
    align-items: start;
}
@media (max-width: 900px) { .bug-detail-grid { grid-template-columns: minmax(0, 1fr); } }
.bug-detail-meta { color: var(--bspkfw-field-muted, var(--bspkfw-fg-5f6774)); margin: 0 0 16px; }
.bug-detail-text { white-space: pre-wrap; margin: 0 0 16px; }
.bug-detail h2 { font-size: 1.05rem; margin: 20px 0 8px; }
.bug-detail h2:first-child { margin-top: 0; }
.bug-detail details { margin: 0 0 10px; }
.bug-detail details > summary { cursor: pointer; font-weight: 600; }
.bug-detail-shots { display: flex; flex-wrap: wrap; gap: 10px; margin: 0 0 16px; }
.bug-detail-shots button {
    padding: 0;
    border: 1px solid var(--bspkfw-field-border, var(--bspkfw-bd-d1d5db));
    border-radius: 4px;
    background: var(--bspkfw-field-surface, var(--bspkfw-bg-f9fafb));
    cursor: zoom-in;
}
.bug-detail-shots img { display: block; width: 220px; height: 150px; object-fit: contain; }
.bug-detail-list { margin: 0; padding-left: 18px; font-size: 13px; }
.bug-detail-list li { margin-bottom: 6px; word-break: break-word; }
.bug-detail-list code { font-size: 12px; }
.bug-detail-nav { display: flex; gap: 8px; }
.bug-detail-prompt-actions { display: flex; justify-content: flex-end; margin-bottom: 6px; }
.bug-detail .bug-detail-prompt { max-height: 420px; }

/* A report's status as a badge, and the conversation (BugReportThread) —
   shared by the triage page and the reporter's /my-bug-reports. */
.bug-status {
    display: inline-block;
    padding: 1px 8px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 600;
    line-height: 1.6;
    white-space: nowrap;
    background: var(--bspkfw-sunken);
    color: var(--bspkfw-text-2);
}
.bug-status--new, .bug-status--triaged, .bug-status--in_progress { background: var(--bspkfw-accent-soft); color: var(--bspkfw-accent-text); }
.bug-status--reopened { background: var(--bspkfw-warn-soft); color: var(--bspkfw-warn); }
.bug-status--fixed, .bug-status--tested { background: var(--bspkfw-success-soft); color: var(--bspkfw-success); }
.bug-thread { list-style: none; margin: 0 0 16px; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.bug-thread-entry {
    border: 1px solid var(--bspkfw-border);
    border-left-width: 3px;
    border-radius: 6px;
    padding: 8px 12px;
    background: var(--bspkfw-surface);
}
.bug-thread-entry--triager { border-left-color: var(--bspkfw-accent); }
.bug-thread-entry--reporter { border-left-color: var(--bspkfw-border-strong); background: var(--bspkfw-surface-2); }
.bug-thread-meta { font-size: 12px; color: var(--bspkfw-muted); margin-bottom: 4px; }
.bug-thread-body { overflow-wrap: anywhere; }
.bug-thread-status, .bug-thread-note { font-size: 12px; margin-top: 6px; }
.bug-thread-status { color: var(--bspkfw-text-2); }
.bug-reply-warning {
    background: var(--bspkfw-warn-soft);
    color: var(--bspkfw-warn);
    border-radius: 6px;
    padding: 8px 12px;
    font-size: 13px;
    margin: 0 0 12px;
}

/* The reporter's own reports, /my-bug-reports (pages/my-bug-reports.php). */
.bug-mine-intro { color: var(--bspkfw-muted); margin: 0 0 16px; }
.bug-mine-title { font-weight: 600; }
.bug-mine-tested {
    border: 1px solid var(--bspkfw-border);
    border-radius: 6px;
    background: var(--bspkfw-success-soft);
    padding: 12px 16px;
    margin: 0 0 16px;
}
.bug-mine-tested p { margin: 0 0 8px; }
.bug-mine-tested .bug-mine-hint { font-size: 13px; color: var(--bspkfw-muted); margin: 8px 0 0; }
.bug-detail-kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 12px; margin: 0; font-size: 13px; }
.bug-detail-kv dt { color: var(--bspkfw-field-muted, var(--bspkfw-fg-5f6774)); }
.bug-detail-kv dd { margin: 0; word-break: break-word; }

/* ============================================================
   Rich-text content — what HtmlInput's editor (kHtmlEditor) writes
   ============================================================
   Alignment is a class, never a style attribute (the CSP blocks those):
   the editor's alignment buttons write these, the `khtml` / `rich`
   sanitise profiles keep them, and every page that shows the HTML
   aligns with them. Mirrored inside the editor by
   public_html/css/khtmleditor/bspkfw-content.css. */
.bspkfw-align-left    { text-align: left; }
.bspkfw-align-center  { text-align: center; }
.bspkfw-align-right   { text-align: right; }
.bspkfw-align-justify { text-align: justify; }
