/* @toc Reset */
/*! modern-normalize v3.0.1 | MIT License | https://github.com/sindresorhus/modern-normalize */

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

html {
    font-family:
        system-ui,
        'Segoe UI',
        Roboto,
        Helvetica,
        Arial,
        sans-serif,
        'Apple Color Emoji',
        'Segoe UI Emoji';
    line-height: 1.15;
    -webkit-text-size-adjust: 100%;
    tab-size: 4;
}

body {
    margin: 0;
}

b,
strong {
    font-weight: bolder;
}

code,
kbd,
samp,
pre {
    font-family:
        ui-monospace,
        SFMono-Regular,
        Consolas,
        'Liberation Mono',
        Menlo,
        monospace;
    font-size: 1em;
}

small {
    font-size: 80%;
}

sub,
sup {
    font-size: 75%;
    line-height: 0;
    position: relative;
    vertical-align: baseline;
}

sub {
    bottom: -0.25em;
}

sup {
    top: -0.5em;
}

table {
    border-color: currentcolor;
}

button,
input,
optgroup,
select,
textarea {
    font-family: inherit;
    font-size: 100%;
    line-height: 1.15;
    margin: 0;
}

button,
[type='button'],
[type='reset'],
[type='submit'] {
    -webkit-appearance: button;
}

legend {
    padding: 0;
}

progress {
    vertical-align: baseline;
}

summary {
    display: list-item;
}

/* @toc Fonts */
/* @font-face for Geist + Geist Mono. Hand-written for now; TypographyService
 * will generate these by scanning the fonts folder. Files in public/fonts/. */

@font-face {
    font-family: 'Geist';
    font-weight: 400;
    font-style: normal;
    font-display: swap;
    src: url('/fonts/geist/Geist-Regular.ttf') format('truetype');
}

@font-face {
    font-family: 'Geist';
    font-weight: 700;
    font-style: normal;
    font-display: swap;
    src: url('/fonts/geist/Geist-Bold.ttf') format('truetype');
}

@font-face {
    font-family: 'Geist';
    font-weight: 900;
    font-style: normal;
    font-display: swap;
    src: url('/fonts/geist/Geist-Black.ttf') format('truetype');
}

@font-face {
    font-family: 'Geist Mono';
    font-weight: 400;
    font-style: normal;
    font-display: swap;
    src: url('/fonts/geist-mono/GeistMono-Regular.ttf') format('truetype');
}

@font-face {
    font-family: 'Geist Mono';
    font-weight: 700;
    font-style: normal;
    font-display: swap;
    src: url('/fonts/geist-mono/GeistMono-Bold.ttf') format('truetype');
}

/* ── #1247: the two self-hosted OFL families for Dante ────────────────────────────────────────────────
 * Declared in config/css/fonts.php (the manifest) and asserted against it by TypographyCatalogTest, so
 * these blocks and that file cannot drift apart. Self-hosted from public/fonts/ — NO runtime font CDN.
 *
 * `font-display: swap` throughout, matching Geist: text is readable in the fallback immediately and
 * re-renders when the face arrives. For a rare display face that is strictly better than a blocking
 * period where the heading is invisible.
 */

/* IM FELL Great Primer — RARE DISPLAY. Static 400 only, regular + italic: upstream ships no other weight,
   and a synthesised bold would smear letterforms whose irregularity is the entire point of the face. */
@font-face {
    font-family: 'IM FELL Great Primer';
    font-weight: 400;
    font-style: normal;
    font-display: swap;
    src: url('/fonts/im-fell-great-primer/IMFeGPrm28P.ttf') format('truetype');
}

@font-face {
    font-family: 'IM FELL Great Primer';
    font-weight: 400;
    font-style: italic;
    font-display: swap;
    src: url('/fonts/im-fell-great-primer/IMFeGPit28P.ttf') format('truetype');
}

/* Cormorant Garamond — EDITORIAL/LORE, VARIABLE on weight.
   `font-weight: 300 700` is a RANGE, not a typo: it tells the browser this one file covers the whole axis,
   so it interpolates any weight in between. Declaring a single number here would waste the variable file and
   make the browser synthesise everything else. The filename's [wght] brackets are literal — they are the
   upstream google/fonts name, kept so the bytes verify against source, and they are URL-safe unquoted. */
@font-face {
    font-family: 'Cormorant Garamond';
    font-weight: 300 700;
    font-style: normal;
    font-display: swap;
    src: url('/fonts/cormorant-garamond/CormorantGaramond[wght].ttf') format('truetype-variations');
}

@font-face {
    font-family: 'Cormorant Garamond';
    font-weight: 300 700;
    font-style: italic;
    font-display: swap;
    src: url('/fonts/cormorant-garamond/CormorantGaramond-Italic[wght].ttf') format('truetype-variations');
}

/* @toc Palettes */
:root {
    --palette-color-names: red orange amber yellow lime green emerald teal cyan sky blue indigo violet purple fuchsia pink rose;
    --palette-neutral-names: slate gray zinc neutral stone;
}
:root {
    --palette-amber-50: #fffbeb;
    --palette-amber-100: #fef3c7;
    --palette-amber-200: #fde68a;
    --palette-amber-300: #fcd34d;
    --palette-amber-400: #fbbf24;
    --palette-amber-500: #f59e0b;
    --palette-amber-600: #d97706;
    --palette-amber-700: #b45309;
    --palette-amber-800: #92400e;
    --palette-amber-900: #78350f;
    --palette-amber-950: #451a03;
}
:root {
    --palette-blue-50: #eff6ff;
    --palette-blue-100: #dbeafe;
    --palette-blue-200: #bfdbfe;
    --palette-blue-300: #93c5fd;
    --palette-blue-400: #60a5fa;
    --palette-blue-500: #3b82f6;
    --palette-blue-600: #2563eb;
    --palette-blue-700: #1d4ed8;
    --palette-blue-800: #1e40af;
    --palette-blue-900: #1e3a8a;
    --palette-blue-950: #172554;
}
:root {
    --palette-cyan-50: #ecfeff;
    --palette-cyan-100: #cffafe;
    --palette-cyan-200: #a5f3fc;
    --palette-cyan-300: #67e8f9;
    --palette-cyan-400: #22d3ee;
    --palette-cyan-500: #06b6d4;
    --palette-cyan-600: #0891b2;
    --palette-cyan-700: #0e7490;
    --palette-cyan-800: #155e75;
    --palette-cyan-900: #164e63;
    --palette-cyan-950: #083344;
}
:root {
    --palette-emerald-50: #ecfdf5;
    --palette-emerald-100: #d1fae5;
    --palette-emerald-200: #a7f3d0;
    --palette-emerald-300: #6ee7b7;
    --palette-emerald-400: #34d399;
    --palette-emerald-500: #10b981;
    --palette-emerald-600: #059669;
    --palette-emerald-700: #047857;
    --palette-emerald-800: #065f46;
    --palette-emerald-900: #064e3b;
    --palette-emerald-950: #022c22;
}
:root {
    --palette-fuchsia-50: #fdf4ff;
    --palette-fuchsia-100: #fae8ff;
    --palette-fuchsia-200: #f5d0fe;
    --palette-fuchsia-300: #f0abfc;
    --palette-fuchsia-400: #e879f9;
    --palette-fuchsia-500: #d946ef;
    --palette-fuchsia-600: #c026d3;
    --palette-fuchsia-700: #a21caf;
    --palette-fuchsia-800: #86198f;
    --palette-fuchsia-900: #701a75;
    --palette-fuchsia-950: #4a044e;
}
:root {
    --palette-gray-50: #f9fafb;
    --palette-gray-100: #f3f4f6;
    --palette-gray-200: #e5e7eb;
    --palette-gray-300: #d1d5db;
    --palette-gray-400: #9ca3af;
    --palette-gray-500: #6b7280;
    --palette-gray-600: #4b5563;
    --palette-gray-700: #374151;
    --palette-gray-800: #1f2937;
    --palette-gray-900: #111827;
    --palette-gray-950: #030712;
}
:root {
    --palette-green-50: #f0fdf4;
    --palette-green-100: #dcfce7;
    --palette-green-200: #bbf7d0;
    --palette-green-300: #86efac;
    --palette-green-400: #4ade80;
    --palette-green-500: #22c55e;
    --palette-green-600: #16a34a;
    --palette-green-700: #15803d;
    --palette-green-800: #166534;
    --palette-green-900: #14532d;
    --palette-green-950: #052e16;
}
:root {
    --palette-indigo-50: #eef2ff;
    --palette-indigo-100: #e0e7ff;
    --palette-indigo-200: #c7d2fe;
    --palette-indigo-300: #a5b4fc;
    --palette-indigo-400: #818cf8;
    --palette-indigo-500: #6366f1;
    --palette-indigo-600: #4f46e5;
    --palette-indigo-700: #4338ca;
    --palette-indigo-800: #3730a3;
    --palette-indigo-900: #312e81;
    --palette-indigo-950: #1e1b4b;
}
:root {
    --palette-lime-50: #f7fee7;
    --palette-lime-100: #ecfccb;
    --palette-lime-200: #d9f99d;
    --palette-lime-300: #bef264;
    --palette-lime-400: #a3e635;
    --palette-lime-500: #84cc16;
    --palette-lime-600: #65a30d;
    --palette-lime-700: #4d7c0f;
    --palette-lime-800: #3f6212;
    --palette-lime-900: #365314;
    --palette-lime-950: #1a2e05;
}
:root {
    --palette-neutral-50: #fafafa;
    --palette-neutral-100: #f5f5f5;
    --palette-neutral-200: #e5e5e5;
    --palette-neutral-300: #d4d4d4;
    --palette-neutral-400: #a3a3a3;
    --palette-neutral-500: #737373;
    --palette-neutral-600: #525252;
    --palette-neutral-700: #404040;
    --palette-neutral-800: #262626;
    --palette-neutral-900: #171717;
    --palette-neutral-950: #0a0a0a;
}
:root {
    --palette-orange-50: #fff7ed;
    --palette-orange-100: #ffedd5;
    --palette-orange-200: #fed7aa;
    --palette-orange-300: #fdba74;
    --palette-orange-400: #fb923c;
    --palette-orange-500: #f97316;
    --palette-orange-600: #ea580c;
    --palette-orange-700: #c2410c;
    --palette-orange-800: #9a3412;
    --palette-orange-900: #7c2d12;
    --palette-orange-950: #431407;
}
:root {
    --palette-pink-50: #fdf2f8;
    --palette-pink-100: #fce7f3;
    --palette-pink-200: #fbcfe8;
    --palette-pink-300: #f9a8d4;
    --palette-pink-400: #f472b6;
    --palette-pink-500: #ec4899;
    --palette-pink-600: #db2777;
    --palette-pink-700: #be185d;
    --palette-pink-800: #9d174d;
    --palette-pink-900: #831843;
    --palette-pink-950: #500724;
}
:root {
    --palette-purple-50: #faf5ff;
    --palette-purple-100: #f3e8ff;
    --palette-purple-200: #e9d5ff;
    --palette-purple-300: #d8b4fe;
    --palette-purple-400: #c084fc;
    --palette-purple-500: #a855f7;
    --palette-purple-600: #9333ea;
    --palette-purple-700: #7e22ce;
    --palette-purple-800: #6b21a8;
    --palette-purple-900: #581c87;
    --palette-purple-950: #3b0764;
}
:root {
    --palette-red-50: #fef2f2;
    --palette-red-100: #fee2e2;
    --palette-red-200: #fecaca;
    --palette-red-300: #fca5a5;
    --palette-red-400: #f87171;
    --palette-red-500: #ef4444;
    --palette-red-600: #dc2626;
    --palette-red-700: #b91c1c;
    --palette-red-800: #991b1b;
    --palette-red-900: #7f1d1d;
    --palette-red-950: #450a0a;
}
:root {
    --palette-rose-50: #fff1f2;
    --palette-rose-100: #ffe4e6;
    --palette-rose-200: #fecdd3;
    --palette-rose-300: #fda4af;
    --palette-rose-400: #fb7185;
    --palette-rose-500: #f43f5e;
    --palette-rose-600: #e11d48;
    --palette-rose-700: #be123c;
    --palette-rose-800: #9f1239;
    --palette-rose-900: #881337;
    --palette-rose-950: #4c0519;
}
:root {
    --palette-sky-50: #f0f9ff;
    --palette-sky-100: #e0f2fe;
    --palette-sky-200: #bae6fd;
    --palette-sky-300: #7dd3fc;
    --palette-sky-400: #38bdf8;
    --palette-sky-500: #0ea5e9;
    --palette-sky-600: #0284c7;
    --palette-sky-700: #0369a1;
    --palette-sky-800: #075985;
    --palette-sky-900: #0c4a6e;
    --palette-sky-950: #082f49;
}
:root {
    --palette-slate-50: #f8fafc;
    --palette-slate-100: #f1f5f9;
    --palette-slate-200: #e2e8f0;
    --palette-slate-300: #cbd5e1;
    --palette-slate-400: #94a3b8;
    --palette-slate-500: #64748b;
    --palette-slate-600: #475569;
    --palette-slate-700: #334155;
    --palette-slate-800: #1e293b;
    --palette-slate-900: #0f172a;
    --palette-slate-950: #020617;
}
:root {
    --palette-stone-50: #fafaf9;
    --palette-stone-100: #f5f5f4;
    --palette-stone-200: #e7e5e4;
    --palette-stone-300: #d6d3d1;
    --palette-stone-400: #a8a29e;
    --palette-stone-500: #78716c;
    --palette-stone-600: #57534e;
    --palette-stone-700: #44403c;
    --palette-stone-800: #292524;
    --palette-stone-900: #1c1917;
    --palette-stone-950: #0c0a09;
}
:root {
    --palette-teal-50: #f0fdfa;
    --palette-teal-100: #ccfbf1;
    --palette-teal-200: #99f6e4;
    --palette-teal-300: #5eead4;
    --palette-teal-400: #2dd4bf;
    --palette-teal-500: #14b8a6;
    --palette-teal-600: #0d9488;
    --palette-teal-700: #0f766e;
    --palette-teal-800: #115e59;
    --palette-teal-900: #134e4a;
    --palette-teal-950: #042f2e;
}
:root {
    --palette-violet-50: #f5f3ff;
    --palette-violet-100: #ede9fe;
    --palette-violet-200: #ddd6fe;
    --palette-violet-300: #c4b5fd;
    --palette-violet-400: #a78bfa;
    --palette-violet-500: #8b5cf6;
    --palette-violet-600: #7c3aed;
    --palette-violet-700: #6d28d9;
    --palette-violet-800: #5b21b6;
    --palette-violet-900: #4c1d95;
    --palette-violet-950: #2e1065;
}
:root {
    --palette-yellow-50: #fefce8;
    --palette-yellow-100: #fef9c3;
    --palette-yellow-200: #fef08a;
    --palette-yellow-300: #fde047;
    --palette-yellow-400: #facc15;
    --palette-yellow-500: #eab308;
    --palette-yellow-600: #ca8a04;
    --palette-yellow-700: #a16207;
    --palette-yellow-800: #854d0e;
    --palette-yellow-900: #713f12;
    --palette-yellow-950: #422006;
}
:root {
    --palette-zinc-50: #fafafa;
    --palette-zinc-100: #f4f4f5;
    --palette-zinc-200: #e4e4e7;
    --palette-zinc-300: #d4d4d8;
    --palette-zinc-400: #a1a1aa;
    --palette-zinc-500: #71717a;
    --palette-zinc-600: #52525b;
    --palette-zinc-700: #3f3f46;
    --palette-zinc-800: #27272a;
    --palette-zinc-900: #18181b;
    --palette-zinc-950: #09090b;
}
:root[data-primary="amber"] {
    --primary-50: #fffbeb;
    --primary-100: #fef3c7;
    --primary-200: #fde68a;
    --primary-300: #fcd34d;
    --primary-400: #fbbf24;
    --primary-500: #f59e0b;
    --primary-600: #d97706;
    --primary-700: #b45309;
    --primary-800: #92400e;
    --primary-900: #78350f;
    --primary-950: #451a03;
}
:root[data-primary="blue"] {
    --primary-50: #eff6ff;
    --primary-100: #dbeafe;
    --primary-200: #bfdbfe;
    --primary-300: #93c5fd;
    --primary-400: #60a5fa;
    --primary-500: #3b82f6;
    --primary-600: #2563eb;
    --primary-700: #1d4ed8;
    --primary-800: #1e40af;
    --primary-900: #1e3a8a;
    --primary-950: #172554;
}
:root[data-primary="cyan"] {
    --primary-50: #ecfeff;
    --primary-100: #cffafe;
    --primary-200: #a5f3fc;
    --primary-300: #67e8f9;
    --primary-400: #22d3ee;
    --primary-500: #06b6d4;
    --primary-600: #0891b2;
    --primary-700: #0e7490;
    --primary-800: #155e75;
    --primary-900: #164e63;
    --primary-950: #083344;
}
:root[data-primary="emerald"] {
    --primary-50: #ecfdf5;
    --primary-100: #d1fae5;
    --primary-200: #a7f3d0;
    --primary-300: #6ee7b7;
    --primary-400: #34d399;
    --primary-500: #10b981;
    --primary-600: #059669;
    --primary-700: #047857;
    --primary-800: #065f46;
    --primary-900: #064e3b;
    --primary-950: #022c22;
}
:root[data-primary="fuchsia"] {
    --primary-50: #fdf4ff;
    --primary-100: #fae8ff;
    --primary-200: #f5d0fe;
    --primary-300: #f0abfc;
    --primary-400: #e879f9;
    --primary-500: #d946ef;
    --primary-600: #c026d3;
    --primary-700: #a21caf;
    --primary-800: #86198f;
    --primary-900: #701a75;
    --primary-950: #4a044e;
}
:root[data-neutral="gray"] {
    --neutral-50: #f9fafb;
    --neutral-100: #f3f4f6;
    --neutral-200: #e5e7eb;
    --neutral-300: #d1d5db;
    --neutral-400: #9ca3af;
    --neutral-500: #6b7280;
    --neutral-600: #4b5563;
    --neutral-700: #374151;
    --neutral-800: #1f2937;
    --neutral-900: #111827;
    --neutral-950: #030712;
}
:root[data-primary="green"] {
    --primary-50: #f0fdf4;
    --primary-100: #dcfce7;
    --primary-200: #bbf7d0;
    --primary-300: #86efac;
    --primary-400: #4ade80;
    --primary-500: #22c55e;
    --primary-600: #16a34a;
    --primary-700: #15803d;
    --primary-800: #166534;
    --primary-900: #14532d;
    --primary-950: #052e16;
}
:root[data-primary="indigo"] {
    --primary-50: #eef2ff;
    --primary-100: #e0e7ff;
    --primary-200: #c7d2fe;
    --primary-300: #a5b4fc;
    --primary-400: #818cf8;
    --primary-500: #6366f1;
    --primary-600: #4f46e5;
    --primary-700: #4338ca;
    --primary-800: #3730a3;
    --primary-900: #312e81;
    --primary-950: #1e1b4b;
}
:root[data-primary="lime"] {
    --primary-50: #f7fee7;
    --primary-100: #ecfccb;
    --primary-200: #d9f99d;
    --primary-300: #bef264;
    --primary-400: #a3e635;
    --primary-500: #84cc16;
    --primary-600: #65a30d;
    --primary-700: #4d7c0f;
    --primary-800: #3f6212;
    --primary-900: #365314;
    --primary-950: #1a2e05;
}
:root[data-neutral="neutral"] {
    --neutral-50: #fafafa;
    --neutral-100: #f5f5f5;
    --neutral-200: #e5e5e5;
    --neutral-300: #d4d4d4;
    --neutral-400: #a3a3a3;
    --neutral-500: #737373;
    --neutral-600: #525252;
    --neutral-700: #404040;
    --neutral-800: #262626;
    --neutral-900: #171717;
    --neutral-950: #0a0a0a;
}
:root[data-primary="orange"] {
    --primary-50: #fff7ed;
    --primary-100: #ffedd5;
    --primary-200: #fed7aa;
    --primary-300: #fdba74;
    --primary-400: #fb923c;
    --primary-500: #f97316;
    --primary-600: #ea580c;
    --primary-700: #c2410c;
    --primary-800: #9a3412;
    --primary-900: #7c2d12;
    --primary-950: #431407;
}
:root[data-primary="pink"] {
    --primary-50: #fdf2f8;
    --primary-100: #fce7f3;
    --primary-200: #fbcfe8;
    --primary-300: #f9a8d4;
    --primary-400: #f472b6;
    --primary-500: #ec4899;
    --primary-600: #db2777;
    --primary-700: #be185d;
    --primary-800: #9d174d;
    --primary-900: #831843;
    --primary-950: #500724;
}
:root[data-primary="purple"] {
    --primary-50: #faf5ff;
    --primary-100: #f3e8ff;
    --primary-200: #e9d5ff;
    --primary-300: #d8b4fe;
    --primary-400: #c084fc;
    --primary-500: #a855f7;
    --primary-600: #9333ea;
    --primary-700: #7e22ce;
    --primary-800: #6b21a8;
    --primary-900: #581c87;
    --primary-950: #3b0764;
}
:root[data-primary="red"] {
    --primary-50: #fef2f2;
    --primary-100: #fee2e2;
    --primary-200: #fecaca;
    --primary-300: #fca5a5;
    --primary-400: #f87171;
    --primary-500: #ef4444;
    --primary-600: #dc2626;
    --primary-700: #b91c1c;
    --primary-800: #991b1b;
    --primary-900: #7f1d1d;
    --primary-950: #450a0a;
}
:root[data-primary="rose"] {
    --primary-50: #fff1f2;
    --primary-100: #ffe4e6;
    --primary-200: #fecdd3;
    --primary-300: #fda4af;
    --primary-400: #fb7185;
    --primary-500: #f43f5e;
    --primary-600: #e11d48;
    --primary-700: #be123c;
    --primary-800: #9f1239;
    --primary-900: #881337;
    --primary-950: #4c0519;
}
:root[data-primary="sky"] {
    --primary-50: #f0f9ff;
    --primary-100: #e0f2fe;
    --primary-200: #bae6fd;
    --primary-300: #7dd3fc;
    --primary-400: #38bdf8;
    --primary-500: #0ea5e9;
    --primary-600: #0284c7;
    --primary-700: #0369a1;
    --primary-800: #075985;
    --primary-900: #0c4a6e;
    --primary-950: #082f49;
}
:root[data-neutral="slate"] {
    --neutral-50: #f8fafc;
    --neutral-100: #f1f5f9;
    --neutral-200: #e2e8f0;
    --neutral-300: #cbd5e1;
    --neutral-400: #94a3b8;
    --neutral-500: #64748b;
    --neutral-600: #475569;
    --neutral-700: #334155;
    --neutral-800: #1e293b;
    --neutral-900: #0f172a;
    --neutral-950: #020617;
}
:root[data-neutral="stone"] {
    --neutral-50: #fafaf9;
    --neutral-100: #f5f5f4;
    --neutral-200: #e7e5e4;
    --neutral-300: #d6d3d1;
    --neutral-400: #a8a29e;
    --neutral-500: #78716c;
    --neutral-600: #57534e;
    --neutral-700: #44403c;
    --neutral-800: #292524;
    --neutral-900: #1c1917;
    --neutral-950: #0c0a09;
}
:root[data-primary="teal"] {
    --primary-50: #f0fdfa;
    --primary-100: #ccfbf1;
    --primary-200: #99f6e4;
    --primary-300: #5eead4;
    --primary-400: #2dd4bf;
    --primary-500: #14b8a6;
    --primary-600: #0d9488;
    --primary-700: #0f766e;
    --primary-800: #115e59;
    --primary-900: #134e4a;
    --primary-950: #042f2e;
}
:root[data-primary="violet"] {
    --primary-50: #f5f3ff;
    --primary-100: #ede9fe;
    --primary-200: #ddd6fe;
    --primary-300: #c4b5fd;
    --primary-400: #a78bfa;
    --primary-500: #8b5cf6;
    --primary-600: #7c3aed;
    --primary-700: #6d28d9;
    --primary-800: #5b21b6;
    --primary-900: #4c1d95;
    --primary-950: #2e1065;
}
:root[data-primary="yellow"] {
    --primary-50: #fefce8;
    --primary-100: #fef9c3;
    --primary-200: #fef08a;
    --primary-300: #fde047;
    --primary-400: #facc15;
    --primary-500: #eab308;
    --primary-600: #ca8a04;
    --primary-700: #a16207;
    --primary-800: #854d0e;
    --primary-900: #713f12;
    --primary-950: #422006;
}
:root[data-neutral="zinc"] {
    --neutral-50: #fafafa;
    --neutral-100: #f4f4f5;
    --neutral-200: #e4e4e7;
    --neutral-300: #d4d4d8;
    --neutral-400: #a1a1aa;
    --neutral-500: #71717a;
    --neutral-600: #52525b;
    --neutral-700: #3f3f46;
    --neutral-800: #27272a;
    --neutral-900: #18181b;
    --neutral-950: #09090b;
}
/* @toc Planner categorical label tokens (R13, vetted) */
:root {
    --planner-fam-slate: #64748b;
    --planner-fam-slate-active: #475569;
    --planner-fam-gray: #6b7280;
    --planner-fam-gray-active: #4b5563;
    --planner-fam-zinc: #71717a;
    --planner-fam-zinc-active: #52525b;
    --planner-fam-neutral: #737373;
    --planner-fam-neutral-active: #525252;
    --planner-fam-stone: #78716c;
    --planner-fam-stone-active: #57534e;
    --planner-fam-red: #ef4444;
    --planner-fam-red-active: #dc2626;
    --planner-fam-orange: #f97316;
    --planner-fam-orange-active: #c2410c;
    --planner-fam-amber: #f59e0b;
    --planner-fam-amber-active: #b45309;
    --planner-fam-yellow: #eab308;
    --planner-fam-yellow-active: #a16207;
    --planner-fam-lime: #84cc16;
    --planner-fam-lime-active: #4d7c0f;
    --planner-fam-green: #22c55e;
    --planner-fam-green-active: #15803d;
    --planner-fam-emerald: #10b981;
    --planner-fam-emerald-active: #047857;
    --planner-fam-teal: #14b8a6;
    --planner-fam-teal-active: #0f766e;
    --planner-fam-cyan: #06b6d4;
    --planner-fam-cyan-active: #0e7490;
    --planner-fam-sky: #0ea5e9;
    --planner-fam-sky-active: #0369a1;
    --planner-fam-blue: #3b82f6;
    --planner-fam-blue-active: #2563eb;
    --planner-fam-indigo: #6366f1;
    --planner-fam-indigo-active: #4f46e5;
    --planner-fam-violet: #8b5cf6;
    --planner-fam-violet-active: #7c3aed;
    --planner-fam-purple: #a855f7;
    --planner-fam-purple-active: #9333ea;
    --planner-fam-fuchsia: #d946ef;
    --planner-fam-fuchsia-active: #c026d3;
    --planner-fam-pink: #ec4899;
    --planner-fam-pink-active: #db2777;
    --planner-fam-rose: #f43f5e;
    --planner-fam-rose-active: #e11d48;
}
/* @toc Annotation slot tokens (reader, vetted) */
:root[data-theme="light"], [data-theme-scope="light"] {
    --annotation-1: #be123c;
    --annotation-1-surface: #ffe4e6;
    --annotation-2: #047857;
    --annotation-2-surface: #d1fae5;
    --annotation-3: #1d4ed8;
    --annotation-3-surface: #dbeafe;
    --annotation-4: #6d28d9;
    --annotation-4-surface: #ede9fe;
    --annotation-5: #c2410c;
    --annotation-5-surface: #ffedd5;
    --annotation-6: #be185d;
    --annotation-6-surface: #fce7f3;
}
:root[data-theme="dark"], [data-theme-scope="dark"] {
    --annotation-1: #fb7185;
    --annotation-1-surface: #4c0519;
    --annotation-2: #34d399;
    --annotation-2-surface: #022c22;
    --annotation-3: #60a5fa;
    --annotation-3-surface: #172554;
    --annotation-4: #a78bfa;
    --annotation-4-surface: #2e1065;
    --annotation-5: #fb923c;
    --annotation-5-surface: #431407;
    --annotation-6: #f472b6;
    --annotation-6-surface: #500724;
}
/* @toc Utilities */
/* Utilities — shared CSS variables that don't belong to a palette, to typography,
 * to a component, to a page, or to reset: the structural scales. Theme-neutral
 * primitives, cribbed pared-down from leadthink's utilities. A catch-all by
 * definition, but a justified one — every var here is a reusable primitive a
 * component, global, or the typography config can space / round / size / elevate
 * to. */

:root {
    /* @toc Spacing — 4px base; padding, margin, gap. Tailwind's rungs: the integer ladder is complete 1–12
     * (13/15 don't exist in Tailwind), then 14/16/20/24/28/32 (28/32 added for the marketing-section scale —
     * section vertical padding runs to 128px). This is the ONE structural scale — width/height read
     * it too (no separate ladders), and config facets can name `space-*` bare (CssService::mapValue). */
    --space-0: 0;
    --space-px: 1px;
    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 20px;
    --space-6: 24px;
    --space-7: 28px;
    --space-8: 32px;
    --space-9: 36px;
    --space-10: 40px;
    --space-11: 44px;
    --space-12: 48px;
    --space-14: 56px;
    --space-16: 64px;
    --space-20: 80px;
    --space-24: 96px;
    --space-28: 112px;
    --space-32: 128px;
    --space-40: 160px;

    /* @toc Type scale — rungs the typography config maps elements to. */
    --text-2xs: 0.625rem;
    --text-xs: 0.75rem;
    --text-sm: 0.875rem;
    --text-base: 1rem;
    --text-lg: 1.125rem;
    --text-xl: 1.25rem;
    --text-2xl: 1.5rem;
    --text-3xl: 1.875rem;
    --text-4xl: 2.25rem;
    --text-5xl: 3rem;

    /* @toc Border radius — rungs global / components round to. */
    --radius-sm: 6px;
    --radius: 8px;
    --radius-md: 10px;
    --radius-lg: 12px;
    --radius-xl: 16px;
    --radius-2xl: 22px;
    --radius-full: 9999px;

    /* @toc Semantic radius — the ruled TWO-TIER system (R1, ruling sheet 2026-07-14): CONTROLS (buttons,
     * inputs, chips, small interactive) round to 6; CONTAINERS (cards, panels, dialogs, menus) to 8.
     * Components consume these two — not the raw rungs or off-scale literals (card 14 / dropdown 12 / tabs 7)
     * — so the system stays coherent. Ticket B migrates the components onto them. */
    --radius-control: 6px;
    --radius-container: 8px;

    /* @toc Interaction — the ruled disabled dim (R3): ONE house-wide opacity for a disabled control,
     * unifying the scattered 0.5 / 0.55. Consumed as `opacity: var(--disabled-opacity)`. */
    --disabled-opacity: 0.55;

    /* @toc Max width — containers / prose. */
    --max-w-sm: 384px;
    --max-w-md: 448px;
    --max-w-lg: 512px;
    --max-w-xl: 576px;
    --max-w-2xl: 672px;
    --max-w-3xl: 768px;
    --max-w-4xl: 896px;
    --max-w-prose: 65ch;

    /* @toc Shadows — NOT here: elevation reads differently on a dark surface than a light one, so the
     * --shadow-* set lives in config/theme.php (light/dark), remapped per data-theme like --surface/--text.
     * Same names (var(--shadow-md) …), Tailwind's two-layer values, higher alpha in dark. */

    /* @toc Opacity — ZERO consumers today (kept: the scale is tiny and cheap). First real use adopts it;
     * if none arrives it's a clean prune. Recorded here so its status isn't a mystery. */
    --opacity-0: 0;
    --opacity-25: 0.25;
    --opacity-50: 0.5;
    --opacity-75: 0.75;
    --opacity-100: 1;

    /* @toc Z-index — stacking ladder. */
    --z-10: 10;
    --z-20: 20;
    --z-30: 30;
    --z-40: 40;
    --z-50: 50;
    --z-overlay: 100;
    --z-modal: 200;
    --z-popover: 300;
    --z-tooltip: 400;

    /* @toc Transitions */
    --transition-fast: 150ms ease-in-out;
    --transition-default: 300ms ease-in-out;
    --transition-slow: 500ms ease-in-out;
}

/* @toc Typography */
:root {
    --font-sans: 'Geist', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
    --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Consolas, monospace;
    --font-display: 'IM FELL Great Primer', 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, serif;
    --font-editorial: 'Cormorant Garamond', 'Iowan Old Style', Georgia, 'Times New Roman', serif;
    --h1-size: var(--text-3xl);
    --h1-weight: 500;
    --h1-leading: 1.2;
    --h2-size: var(--text-2xl);
    --h2-weight: 500;
    --h2-leading: 1.25;
    --h3-size: var(--text-xl);
    --h3-weight: 500;
    --h3-leading: 1.3;
    --h4-size: var(--text-lg);
    --h4-weight: 500;
    --h4-leading: 1.35;
    --h5-size: var(--text-base);
    --h5-weight: 600;
    --h5-leading: 1.4;
    --h6-size: var(--text-sm);
    --h6-weight: 600;
    --h6-leading: 1.4;
    --body-size: var(--text-base);
    --body-weight: 400;
    --body-leading: 1.6;
}
/* Typography — applies the config-driven type system to the base elements. The
 * families and per-element size/weight/line-height come from
 * config/css/typography.php, emitted as --font-* and --<el>-* by
 * TypographyService::vars(); this file just wires those vars onto the elements
 * and adds the structural bits the config doesn't carry (heading margins, links,
 * mono). The raw --text-* scale lives in utilities.css. Font files: fonts.css. */

body {
    font-family: var(--font-sans);
    font-size: var(--body-size);
    line-height: var(--body-leading);
}

h1,
h2,
h3,
h4,
h5,
h6 {
    margin: 0 0 0.4em;
    font-family: var(--font-sans);
}

h1 {
    font-size: var(--h1-size);
    font-weight: var(--h1-weight);
    line-height: var(--h1-leading);
}

h2 {
    font-size: var(--h2-size);
    font-weight: var(--h2-weight);
    line-height: var(--h2-leading);
}

h3 {
    font-size: var(--h3-size);
    font-weight: var(--h3-weight);
    line-height: var(--h3-leading);
}

h4 {
    font-size: var(--h4-size);
    font-weight: var(--h4-weight);
    line-height: var(--h4-leading);
}

h5 {
    font-size: var(--h5-size);
    font-weight: var(--h5-weight);
    line-height: var(--h5-leading);
}

h6 {
    font-size: var(--h6-size);
    font-weight: var(--h6-weight);
    line-height: var(--h6-leading);
}

p {
    margin: 0 0 0.8em;
    line-height: var(--body-leading);
}

a {
    color: var(--link);
    text-decoration: underline;
    text-underline-offset: 2px;
    transition: opacity var(--transition-fast);
}

a:hover {
    opacity: 0.8;
}

/* @toc Animations */
/* Animations — reusable, swappable entrance animations a component can opt into
 * (a card fading / rising / sliding / zooming in). Apply a class; the keyframes +
 * sane defaults are below, with duration/easing exposed as --anim-* so a component
 * can override per use. Cribbed pared-down from leadthink's animation set,
 * entrance-only. Honours prefers-reduced-motion. */

:root {
    --anim-duration: 500ms;
    --anim-easing: cubic-bezier(0.16, 1, 0.3, 1);
}

.anim-fade-in {
    animation: anim-fade-in var(--anim-duration) var(--anim-easing) both;
}

.anim-rise {
    animation: anim-rise var(--anim-duration) var(--anim-easing) both;
}

.anim-scale-in {
    animation: anim-scale-in var(--anim-duration) var(--anim-easing) both;
}

.anim-slide-in {
    animation: anim-slide-in var(--anim-duration) var(--anim-easing) both;
}

.anim-zoom-in {
    animation: anim-zoom-in var(--anim-duration) var(--anim-easing) both;
}

@keyframes anim-fade-in {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}

@keyframes anim-rise {
    from {
        opacity: 0;
        transform: translateY(16px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

@keyframes anim-scale-in {
    from {
        opacity: 0;
        transform: scale(0.96);
    }
    to {
        opacity: 1;
        transform: scale(1);
    }
}

@keyframes anim-slide-in {
    from {
        opacity: 0;
        transform: translateX(24px);
    }
    to {
        opacity: 1;
        transform: translateX(0);
    }
}

@keyframes anim-zoom-in {
    from {
        opacity: 0;
        transform: scale(0.8) translateY(12px);
    }
    to {
        opacity: 1;
        transform: scale(1) translateY(0);
    }
}

@media (prefers-reduced-motion: reduce) {
    .anim-fade-in,
    .anim-rise,
    .anim-scale-in,
    .anim-slide-in,
    .anim-zoom-in {
        animation: none;
    }
}

/* Beyond the entrance classes: neutralise ALL motion for users who ask for it -
 * component transitions (tabs indicator, drawer, tooltip, swatches, chevron) and
 * the login shake alike. The !important duration override even beats the inline
 * animation login.js sets, so no per-component JS gate is needed (the one exception
 * is JS scrollIntoView, which takes a behavior arg dev-tools.js checks itself). */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

/* @toc Globals */
:root {
    --global-border-radius: var(--radius-lg);
    --global-border-width: 1px;
    --global-border-style: solid;
}
/* @toc Theme + component vars */
:root {
    --login-border-radius: 14px;
    --login-padding: 28px;
    --login-background-color: var(--surface);
    --login-color: var(--text);
    --login-border: var(--line);
    --login-error-color: var(--primary-600);
    --theme-panel-swatch-size: 26px;
    --theme-panel-ring: var(--primary);
    --theme-panel-ring-width: 2px;
    --typography-panel-specimen-font-size: 20px;
    --typography-panel-row-gap: var(--space-2);
    --typography-panel-ring: var(--primary);
    --typography-panel-ring-width: 2px;
    --sidebar-nav-width: 240px;
    --flow-node-background-color: var(--neutral-50);
    --flow-node-border: var(--neutral-200);
    --flow-node-text: var(--neutral-900);
    --flow-note-text: var(--neutral-500);
    --flow-edge: var(--neutral-400);
    --flow-accent-background-color: var(--primary-600);
    --flow-accent-text: var(--neutral-50);
    --dev-tools-surface: var(--palette-slate-950);
    --dev-tools-surface-raised: var(--palette-slate-900);
    --dev-tools-surface-hover: var(--palette-slate-800);
    --dev-tools-border: var(--palette-slate-800);
    --dev-tools-border-strong: var(--palette-slate-700);
    --dev-tools-backdrop: rgba(2, 6, 23, 0.6);
    --dev-tools-text: var(--palette-slate-100);
    --dev-tools-text-muted: var(--palette-slate-400);
    --dev-tools-text-faint: var(--palette-slate-600);
    --dev-tools-accent: var(--primary);
    --dev-tools-accent-background-color: var(--palette-slate-800);
    --dev-tools-pill-solid-text: var(--palette-slate-950);
    --dev-tools-pill-muted-text: var(--palette-slate-300);
    --dev-tools-pill-muted-background-color: var(--palette-slate-800);
    --dev-tools-pill-all-text: var(--palette-slate-300);
    --dev-tools-pill-all-background-color: var(--palette-slate-800);
    --dev-tools-pill-all-solid-background-color: var(--palette-slate-400);
    --dev-tools-pill-time-text: var(--palette-sky-400);
    --dev-tools-pill-time-background-color: var(--palette-sky-950);
    --dev-tools-pill-time-solid-background-color: var(--palette-sky-400);
    --dev-tools-pill-time-solid-text: var(--palette-sky-950);
    --financials-ledger-head-text: var(--text-muted);
    --financials-ledger-border: var(--line);
    --financials-ledger-row-hover: var(--surface-raised);
    --financials-ledger-amount-positive: var(--success);
    --financials-ledger-amount-negative: var(--danger);
    --financials-ledger-chip-empty: var(--neutral-400);
    --financials-pnl-income: var(--success);
    --financials-pnl-expense: var(--danger);
    --financials-pnl-tile-background-color: var(--surface);
    --planner-board-gap: 14px;
    --planner-board-list-width: 286px;
    --planner-board-list-background-color: var(--surface);
    --planner-board-card-background-color: var(--surface-raised);
    --planner-board-card-border: var(--line);
    --legend-schedule-swatch-size: 12px;
    --legend-schedule-border-radius: var(--radius-control);
    --study-reader-rail-width: 360px;
    --study-reader-stage-measure: 68ch;
    --study-reader-gutter: 24px;
    --study-reader-drawer-breakpoint: 860px;
    --device-frame-border-radius: var(--radius-lg);
    --device-frame-bezel: var(--space-2);
    --issue-card-border-radius: var(--radius-lg);
    --issue-card-padding: var(--space-4);
    --issue-card-ring-color: var(--line);
    --issue-card-teach-line-height: 1.6;
    --media-strip-gap: 8px;
    --media-strip-row-padding: 16px;
    --media-strip-line: var(--line);
    --media-strip-name-color: var(--text);
    --media-strip-name-font-size: var(--text-sm);
    --media-strip-purpose-color: var(--text-muted);
    --media-strip-purpose-font-size: var(--text-xs);
    --media-strip-number-color: var(--text-muted);
    --media-strip-number-font-size: var(--text-xs);
    --media-strip-tile-background: var(--surface-raised);
    --media-strip-border-radius: 8px;
    --media-strip-focus: var(--primary-500);
    --media-strip-checker: var(--neutral-800);
    --media-preview-checker: var(--neutral-800);
    --media-preview-checker-base: var(--neutral-900);
    --media-preview-white: var(--neutral-50);
    --media-preview-black: var(--neutral-950);
    --media-preview-line: var(--line);
    --media-preview-gap: 8px;
    --media-preview-controls-padding: 12px;
    --media-preview-unbuilt-color: var(--text-muted);
    --media-preview-panel-background: var(--surface);
    --media-preview-panel-width: 380px;
    --media-preview-prompt-background: var(--surface-raised);
    --media-preview-heading-color: var(--text);
    --media-preview-heading-font-size: var(--text-sm);
    --media-preview-label-color: var(--text-muted);
    --media-preview-body-font-size: var(--text-xs);
    --room-canvas-min-block-size: 320px;
    --room-canvas-background-color: var(--surface-muted);
    --room-canvas-border-radius: var(--radius-container);
    --room-canvas-border-color: var(--line);
    --room-canvas-grid-minor-color: var(--line);
    --room-canvas-grid-major-color: var(--line-strong);
    --room-canvas-solid-fill: var(--neutral-500);
    --room-canvas-solid-border-color: var(--neutral-300);
    --room-canvas-anchor-fill: var(--info);
    --room-canvas-hazard-fill: var(--warning);
    --room-canvas-selected-border-color: var(--primary);
    --room-canvas-selected-border-width: 2px;
    --room-canvas-handle-size: 12px;
    --room-canvas-handle-hit-size: 44px;
    --room-canvas-handle-fill: var(--primary);
    --room-canvas-toolbar-gap: var(--space-2);
    --room-canvas-toolbar-padding: var(--space-2);
    --room-canvas-toolbar-background-color: var(--surface-raised);
    --reveal-phase-border-radius: var(--radius-control);
    --annotated-text-underline-width: 2px;
    --annotated-text-marker-font-size: 0.6875rem;
    --annotated-text-marker-font-weight: 600;
    --annotated-text-span-border-radius: 3px;
    --annotated-text-paragraph-gap: 1em;
    --annotated-text-line-height: 1.7;
    --pt-answer-field-gap: 8px;
    --pt-answer-field-min-height: 28rem;
    --pt-answer-field-font-size: var(--text-base);
    --pt-answer-field-line-height: 1.75;
    --pt-answer-field-status-font-size: var(--text-xs);
    --pt-answer-field-status-color: var(--text-muted);
    --pt-answer-field-error-color: var(--danger);
    --editor-tools-gap: var(--space-4);
    --editor-tools-padding: var(--space-4);
    --editor-tools-heading-color: var(--text-muted);
    --editor-tools-item-gap: 0;
    --editor-tools-control-gap: var(--space-3);
    --editor-tools-uneditable-color: var(--text-muted);
    --page-section-max-width: 72rem;
    --section-stats-border-radius: var(--radius-container);
    --section-pricing-border-radius: var(--radius-container);
    --game-border-radius: 8px;
    --game-poster-background-color: var(--surface);
    --game-poster-color: var(--text);
    --game-overlay: var(--neutral-950);
    --game-progress-track: var(--line);
    --game-progress-fill: var(--primary);
    --game-chrome-gap: var(--space-2);
    --game-chrome-padding: var(--space-3);
    --sprite-strip-gap: var(--space-3);
    --sprite-strip-cell-background-color: var(--surface-muted);
    --sprite-strip-cell-border: var(--line);
    --sprite-strip-border-radius: var(--radius-control);
    --sprite-player-stage-background-color: var(--surface-muted);
    --sprite-player-stage-border: var(--line);
    --sprite-player-border-radius: var(--radius-container);
    --button-border-radius: 10px;
    --button-padding-y: 9px;
    --button-padding-x: 18px;
    --button-font-size: var(--text-sm);
    --button-font-weight: 500;
    --button-primary-background-color: var(--primary-600);
    --button-primary-background-color-hover: var(--primary-700);
    --button-primary-background-color-active: var(--primary-800);
    --button-primary-color: var(--on-primary);
    --button-focus-ring: var(--primary);
    --button-ghost-color: var(--text);
    --button-ghost-border: var(--line);
    --button-ghost-border-hover: var(--text-muted);
    --button-ghost-background-color-hover: var(--surface-raised);
    --button-ghost-background-color-active: var(--line);
    --link-color: var(--link);
    --link-muted-color: var(--text-muted);
    --link-muted-color-hover: var(--text);
    --link-focus-ring: var(--primary);
    --link-border-radius: 2px;
    --link-underline-thickness: 1px;
    --link-underline-offset: 2px;
    --link-icon-size: 0.9em;
    --input-border-radius: 8px;
    --input-padding-y: 10px;
    --input-padding-x: 12px;
    --input-font-size: var(--text-sm);
    --input-label-font-size: var(--text-sm);
    --input-label-color: var(--text-muted);
    --input-background-color: var(--surface);
    --input-color: var(--text);
    --input-border: var(--neutral-500);
    --input-focus-border: var(--primary);
    --textarea-border-radius: 8px;
    --textarea-padding-y: 10px;
    --textarea-padding-x: 12px;
    --textarea-font-size: var(--text-sm);
    --textarea-line-height: 1.5;
    --textarea-label-font-size: var(--text-sm);
    --textarea-label-color: var(--text-muted);
    --textarea-background-color: var(--surface);
    --textarea-color: var(--text);
    --textarea-border: var(--neutral-500);
    --textarea-focus-border: var(--primary);
    --textarea-resize: vertical;
    --textarea-min-height: 80px;
    --select-border-radius: 8px;
    --select-padding-y: 10px;
    --select-padding-x: 12px;
    --select-font-size: var(--text-sm);
    --select-label-font-size: var(--text-sm);
    --select-label-color: var(--text-muted);
    --select-background-color: var(--surface);
    --select-color: var(--text);
    --select-border: var(--neutral-500);
    --select-focus-border: var(--primary);
    --select-chevron: var(--text-muted);
    --select-chevron-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%23000'%3E%3Cpath fill-rule='evenodd' d='M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z' clip-rule='evenodd' /%3E%3C/svg%3E");
    --checkbox-size: 18px;
    --checkbox-border-radius: 5px;
    --checkbox-gap: 10px;
    --checkbox-font-size: var(--text-sm);
    --checkbox-dot-size: 8px;
    --checkbox-background-color: var(--surface);
    --checkbox-border: var(--line);
    --checkbox-border-hover: var(--text-muted);
    --checkbox-check: var(--on-primary);
    --checkbox-checked-background-color: var(--primary);
    --checkbox-label-color: var(--text);
    --checkbox-focus-ring: var(--primary);
    --switch-gap: 10px;
    --switch-label-font-size: var(--text-sm);
    --switch-label-color: var(--text);
    --switch-width: 40px;
    --switch-height: 22px;
    --switch-knob-size: 16px;
    --switch-knob-gap: 2px;
    --switch-track-background-color: var(--surface-raised);
    --switch-track-border: var(--line);
    --switch-track-background-color-on: var(--primary);
    --switch-knob-background-color: var(--text-muted);
    --switch-knob-background-color-on: var(--on-primary);
    --switch-focus-ring: var(--primary);
    --slider-label-color: var(--text-muted);
    --slider-label-font-size: var(--text-sm);
    --slider-value-color: var(--text);
    --slider-value-font-size: var(--text-sm);
    --slider-track-height: 6px;
    --slider-track-border-radius: 999px;
    --slider-track-background-color: var(--line);
    --slider-fill: var(--primary);
    --slider-thumb-size: 18px;
    --slider-thumb-background-color: var(--primary);
    --slider-thumb-border: var(--surface);
    --slider-thumb-border-width: 2px;
    --slider-focus-ring: var(--primary);
    --slider-focus-ring-width: 3px;
    --fieldset-padding: 16px;
    --fieldset-border-radius: 8px;
    --fieldset-gap: 4px;
    --fieldset-border: var(--line);
    --fieldset-background-color: var(--surface);
    --fieldset-focus-border: var(--primary);
    --fieldset-legend-color: var(--text);
    --fieldset-legend-font-size: var(--text-sm);
    --fieldset-legend-font-weight: 500;
    --fieldset-legend-padding-x: 6px;
    --multiple-choice-gap: var(--space-4);
    --multiple-choice-option-border-radius: 8px;
    --card-border-radius: var(--radius-container);
    --card-padding: 20px;
    --card-background-color: var(--surface);
    --card-color: var(--text);
    --card-media-background-color: var(--surface-raised);
    --card-description-color: var(--text-muted);
    --badge-border-radius: var(--radius-control);
    --badge-font-weight: 500;
    --swatch-height: 44px;
    --swatch-border-radius: 8px;
    --swatch-hover-scale: 1.12;
    --swatch-ring-width: 2px;
    --table-head-text: var(--text-muted);
    --table-border: var(--line);
    --table-row-hover: var(--surface-raised);
    --table-positive: var(--success);
    --table-negative: var(--danger);
    --breadcrumb-font-size: var(--text-sm);
    --breadcrumb-gap: 8px;
    --breadcrumb-link: var(--text-muted);
    --breadcrumb-link-hover: var(--text);
    --breadcrumb-current: var(--text);
    --breadcrumb-current-font-weight: 500;
    --breadcrumb-separator: var(--line);
    --breadcrumb-border-radius: var(--radius-sm);
    --code-block-background-color: var(--palette-slate-950);
    --code-block-color: var(--palette-slate-300);
    --code-block-comment: var(--palette-slate-400);
    --code-block-string: var(--palette-sky-300);
    --code-block-keyword: var(--palette-red-400);
    --code-block-number: var(--palette-blue-400);
    --code-block-property: var(--palette-blue-400);
    --code-block-operator: var(--palette-red-400);
    --code-block-punctuation: var(--palette-slate-400);
    --code-block-tag: var(--palette-emerald-300);
    --code-block-attribute: var(--palette-sky-400);
    --code-block-blade: var(--palette-violet-300);
    --figure-border: var(--line);
    --figure-background-color: var(--surface-raised);
    --figure-border-radius: 0;
    --figure-border-radius-rounded: 12px;
    --figure-caption-color: var(--text-muted);
    --figure-caption-font-size: var(--text-sm);
    --figure-caption-gap: 8px;
    --figure-caption-align: start;
    --embed-background-color: var(--surface-raised);
    --tabs-tab-text: var(--text-muted);
    --tabs-tab-text-hover: var(--text);
    --tabs-tab-text-active: var(--text);
    --tabs-tab-hover-background-color: var(--neutral-200);
    --tabs-line: var(--line);
    --tabs-accent: var(--primary);
    --tabs-pill-track: var(--neutral-200);
    --tabs-pill-active: var(--neutral-50);
    --tabs-vertical-strip: 13rem;
    --tabs-vertical-gap: 18px;
    --details-border-radius: var(--radius);
    --details-border: var(--line);
    --details-background-color: var(--surface-raised);
    --details-color: var(--text);
    --details-padding-y: 12px;
    --details-padding-x: 14px;
    --details-summary-font-size: var(--text-sm);
    --details-summary-hover: var(--surface);
    --details-marker-color: var(--text-muted);
    --details-focus-ring: var(--primary);
    --details-body-color: var(--text-muted);
    --details-body-font-size: var(--text-sm);
    --accordion-line: var(--line);
    --accordion-color: var(--text);
    --accordion-focus-ring: var(--primary);
    --accordion-border-radius: var(--radius-control);
    --accordion-icon-color: var(--primary);
    --accordion-marker-color: var(--text-muted);
    --accordion-body-color: var(--text);
    --dialog-width: 512px;
    --dialog-border-radius: 8px;
    --dialog-padding: 16px;
    --dialog-background-color: var(--surface-raised);
    --dialog-color: var(--text);
    --dialog-title-font-size: var(--text-base);
    --dialog-shadow: var(--shadow-lg);
    --dialog-backdrop: var(--overlay);
    --dialog-close-color: var(--text-muted);
    --dialog-close-background-color-hover: var(--line);
    --dialog-focus-ring: var(--primary);
    --drawer-width: 360px;
    --drawer-border-radius: 12px;
    --drawer-padding: 20px;
    --drawer-background-color: var(--surface-raised);
    --drawer-color: var(--text);
    --drawer-title-font-size: var(--text-base);
    --drawer-backdrop: var(--overlay);
    --drawer-shadow: var(--shadow-lg);
    --drawer-close-color: var(--text-muted);
    --drawer-close-background-color-hover: var(--line);
    --drawer-focus-ring: var(--primary);
    --drawer-duration: 240ms;
    --drawer-ease: cubic-bezier(0.2, 0.8, 0.2, 1);
    --dropdown-surface: var(--neutral-50);
    --dropdown-line: var(--line);
    --dropdown-border-radius: var(--radius-control);
    --dropdown-min-width: 12rem;
    --dropdown-shadow: var(--shadow-lg);
    --dropdown-item-text: var(--text);
    --dropdown-item-text-muted: var(--text-muted);
    --dropdown-item-hover-background-color: var(--neutral-200);
    --dropdown-danger-text: var(--danger);
    --emoji-picker-border-radius: var(--radius-lg);
    --emoji-picker-surface: var(--surface-raised);
    --emoji-picker-border: var(--line);
    --emoji-picker-width: 352px;
    --emoji-picker-grid-height: 260px;
    --emoji-picker-cell-size: 40px;
    --emoji-picker-cell-border-radius: var(--radius);
    --emoji-picker-cell-hover: var(--surface);
    --emoji-picker-cell-selected: var(--primary);
    --emoji-picker-focus-ring: var(--primary);
    --tree-indent: 16px;
    --tree-row-height: 28px;
    --tree-border-radius: 6px;
    --tree-row-hover-background-color: var(--neutral-200);
    --tree-row-selected-background-color: var(--neutral-200);
    --tree-icon: var(--text-muted);
    --tree-twist: var(--text-muted);
    --tree-status-new: var(--info);
    --tree-status-updated: var(--text-muted);
    --tree-status-deleted: var(--danger);
    --tooltip-background-color: var(--neutral-900);
    --tooltip-text: var(--neutral-50);
    --progress-height: 8px;
    --progress-width: 100%;
    --progress-border-radius: var(--radius-full);
    --progress-track: var(--surface-raised);
    --progress-value: var(--primary);
    --progress-focus-ring: var(--primary);
    --skeleton-fill: var(--line);
    --skeleton-border-radius: var(--radius-control);
    --flowchart-background-color: var(--neutral-100);
    --flowchart-node-background-color: var(--neutral-50);
    --flowchart-node-border: var(--neutral-300);
    --flowchart-node-text: var(--neutral-900);
    --flowchart-note-text: var(--neutral-500);
    --flowchart-edge: var(--neutral-400);
    --flowchart-edge-label: var(--neutral-500);
    --flowchart-accent-background-color: var(--primary-600);
    --flowchart-accent-text: var(--on-primary);
    --theme-picker-border-radius: 8px;
    --footer-padding: 2rem 1rem;
    --footer-gap: 1rem;
}
:root[data-theme="light"], [data-theme-scope="light"] {
    --page-bg: var(--neutral-100);
    --surface: #ffffff;
    --surface-raised: var(--neutral-50);
    --surface-muted: var(--neutral-50);
    --overlay: color-mix(in srgb, var(--neutral-100) 75%, transparent);
    --text: var(--neutral-900);
    --text-muted: var(--neutral-600);
    --paper: #ffffff;
    --ink: #000000;
    --success: var(--palette-green-700);
    --success-surface: var(--palette-green-100);
    --danger: var(--palette-red-700);
    --danger-surface: var(--palette-red-100);
    --warning: var(--palette-amber-700);
    --warning-surface: var(--palette-amber-100);
    --info: var(--palette-blue-700);
    --info-surface: var(--palette-blue-100);
    --text-danger: var(--danger);
    --line: var(--neutral-300);
    --line-strong: var(--neutral-400);
    --primary: var(--primary-500);
    --on-primary: var(--neutral-50);
    --link: var(--primary-700);
    --scrollbar-thumb: var(--neutral-300);
    --scrollbar-thumb-hover: var(--neutral-400);
    --scrollbar-track: transparent;
    --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
    --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
    --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
    --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
    --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
    --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
    --dev-tools-pill-info-text: var(--info);
    --dev-tools-pill-info-background-color: var(--info-surface);
    --dev-tools-pill-info-solid-background-color: var(--info);
    --dev-tools-pill-info-solid-text: var(--info-surface);
    --dev-tools-pill-warn-text: var(--warning);
    --dev-tools-pill-warn-background-color: var(--warning-surface);
    --dev-tools-pill-warn-solid-background-color: var(--warning);
    --dev-tools-pill-warn-solid-text: var(--warning-surface);
    --dev-tools-pill-error-text: var(--danger);
    --dev-tools-pill-error-background-color: var(--danger-surface);
    --dev-tools-pill-error-solid-background-color: var(--danger);
    --dev-tools-pill-error-solid-text: var(--danger-surface);
    --card-border: var(--neutral-200);
    --dialog-border: var(--neutral-200);
    --drawer-border: var(--neutral-200);
}
:root[data-theme="dark"], [data-theme-scope="dark"] {
    --page-bg: var(--neutral-950);
    --surface: var(--neutral-900);
    --surface-raised: var(--neutral-800);
    --surface-muted: var(--neutral-800);
    --overlay: color-mix(in srgb, var(--neutral-800) 75%, transparent);
    --text: var(--neutral-50);
    --text-muted: var(--neutral-400);
    --paper: #ffffff;
    --ink: #000000;
    --success: var(--palette-green-400);
    --success-surface: var(--palette-green-950);
    --danger: var(--palette-red-400);
    --danger-surface: var(--palette-red-950);
    --warning: var(--palette-amber-400);
    --warning-surface: var(--palette-amber-950);
    --info: var(--palette-blue-400);
    --info-surface: var(--palette-blue-950);
    --text-danger: var(--danger);
    --line: var(--neutral-700);
    --line-strong: var(--neutral-600);
    --primary: var(--primary-500);
    --on-primary: var(--neutral-50);
    --link: var(--primary-400);
    --scrollbar-thumb: var(--neutral-700);
    --scrollbar-thumb-hover: var(--neutral-600);
    --scrollbar-track: transparent;
    --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
    --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.5), 0 1px 2px -1px rgb(0 0 0 / 0.5);
    --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.5), 0 2px 4px -2px rgb(0 0 0 / 0.5);
    --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.6), 0 4px 6px -4px rgb(0 0 0 / 0.6);
    --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.6), 0 8px 10px -6px rgb(0 0 0 / 0.6);
    --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.7);
    --flow-node-background-color: var(--neutral-900);
    --flow-node-border: var(--neutral-700);
    --flow-node-text: var(--neutral-50);
    --flow-note-text: var(--neutral-400);
    --flow-edge: var(--neutral-600);
    --dev-tools-pill-info-text: var(--info);
    --dev-tools-pill-info-background-color: var(--info-surface);
    --dev-tools-pill-info-solid-background-color: var(--info);
    --dev-tools-pill-info-solid-text: var(--info-surface);
    --dev-tools-pill-warn-text: var(--warning);
    --dev-tools-pill-warn-background-color: var(--warning-surface);
    --dev-tools-pill-warn-solid-background-color: var(--warning);
    --dev-tools-pill-warn-solid-text: var(--warning-surface);
    --dev-tools-pill-error-text: var(--danger);
    --dev-tools-pill-error-background-color: var(--danger-surface);
    --dev-tools-pill-error-solid-background-color: var(--danger);
    --dev-tools-pill-error-solid-text: var(--danger-surface);
    --financials-ledger-chip-empty: var(--neutral-600);
    --card-border: var(--neutral-800);
    --tabs-tab-hover-background-color: var(--neutral-800);
    --tabs-pill-track: var(--neutral-900);
    --tabs-pill-active: var(--neutral-800);
    --dialog-border: var(--neutral-700);
    --drawer-border: var(--neutral-700);
    --dropdown-surface: var(--neutral-900);
    --dropdown-item-hover-background-color: var(--neutral-800);
    --tree-row-hover-background-color: var(--neutral-800);
    --tree-row-selected-background-color: var(--neutral-800);
    --tooltip-background-color: var(--neutral-100);
    --tooltip-text: var(--neutral-900);
    --flowchart-background-color: var(--neutral-950);
    --flowchart-node-background-color: var(--neutral-900);
    --flowchart-node-border: var(--neutral-700);
    --flowchart-node-text: var(--neutral-50);
    --flowchart-note-text: var(--neutral-400);
    --flowchart-edge: var(--neutral-600);
    --flowchart-edge-label: var(--neutral-400);
}
/* @toc Base */
/*
 * Base — the document skeleton (html/body as a flex column) and the one generic
 * text helper. Layout moved to its proper homes: the public shell is app-page.css,
 * the authed shell is app-shell.css, and page-specific styles live in pages/.
 */

html,
body {
    padding: 0;
}

body {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    background: var(--page-bg);
    color: var(--text);
    -webkit-font-smoothing: antialiased;
}

/* Themed scrollbars — ONE base treatment for every overflow container. scrollbar-color inherits, but
 * scrollbar-width does NOT — so both go on * to reach every scroller (thin + themed everywhere, not
 * just the root). Modern engines use these standard properties (MDN-anchored); the ::-webkit-scrollbar
 * fallback below gives older Chromium/Safari the rounded thumb + hover polish. Ramp-tokened
 * (config/theme.php) → flips per theme + tenant. Opt a scroller OUT with scrollbar-width: none (see
 * tabs' nav and the dev-tools chip rows). */
* {
    scrollbar-width: thin;
    scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}

::-webkit-scrollbar {
    width: 10px;
    height: 10px;
}

::-webkit-scrollbar-track {
    background: var(--scrollbar-track);
}

::-webkit-scrollbar-thumb {
    background: var(--scrollbar-thumb);
    border-radius: 999px;
}

::-webkit-scrollbar-thumb:hover {
    background: var(--scrollbar-thumb-hover);
}

.muted {
    color: var(--text-muted);
    font-size: var(--text-sm);
}

/* Keyboard focus — one consistent ring on every focusable control. :where() keeps
 * it zero-specificity, so a component can still draw its own (e.g. .button) without
 * fighting this. A real outline (not box-shadow) so it survives forced-colors mode.
 * tabindex="-1" targets (skip-link's main, modal panels) are programmatic focus,
 * not keyboard stops, so they're excluded. */
:where(a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])):focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

/* Suppress the browser-default outline on non-keyboard focus (mouse / programmatic) -
 * Safari and some Chromium builds draw a stray blue ring there, e.g. tracing the
 * scrollable sidebar list against its divider. The keyboard :focus-visible ring above
 * is untouched, so this only removes the default, never the intentional one. */
:focus:not(:focus-visible) {
    outline: none;
}

/* Visually hidden, still read by assistive tech — for labels and aria-live regions. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* THE SHARED LABEL RECIPE (R5-B, James 2026-07-14 — PROVISIONAL). One place for the small-heading label
   treatment every label surface shares: table column headers, dropdown/menu group labels, docs props-table
   headers, planner meta labels. Consumers apply class="ui-label" (ticket B migrates them), so the treatment
   lives in ONE rule.

   Ruled to Nuxt's NORMAL-CASE 14/600 --text, replacing the old small-caps "eyebrow" (uppercase + tracking +
   muted). James ruled this "for now, not married", so the REVERSIBILITY RIDER is load-bearing:

     >>> To revert to the small-caps eyebrow, this is the ONE edit — swap .ui-label's four body lines for:
     >>>     font-size: var(--text-xs); font-weight: 700; text-transform: uppercase;
     >>>     letter-spacing: 0.05em; color: var(--text-muted);
     >>> No consumer changes — every label surface flips at once. That single-edit reversibility IS the ruling. */
.ui-label {
    font-size: var(--text-sm);   /* 14px */
    font-weight: 600;
    text-transform: none;        /* normal-case (was: uppercase) */
    letter-spacing: normal;      /* (was: ~0.05em tracking) */
    color: var(--text);          /* (was: --text-muted) */
}

/* A keyboard key, styled as a physical keycap — the Key column of the shared keyboard-table
   partial (/docs + /accessibility). Ramp tokens only, so it flips per theme. */
kbd {
    display: inline-block;
    padding: 1px 7px;
    font-size: 0.85em;
    line-height: 1.6;
    color: var(--text);
    background: var(--surface-raised);
    border: 1px solid var(--line);
    border-radius: 5px;
    box-shadow: 0 1px 0 var(--line);
}

/* The em-dash placeholder for a component with no keyboard interaction (e.g. a presentational callout). */
.kbd-none {
    color: var(--text-muted);
}

/* @toc Accordion */
/*
 * x-accordion — APG Accordion primitive. STRUCTURAL styling (the decorative skin waits on the direction pick;
 * the FAQ section refines the frame/padding): the a11y-critical focus ring, the divided-row list, the chevron,
 * and the reduced-motion-guarded open/close. A single-column vertical stack that never reflows on its own box
 * width, so — per the Section-Authoring Law — it needs NO @container and NO viewport @media (the layout simply
 * does not vary by width; only text wraps). Colour is semantic tokens only. --accordion-* vars come from the
 * css facet; the open/close duration comes from the JS island (accordion.js sets --accordion-duration), with a
 * 200ms fallback here.
 */

.accordion-header {
    margin: 0;
    /* Every header carries a top rule: the first is the list's top edge, the rest are the dividers between
       items (spec: rows split by 1px --line, last row unruled — no bottom rules anywhere). */
    border-block-start: 1px solid var(--accordion-line, var(--line));
}

.accordion-trigger {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    width: 100%;
    padding-block: var(--space-4);
    padding-inline: 0;
    border: none;
    background: transparent;
    color: var(--accordion-color, var(--text));
    font-size: var(--text-base);
    font-weight: 500;
    line-height: 1.4;
    text-align: start;
    cursor: pointer;
}

.accordion-trigger:hover {
    /* A whisper-subtle neutral tint — no literal colour, so it stays theme-safe (color-mix on a token). */
    background: color-mix(in srgb, var(--text) 4%, transparent);
}

.accordion-trigger:focus-visible {
    outline: 2px solid transparent;
    outline-offset: -2px;
    box-shadow: inset 0 0 0 2px var(--accordion-focus-ring, var(--primary));
    border-radius: var(--accordion-border-radius, var(--radius-control));
}

.accordion-icon {
    display: inline-flex;
    flex: none;
    color: var(--accordion-icon-color, var(--primary));
}

.accordion-icon svg {
    width: 1.25rem;
    height: 1.25rem;
}

.accordion-title {
    flex: 1;
    min-width: 0;
}

.accordion-marker {
    display: inline-flex;
    flex: none;
    margin-inline-start: var(--space-2);
    color: var(--accordion-marker-color, var(--text-muted));
    transition: transform var(--accordion-duration, 200ms) ease;
}

.accordion-marker svg {
    width: 1.25rem;
    height: 1.25rem;
}

.accordion-trigger[aria-expanded="true"] .accordion-marker {
    transform: rotate(180deg);
}

/* The panel animates open/close via a grid-template-rows 0fr → 1fr tween (the modern unknown-height technique).
   accordion.js adds/removes .is-open and toggles the `hidden` attribute at the animation boundary (a collapsed
   panel is `hidden` = removed from the AT tree, per APG). */
.accordion-panel {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows var(--accordion-duration, 200ms) ease;
}

/* A collapsed panel is `hidden` — removed from layout AND the AT tree. This explicit rule is REQUIRED because
   the author `display: grid` above otherwise overrides the UA [hidden]{display:none}, leaving a clipped sliver
   of the panel visible. The JS un-hides BEFORE animating open and re-hides AFTER the collapse tween, so a panel
   is never `hidden` mid-animation — this never fights the open/close machinery. */
.accordion-panel[hidden] {
    display: none;
}

.accordion-panel.is-open {
    grid-template-rows: 1fr;
}

.accordion-panel-inner {
    overflow: hidden;
    padding-block-end: var(--space-4);
    color: var(--accordion-body-color, var(--text));
}

.accordion-panel-inner > :first-child {
    margin-block-start: 0;
}

/* Reduced motion: instant open/close and no chevron spin. The global animations.css blanket already zeroes all
   transitions; this local block states the intent for honesty (the dialog.css precedent). */
@media (prefers-reduced-motion: reduce) {
    .accordion-panel,
    .accordion-marker {
        transition: none;
    }
}

/* @toc Annotated text */
/*
 * Annotated text — the §1 reader runs-renderer. The block is inline body text; a REVEALED issue-run is a
 * <button> mark: a tinted, underlined phrase carrying a superscript issue number. Three cues, never colour
 * alone: number + underline + tint (forced-colors strips the tint but keeps the number + underline). The two
 * hues come STRAIGHT from the emitted annotation tokens (PaletteService::annotationVars, AA-vetted both
 * themes; NEVER hand-authored — a per-component hue is a contract violation):
 *   --annotation-<n>          the STRONG hue → the underline + the marker number.
 *   --annotation-<n>-surface  the span TINT fill.
 * Anatomy dims are --annotated-text-* config, with fallbacks here so the structure renders before the domain
 * manifest emits them; the currentColor token fallbacks are a belt-and-braces net (the tokens are always bundled).
 */

.annotated-text {
    line-height: var(--annotated-text-line-height, 1.7);
    color: var(--text);
}

.annotated-text-p {
    margin-block: 0 var(--annotated-text-paragraph-gap, 1em);
}

.annotated-text-p:last-child {
    margin-block-end: 0;
}

/* Direction B's deposition-style margin paragraph-number; rendered only when numbered-paragraphs is on. */
.annotated-text-pnum {
    display: inline-block;
    min-width: 1.5em;
    margin-inline-end: 0.5em;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
    user-select: none;
}

/* The revealed mark — a <button> reset to inline text; the tint + underline live on the inner text span so
   the underline hugs the phrase, not the trailing superscript number. */
.annotated-text-mark {
    display: inline;
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
    text-align: inherit;
    cursor: pointer;
    appearance: none;
    -webkit-appearance: none;
}

/* Slot hue + tint (1..6), each from its emitted annotation token pair. currentColor / transparent fallbacks =
   a visible neutral mark if a token is ever absent. */
.annotated-text-mark[data-annotation="1"] {
    --_hue: var(--annotation-1, currentColor);
    --_surface: var(--annotation-1-surface, transparent);
}

.annotated-text-mark[data-annotation="2"] {
    --_hue: var(--annotation-2, currentColor);
    --_surface: var(--annotation-2-surface, transparent);
}

.annotated-text-mark[data-annotation="3"] {
    --_hue: var(--annotation-3, currentColor);
    --_surface: var(--annotation-3-surface, transparent);
}

.annotated-text-mark[data-annotation="4"] {
    --_hue: var(--annotation-4, currentColor);
    --_surface: var(--annotation-4-surface, transparent);
}

.annotated-text-mark[data-annotation="5"] {
    --_hue: var(--annotation-5, currentColor);
    --_surface: var(--annotation-5-surface, transparent);
}

.annotated-text-mark[data-annotation="6"] {
    --_hue: var(--annotation-6, currentColor);
    --_surface: var(--annotation-6-surface, transparent);
}

.annotated-text-mark-text {
    padding-inline: 0.1em;
    border-bottom: var(--annotated-text-underline-width, 2px) solid var(--_hue, currentColor);
    border-radius: var(--annotated-text-span-border-radius, 3px) var(--annotated-text-span-border-radius, 3px) 0 0;
    /* The vetted span tint; linear-gradient(colour, colour) is the cross-engine mark-fill trick. */
    background-image: linear-gradient(var(--_surface, transparent), var(--_surface, transparent));
}

.annotated-text-mark-num {
    margin-inline-start: 0.1em;
    color: var(--_hue, currentColor);
    font-size: var(--annotated-text-marker-font-size, 0.6875rem);
    font-weight: var(--annotated-text-marker-font-weight, 600);
    line-height: 1;
    vertical-align: super;
    /* Keep the decorative marker digit out of selection/copy (the pnum precedent), so verbatim copy stays clean. */
    user-select: none;
}

/* Use the GLOBAL :focus-visible ring (base.css); only nudge the offset so it clears the tinted phrase. */
.annotated-text-mark:focus-visible {
    outline-offset: 2px;
    border-radius: var(--annotated-text-span-border-radius, 3px);
}

/* Forced-colors: the OS strips the tint background — keep the number + underline as the surviving cues. */
@media (forced-colors: active) {
    .annotated-text-mark-text {
        border-bottom-color: currentColor;
    }

    .annotated-text-mark-num {
        color: currentColor;
    }
}

/* @toc App header bar */
/* App header bar — the page-title strip at the top of the app-shell content
 * area, above the scrollable main (a sibling of app-shell-main inside
 * app-shell-wrap, so it stays put while main scrolls). Title left, optional
 * subtitle under it, optional actions slot right. Horizontal padding matches
 * app-shell-main so the title aligns with page content.
 *
 * Cribbed pared-down from leadthink's app-header-bar. Used by
 * /resources/views/components/app-shell.blade.php.
 */

.app-header-bar {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    min-height: 60px;
    padding: 12px 32px;
    background: var(--page-bg);
    border-bottom: 1px solid var(--line);
}

.app-header-bar-content {
    display: flex;
    flex-direction: column;
    gap: 3px;
}

.app-header-bar-title {
    margin: 0;
    font-size: var(--text-xl);
    font-weight: 600;
    color: var(--text);
}

.app-header-bar-subtitle {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--text-muted);
}

.app-header-bar-actions {
    display: flex;
    align-items: center;
    gap: 10px;
}

@media (max-width: 768px) {
    .app-header-bar {
        min-height: 0;
        padding: 14px 16px;
    }

    .app-header-bar-title {
        font-size: var(--text-lg);
    }
}

/* @toc App page */
/* Public shell layout — a topbar (logo / login row, right-aligned) above a
 * centered main. The shell for the public pages (landing, login); the authed app
 * uses app-shell.css instead. Used by /resources/views/components/app-page.blade.php. */

.topbar {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    padding: 18px 24px;
}

.topbar-nav {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 14px;
}

.topbar-nav form {
    margin: 0;
}

.main {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: safe center;
    padding: 24px;
}

@media (max-width: 768px) {
    .topbar {
        padding: 14px 16px;
    }

    .main {
        padding: 20px 16px;
    }
}

/* @toc App shell */
/* App shell — sidebar (left) + scrollable main (right) on desktop. On mobile
 * (<= 768px) the sidebar becomes an off-canvas drawer: a sticky top bar with a
 * hamburger (toggled by sidebar-nav.js -> body.nav-open) slides it in over a
 * backdrop, and the page scrolls naturally instead of locking main. On
 * <body class="app-shell">. */

/* Skip-to-content link — the first focusable element on every shell. Parked
 * off-screen until focused, then pinned top-left. Shared by app-shell and app-page
 * (one global bundle), targets the <main id="main-content">. */
.skip-link {
    position: absolute;
    left: -9999px;
    top: 0;
    z-index: var(--z-overlay);
}

.skip-link:focus {
    left: 8px;
    top: 8px;
    padding: 8px 12px;
    background: var(--page-bg);
    color: var(--text);
    border: 1px solid var(--line);
    border-radius: var(--radius);
}

body.app-shell {
    flex-direction: row;
    height: 100vh;
    overflow: hidden;
}

.app-shell-wrap {
    flex: 1;
    min-width: 0;
    height: 100vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.app-shell-main {
    flex: 1;
    overflow-y: auto;
    padding: 28px 32px;
}

/* FLUSH (1209): a full-bleed page (the editor workspace) owns its own edges — no shell padding, and the shell
   itself does not scroll (the page's own columns do). Opt-in via <x-app-shell :flush="true">. */
.app-shell-main-flush {
    padding: 0;
    overflow: hidden;
}

/* Mobile-only chrome — hidden until the drawer breakpoint. */
.app-shell-bar,
.app-shell-backdrop {
    display: none;
}

/* THE COMPACT LAYOUT APPLIES WHEN THE VIEWPORT IS NARROW **OR** SHORT (the comma is OR).
 *
 * It used to be width-only, which meant the shell asked "is this narrow?" and treated everything else as a
 * desktop. A phone held sideways is 844x390: not narrow, so it got the full desktop layout — a 240px sidebar
 * docked over the game, nine nav items on a screen whose ruling is that it shows two words. Every
 * authenticated page had this, on every phone in landscape; the play screen is only where it hurt most.
 *
 * Stated the other way round, which is the rule: THE DESKTOP LAYOUT REQUIRES BOTH AXES — wide AND tall. A
 * short viewport is a phone no matter how wide it is, and it wants the drawer.
 *
 * Measured either side of the 600px line (see config/css/global.php for the full table):
 *     844 x 390   iPhone landscape         → compact   390 is 210px below the line
 *     932 x 430   Pro Max landscape        → compact   430 is 170px below
 *     1024 x 768  iPad landscape           → desktop   768 is 168px above
 *     1280 x 800  laptop                   → desktop   unchanged
 *
 * A desktop window dragged shorter than 600px also gets the drawer. That is deliberate and not a
 * side-effect: at that height the docked sidebar costs more than it gives, which is the same reason a
 * landscape phone should not have it. */
@media (max-width: 768px), (max-height: 600px) {
    body.app-shell {
        flex-direction: column;
        height: auto;
        min-height: 100vh;
        /* overflow-x guard: the drawer is parked off the left edge (translateX
         * -100%), so clamp horizontal scroll while leaving the page scrollable. */
        overflow-x: hidden;
        overflow-y: visible;
    }

    /* Lock background scroll while the drawer is open so content can't bleed
     * behind the backdrop. */
    body.app-shell.nav-open {
        overflow: hidden;
    }

    .app-shell-bar {
        display: flex;
        align-items: center;
        gap: 12px;
        position: sticky;
        top: 0;
        z-index: 30;
        padding: 10px 14px;
        background: var(--page-bg);
        border-bottom: 1px solid var(--line);
    }

    .app-shell-menu {
        appearance: none;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 38px;
        height: 38px;
        padding: 0;
        border: 1px solid var(--line);
        border-radius: 8px;
        background: transparent;
        color: var(--text);
        cursor: pointer;
    }

    .app-shell-menu svg {
        width: 20px;
        height: 20px;
    }

    .app-shell-brand {
        display: inline-flex;
        line-height: 0;
    }

    .app-shell-brand .brand-logo svg {
        width: 30px;
    }

    .sidebar-nav {
        position: fixed;
        top: 0;
        bottom: 0;
        left: 0;
        width: 264px;
        max-width: 82vw;
        transform: translateX(-100%);
        /* Off-canvas: hidden from tab order / AT until opened (visibility flips
         * after the slide-out so the close still animates), like the backdrop. */
        visibility: hidden;
        transition: transform 220ms ease, visibility 0s linear 220ms;
        z-index: 50;
    }

    body.nav-open .sidebar-nav {
        transform: translateX(0);
        visibility: visible;
        transition: transform 220ms ease;
    }

    .app-shell-backdrop {
        display: block;
        position: fixed;
        inset: 0;
        z-index: 40;
        background: rgba(0, 0, 0, 0.5);
        opacity: 0;
        visibility: hidden;
        transition: opacity 220ms ease, visibility 0s linear 220ms;
    }

    body.nav-open .app-shell-backdrop {
        opacity: 1;
        visibility: visible;
        transition: opacity 220ms ease;
    }

    .app-shell-wrap {
        height: auto;
        overflow: visible;
    }

    .app-shell-main {
        padding: 20px 16px;
    }
}

/* @toc Badge */
/* Badge — a small, static status/label chip: the promotion of the bare .badge utility (table.css, R4)
 * into a real primitive, grounded on Harry's badge.md extraction. A rounded-rect on the ruled
 * --radius-control (6px, not the old 999px pill), font-weight 500 (Nuxt font-medium; the util was 600).
 * Colours are the categorical status tokens + a themed neutral; variants are soft (the AA-vetted
 * --<c>-surface + --<c> pair) and outline (--<c> text + a 1px ring). Static + decorative — no states,
 * no JS (badge.md §5-8). Config: config/components/badge.php (--badge-border-radius / --badge-font-weight).
 * View: badge.blade.php.
 *
 * CLASS NAMESPACE — `.ui-badge*`, NOT `.badge`: the legacy `.badge` util (+ .badge-success/.badge-secondary)
 * still lives in table.css for its 7 hand-rolled sites (the migration is a separate, later ticket, fenced
 * here). Sharing `.badge` would collide — the util loads after this sheet (alphabetical glob) and would
 * override it, AND would restyle those 7 bare-util sites. A distinct namespace keeps the primitive correct
 * and the legacy sites untouched; the migration ticket can reclaim `.badge` once the util is removed. */

.ui-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-1) var(--space-2);   /* md: 4px / 8px (extraction: py-1 px-2) */
    border-radius: var(--badge-border-radius, var(--radius-control));   /* 6px; fallback so it renders before the config emits */
    font-size: var(--text-xs);                 /* md: 12px */
    font-weight: var(--badge-font-weight, 500);   /* Nuxt font-medium; fallback per the house convention */
    line-height: 1;
    white-space: nowrap;
    vertical-align: middle;
}

/* Optional leading glyph — inherits the badge text colour, sized to the badge. */
.ui-badge-icon {
    display: inline-flex;
    flex-shrink: 0;
}

.ui-badge-icon svg {
    display: block;
    width: 14px;
    height: 14px;
}

/* --- Colours: each sets --badge-fg (text + ring) and --badge-surface (the soft fill), from the
       categorical status tokens (AA-vetted --<c> / --<c>-surface) + a themed neutral. The variant decides
       how they apply, so colour and variant COMPOSE (custom props resolve per element, order-free). --- */
.ui-badge-neutral {
    --badge-fg: var(--text-muted);
    --badge-surface: var(--surface-muted);
}

.ui-badge-success {
    --badge-fg: var(--success);
    --badge-surface: var(--success-surface);
}

.ui-badge-warning {
    --badge-fg: var(--warning);
    --badge-surface: var(--warning-surface);
}

.ui-badge-danger {
    --badge-fg: var(--danger);
    --badge-surface: var(--danger-surface);
}

.ui-badge-info {
    --badge-fg: var(--info);
    --badge-surface: var(--info-surface);
}

/* --- Variants --- */
/* soft: the tinted, self-contained AA-vetted pair (--<c> text on --<c>-surface). */
.ui-badge-soft {
    color: var(--badge-fg);
    background: var(--badge-surface);
}

/* outline: --<c> text (4.5:1 on the light surfaces) + a 1px inset --<c> accent ring, transparent fill.
   A box-shadow ring adds no layout box, so the 1px never nudges the text. */
.ui-badge-outline {
    color: var(--badge-fg);
    background: transparent;
    box-shadow: inset 0 0 0 1px var(--badge-fg);
}

/* --- Sizes (Harry's scale, house-scoped to sm / md / lg; md is the base rule above). The radius is the
       same ruled 6px across sizes — only the text, padding, gap, and icon scale. --- */
.ui-badge-sm {
    gap: 3px;
    padding: 2px 6px;
    font-size: var(--text-2xs);   /* 10px */
}

.ui-badge-sm .ui-badge-icon svg {
    width: 12px;
    height: 12px;
}

.ui-badge-lg {
    gap: 6px;
    padding: var(--space-1) 10px;   /* 4px / 10px */
    font-size: var(--text-sm);      /* 14px */
}

.ui-badge-lg .ui-badge-icon svg {
    width: 16px;
    height: 16px;
}

/* @toc Barbruiser create */
/* BarBruiser create widget — the prompt block + the paste/drop JSON form. Theme tokens
 * throughout. View: barbruiser-create.blade.php; behaviour: barbruiser-create.js. */

.barbruiser-create {
    max-width: var(--max-w-3xl);
}

.barbruiser-create-step {
    margin-bottom: 28px;
}

.barbruiser-create-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
}

.barbruiser-create-h {
    margin: 0 0 4px;
    font-size: var(--text-lg);
    font-weight: 600;
    color: var(--text);
}

.barbruiser-create-prompt {
    max-height: 320px;
    overflow: auto;
    margin-top: 10px;
    white-space: pre-wrap;
    word-break: break-word;
}

/* Drop zone — a dashed target that also opens the file picker on tap (it wraps a
 * hidden file input), so it works on desktop drag and mobile alike. */
.barbruiser-drop {
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 22px;
    margin-bottom: 12px;
    border: 2px dashed var(--line);
    border-radius: var(--radius-lg);
    background: var(--surface-raised);
    color: var(--text-muted);
    font-size: var(--text-sm);
    cursor: pointer;
    transition: border-color 0.15s ease, color 0.15s ease;
}

.barbruiser-drop:hover,
.barbruiser-drop.is-over {
    border-color: var(--primary);
    color: var(--text);
}

.barbruiser-create-actions {
    margin-top: 12px;
}

/* Type tabs — switch which prompt (essay / theory) is shown + copied. */
.barbruiser-create-tabs {
    display: flex;
    gap: 6px;
    margin: 10px 0;
}

.barbruiser-create-tab {
    padding: 6px 16px;
    border: 1px solid var(--line);
    border-radius: var(--radius-full, 999px);
    background: var(--surface-raised);
    color: var(--text-muted);
    font: inherit;
    font-size: var(--text-sm);
    font-weight: 600;
    cursor: pointer;
}

.barbruiser-create-tab.is-active {
    background: var(--primary);
    border-color: var(--primary);
    color: var(--on-primary);
}

.barbruiser-create-prompt[hidden] {
    display: none;
}

/* @toc Barbruiser page */
/* BarBruiser tenant shell + essay reader (ticket 1016). Light tenant (x-barbruiser-page sets data-theme=light,
   blue primary, slate neutrals). Every colour is a ramp/semantic token; layout uses logical properties + a
   viewport-responsive reader grid. Bundled via CssService's components glob, so it loads on the eightythree host
   during the /barbruiser preview (the MPS approach; a per-host css_namespace is the launch-cutover step). */

body.barbruiser { background: var(--surface); color: var(--text); }

/* Content max-width inside the shared app-shell main (which owns the outer padding). */
.bb-shell { max-width: 72rem; margin-inline: auto; }

/* Brand logo in the shared app-shell sidebar (+ mobile bar). BarBruiser's mark is a WIDE wordmark lockup, so it
   overrides the square 30–40px .brand-logo sizing with height-based sizing (via its own class, not .brand-logo). */
.sidebar-nav-brand .bb-sidebar-logo svg { height: 1.9rem; width: auto; max-width: 100%; display: block; }
.app-shell-brand .bb-sidebar-logo svg { height: 1.5rem; width: auto; display: block; }

/* Same mark, on the public /login page (ticket 1027) — bigger since it stands alone above the card,
   not sharing a bar with other chrome. */
.login-brand .bb-sidebar-logo svg { height: 2.5rem; width: auto; max-width: 100%; display: block; }

/* (The "coming soon" dimming + badge lived here for MBE. MBE went live — it has an index of its own — so the
   nav no longer emits those classes and the rules went with them.) */

/* .bb-logo-acc styles the wordmark accent in the home hero (index.blade.php). */
.bb-logo-acc { color: var(--primary-600); }

/* home */
.bb-hero { padding-block: 2.5rem 1.5rem; max-width: 44rem; }
.bb-hero-title { font-size: clamp(2rem, 5vw, 2.75rem); font-weight: 800; letter-spacing: -0.02em; margin: 0 0 0.5rem; }
.bb-hero-sub { font-size: 1.1rem; color: var(--text-muted); margin: 0; }
.bb-home-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: 1rem; margin-block-start: 1.5rem; }
.bb-home-tile { display: block; padding: 1.5rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-raised); color: var(--text); text-decoration: none; transition: border-color 0.15s, transform 0.15s; }
.bb-home-tile:hover { border-color: var(--primary-500); transform: translateY(-2px); }
.bb-home-tile h2 { margin: 0 0 0.5rem; font-size: 1.3rem; }
.bb-home-tile p { margin: 0 0 1rem; color: var(--text-muted); }
.bb-home-cta { color: var(--primary-600); font-weight: 600; }
.bb-parked { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 0.1rem 0.4rem; vertical-align: middle; }

/* list */
.bb-practice-nav {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin-block-end: var(--space-4);
}

.bb-practice-nav-link,
.ui-badge.bb-reader-taxonomy-pill {
    display: inline-flex;
    flex-shrink: 0;
    align-items: center;
    padding: var(--space-1) var(--space-3);
    font-size: var(--text-sm);
    font-weight: 500;
    color: var(--text-muted);
    background: var(--surface-raised);
    border: 1px solid var(--line);
    border-radius: var(--radius-full);
    text-decoration: none;
    white-space: nowrap;
}

/* Reader taxonomy is the static sibling of #1176's practice links: identical pill presentation without the
   link-only hover/current affordance. The line fits inside a coarse reader block; a genuinely long skill
   wraps inside its own pill rather than creating page-level horizontal scroll. */
.ui-badge.bb-reader-taxonomy-pill {
    max-inline-size: 100%;
    line-height: 1.25;
    white-space: normal;
    overflow-wrap: anywhere;
}

.ui-badge.bb-reader-taxonomy-status {
    color: var(--info);
    background: var(--info-surface);
    border-color: var(--info);
}

.bb-practice-nav-link:hover {
    color: var(--text);
    border-color: var(--primary-600);
}

.bb-practice-nav-link.is-active {
    color: var(--on-primary);
    background: var(--primary-600);
    border-color: var(--primary-600);
}

/* The reader title remains page-owned. This flat line is only labelled, read-only taxonomy followed by the
   existing metadata + phase row; no new surface or selectable Editor child. */
.bb-reader-topbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    min-inline-size: 0;
}

/* Specificity keeps the token gap when the existing page-level topbar rules load after component CSS. */
.essay-topbar.bb-reader-topbar,
.pt-topbar.bb-reader-topbar {
    gap: var(--space-3);
}

.bb-reader-taxonomy {
    display: flex;
    flex: 1 0 100%;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    min-inline-size: 0;
}

.bb-reader-taxonomy-group,
.bb-reader-taxonomy-values {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    min-inline-size: 0;
}

.bb-reader-taxonomy-group {
    flex: 0 0 auto;
    max-inline-size: 100%;
}

.bb-reader-taxonomy-label {
    flex-shrink: 0;
    white-space: nowrap;
}

.bb-reader-topbar > .reveal-phase {
    flex-shrink: 0;
}

.bb-mbe-meta {
    margin-block-end: var(--space-3);
}

.bb-page-head { padding-block: 1.5rem 1rem; }
.bb-page-head h1 { margin: 0 0 0.4rem; font-size: clamp(1.6rem, 4vw, 2.1rem); }
.bb-page-sub { color: var(--text-muted); margin: 0; max-width: 44rem; }
.bb-eyebrow { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--primary-600); font-weight: 700; }
.bb-essay-index { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr)); gap: 1rem; }
.bb-essay-tile { display: flex; flex-direction: column; gap: 0.35rem; padding: 1.25rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-raised); text-decoration: none; color: var(--text); transition: border-color 0.15s; }
.bb-essay-tile:hover { border-color: var(--primary-500); }
.bb-essay-tile-title { font-weight: 700; font-size: 1.05rem; }
.bb-essay-tile-meta { color: var(--text-muted); font-size: 0.85rem; }

/* essay reader */
.bb-back { margin-block: 0.5rem 0; }
.bb-back a { color: var(--text-muted); text-decoration: none; }
.bb-back a:hover { color: var(--text); }
.bb-essay-head { padding-block: 0.5rem 1.5rem; }
.bb-essay-title { margin: 0.25rem 0; font-size: clamp(1.5rem, 4vw, 2rem); }
.bb-essay-sub { color: var(--text-muted); margin: 0; }

.bb-reader { display: grid; grid-template-columns: 1fr; gap: 1.25rem; }
@media (min-width: 60rem) {
    .bb-reader { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); align-items: start; }
    .bb-question { position: sticky; top: 1rem; }
}
.bb-answers { display: grid; grid-template-columns: 1fr; gap: 1.25rem; }
@media (min-width: 48rem) {
    .bb-answers { grid-template-columns: 1fr 1fr; }
}
.bb-lead { font-weight: 600; }
.bb-stem { line-height: 1.6; }
.bb-calls { list-style: none; padding: 0; margin-block: 1rem; display: flex; flex-direction: column; gap: 0.5rem; }
.bb-calls li { font-weight: 600; padding-inline-start: 0.75rem; border-inline-start: 3px solid var(--primary-500); }
.bb-instruction { color: var(--text-muted); font-style: italic; margin-block-start: 1rem; }
.bb-band { font-size: 0.8rem; color: var(--text-muted); margin: 0 0 0.75rem; }
.bb-answer-text { line-height: 1.6; }

/* issue checklist */
.bb-issues { margin-block-start: 2rem; }
.bb-issues-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.75rem; margin-block-end: 1rem; }
.bb-h2 { margin: 0; font-size: 1.35rem; }
.bb-derived { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted); font-weight: 600; }
.bb-filter { display: flex; gap: 0.4rem; }
.bb-issue-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.75rem; }
.bb-issue { padding: 1rem 1.25rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-raised); }
.bb-issue-top { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.6rem; margin-block-end: 0.5rem; }
.bb-weight { font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; padding: 0.15rem 0.5rem; border-radius: var(--radius-sm); }
.bb-weight--must-hit { color: var(--primary-700); background: color-mix(in srgb, var(--primary-500) 14%, var(--surface)); }
.bb-weight--nice-to-have { color: var(--text-muted); background: color-mix(in srgb, var(--neutral-500) 12%, var(--surface)); }
.bb-issue-label { font-weight: 700; }
.bb-call-tag { margin-inline-start: auto; font-size: 0.75rem; color: var(--text-muted); }
.bb-rules { margin: 0.5rem 0 0; padding-inline-start: 1.2rem; line-height: 1.5; }
.bb-rules li { margin-block: 0.2rem; }
.bb-issue-note { margin-block: 0.5rem 0; color: var(--warning); font-size: 0.9rem; }
.bb-seen { margin-block: 0.5rem 0 0; font-size: 0.8rem; color: var(--text-muted); }
.bb-issues-foot { margin-block-start: 1rem; color: var(--text-muted); font-size: 0.85rem; }

/* coming-soon state (MBE tab) — a deliberate, centered "not built yet" state, not a thin note */
.bb-coming-soon { max-width: 40rem; margin-inline: auto; padding-block: clamp(2rem, 8vw, 5rem); text-align: center; }
.bb-coming-soon-badge { display: inline-block; font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--primary-700); background: color-mix(in srgb, var(--primary-500) 14%, var(--surface)); border-radius: var(--radius-sm); padding: 0.3rem 0.7rem; margin-block-end: 1rem; }
.bb-coming-soon-title { font-size: clamp(1.75rem, 5vw, 2.5rem); margin: 0 0 0.75rem; letter-spacing: -0.02em; }
.bb-coming-soon-lead { color: var(--text-muted); font-size: 1.05rem; line-height: 1.6; margin: 0 auto 1.75rem; max-width: 34rem; }
.bb-coming-soon-actions { display: flex; flex-wrap: wrap; gap: 0.75rem; justify-content: center; }

/* @toc Brand logo */
/* Brand logo — sizes the inline tokenized SVG injected by MediaService. */

.brand-logo svg {
    display: block;
    width: 200px;
    max-width: 100%;
    height: auto;
}

/* @toc Breadcrumb */
/* Breadcrumb. A horizontal trail of links ending in the current page. Separators
 * are CSS-only chevrons inserted between items (::before on every item but the
 * first), so they carry no text and never reach the accessibility tree. All
 * --breadcrumb-* tokens come from config/components/breadcrumb.php['css']. */

.breadcrumb {
    font-size: var(--breadcrumb-font-size, var(--text-sm));
}

.breadcrumb-list {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--breadcrumb-gap, 8px);
    margin: 0;
    padding: 0;
    list-style: none;
}

.breadcrumb-item {
    display: flex;
    align-items: center;
    gap: var(--breadcrumb-gap, 8px);
}

/* The separator: a chevron drawn from a border, rotated 45°. Decorative only —
 * it lives in ::before so it never enters the document text or the a11y tree. */
.breadcrumb-item + .breadcrumb-item::before {
    content: "";
    width: 6px;
    height: 6px;
    border-top: 1.5px solid var(--breadcrumb-separator, var(--line));
    border-right: 1.5px solid var(--breadcrumb-separator, var(--line));
    transform: rotate(45deg);
    flex-shrink: 0;
}

.breadcrumb-link {
    color: var(--breadcrumb-link, var(--text-muted));
    text-decoration: none;
    border-radius: var(--breadcrumb-border-radius, var(--radius-sm));
    transition: color 0.15s ease;
}

.breadcrumb-link:hover {
    color: var(--breadcrumb-link-hover, var(--text));
    text-decoration: underline;
    text-underline-offset: 2px;
}

/* Keyboard focus reuses the global :focus-visible ring (base.css); offset a touch
 * so the rounded ring clears the text. */
.breadcrumb-link:focus-visible {
    outline-offset: 3px;
}

.breadcrumb-current {
    color: var(--breadcrumb-current, var(--text));
    font-weight: var(--breadcrumb-current-font-weight, 500);
}

/* @toc Button */
/*
 * Button. Base + variants (primary, ghost) + states (hover, active,
 * focus-visible, disabled). Every token is a --button-* var emitted from
 * config/components/button.php['css'] — this file references only those plus
 * structural rules, so the config is the component's full theming surface.
 */

.button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: var(--button-padding-y) var(--button-padding-x);
    border: 1px solid transparent;
    border-radius: var(--button-border-radius);
    font-size: var(--button-font-size);
    font-weight: var(--button-font-weight);
    line-height: 1;
    text-decoration: none;
    cursor: pointer;
    transition: background 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
}

/* The visible ring is the box-shadow (hugs the rounded border). The transparent
 * outline is a forced-colors fallback: box-shadow is dropped in high-contrast mode,
 * so the outline gets promoted to a system colour and focus stays visible. */
.button:focus-visible {
    outline: 2px solid transparent;
    outline-offset: 2px;
    box-shadow: 0 0 0 3px var(--button-focus-ring);
}

/* primary */
.button-primary {
    background: var(--button-primary-background-color);
    border-color: var(--button-primary-background-color);
    color: var(--button-primary-color);
}

.button-primary:hover {
    background: var(--button-primary-background-color-hover);
    border-color: var(--button-primary-background-color-hover);
}

.button-primary:active {
    background: var(--button-primary-background-color-active);
    border-color: var(--button-primary-background-color-active);
}

/* ghost */
.button-ghost {
    background: transparent;
    border-color: var(--button-ghost-border);
    color: var(--button-ghost-color);
}

.button-ghost:hover {
    border-color: var(--button-ghost-border-hover);
    background: var(--button-ghost-background-color-hover);
}

.button-ghost:active {
    background: var(--button-ghost-background-color-active);
}

/* disabled / inactive — both <button> and <a> */
.button:disabled,
.button.is-disabled {
    opacity: var(--disabled-opacity);
    cursor: not-allowed;
    pointer-events: none;
}

/* @toc Callout */
/* Callout — a note box whose variant colours the left border (and the optional
 * leading icon); the surface and text are theme tokens, so it adapts to light/dark.
 * Variants pull the categorical status tokens (--success/--warning/--info), so the
 * accent is contrast-vetted and self-flips per theme. View: callout.blade.php. */

.callout {
    display: flex;
    gap: 10px;
    margin: 14px 0;
    padding: 11px 15px;
    border-inline-start: 4px solid var(--callout-accent, var(--line));
    border-radius: var(--radius-sm);
    background: var(--surface-raised);
    color: var(--text);
    font-size: var(--text-sm);
    line-height: 1.55;
}

.callout-icon {
    flex-shrink: 0;
    margin-top: 1px;
    color: var(--callout-accent, var(--text-muted));
}

.callout-icon svg {
    display: block;
    width: 18px;
    height: 18px;
}

.callout-body {
    flex: 1;
    min-width: 0;
}

.callout-body :first-child { margin-top: 0; }
.callout-body :last-child { margin-bottom: 0; }

.callout a {
    color: var(--link);
}

.callout-success { --callout-accent: var(--success); }
.callout-warning { --callout-accent: var(--warning); }
.callout-info { --callout-accent: var(--info); }
.callout-note { --callout-accent: var(--text-muted); }

/* @toc Card */
/* Card. All --card-* tokens come from config/components/card.php['css'];
 * --card-border swaps per theme (css.light/dark). */

.card {
    background: var(--card-background-color);
    color: var(--card-color);
    border: 1px solid var(--card-border);
    border-radius: var(--card-border-radius);
    overflow: hidden;
}

.card-media {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    background: var(--card-media-background-color);
}

.card-media img {
    display: block;
    width: 84px;
    height: auto;
}

.card-body {
    padding: var(--card-padding);
}

.card-title {
    margin: 0 0 8px;
    font-size: var(--text-base);
    font-weight: 500;
}

.card-desc {
    margin: 0;
    color: var(--card-description-color);
    font-size: var(--text-sm);
    line-height: 1.5;
}

/* @toc Checkbox */
/*
 * Checkbox / radio. The native <input> is .sr-only (kept for behaviour + a11y);
 * .checkbox-box is the painted control and reacts to the input's state through
 * sibling selectors. Every token is a --checkbox-* var (config/components/
 * checkbox.php['css']) with a global-token fallback, so the config is the full
 * theming surface. Dark-first — colours come from the semantic tokens, which
 * already flip per theme.
 */

.checkbox {
    display: inline-flex;
    align-items: center;
    gap: var(--checkbox-gap, 10px);
    cursor: pointer;
    user-select: none;
    line-height: 1.4;
}

.checkbox-box {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: var(--checkbox-size, 18px);
    height: var(--checkbox-size, 18px);
    border: 1px solid var(--checkbox-border, var(--line));
    border-radius: var(--checkbox-border-radius, 5px);
    background: var(--checkbox-background-color, var(--surface));
    color: var(--checkbox-check, var(--on-primary));
    transition: background 0.15s ease, border-color 0.15s ease;
}

/* The radio is the same box, fully round. */
.checkbox-radio .checkbox-box {
    border-radius: 50%;
}

.checkbox-label {
    color: var(--checkbox-label-color, var(--text));
    font-size: var(--checkbox-font-size, var(--text-sm));
}

/* The tick (checkbox) — hidden until the input is checked. */
.checkbox-check {
    width: 100%;
    height: 100%;
    opacity: 0;
    transition: opacity 0.12s ease;
}

/* The dot (radio) — drawn as a pseudo-element so radios need no inline SVG. */
.checkbox-radio .checkbox-box::after {
    content: "";
    width: var(--checkbox-dot-size, 8px);
    height: var(--checkbox-dot-size, 8px);
    border-radius: 50%;
    background: var(--checkbox-check, var(--on-primary));
    transform: scale(0);
    transition: transform 0.12s ease;
}

/* --- Checked: fill the box with the accent, reveal the mark --- */
.checkbox-input:checked ~ .checkbox-box {
    background: var(--checkbox-checked-background-color, var(--primary));
    border-color: var(--checkbox-checked-background-color, var(--primary));
}

.checkbox-input:checked ~ .checkbox-box .checkbox-check {
    opacity: 1;
}

.checkbox-radio .checkbox-input:checked ~ .checkbox-box::after {
    transform: scale(1);
}

/* --- Hover (only when the row is interactive) --- */
.checkbox:not(.is-disabled):hover .checkbox-box {
    border-color: var(--checkbox-border-hover, var(--text-muted));
}

.checkbox:not(.is-disabled):hover .checkbox-input:checked ~ .checkbox-box {
    border-color: var(--checkbox-checked-background-color, var(--primary));
}

/* --- Keyboard focus — the ring rides the painted box, not the hidden input.
 * Transparent outline is the forced-colors fallback (box-shadow is dropped in
 * high-contrast mode, so the outline gets promoted to a system colour). --- */
.checkbox-input:focus-visible ~ .checkbox-box {
    outline: 2px solid transparent;
    outline-offset: 2px;
    box-shadow: 0 0 0 3px var(--checkbox-focus-ring, var(--primary));
}

/* --- Disabled --- */
.checkbox.is-disabled {
    cursor: not-allowed;
    opacity: var(--disabled-opacity);
}

/* @toc Checklist */
/*
 * Checklist — a labelled native-checkbox list with a computed n/m badge. Extracted from the planner
 * card-open "Acceptance" list (#838, was .cardo-check* in planner-card.css). Colour is semantic tokens
 * only, so it self-flips per theme; the ticked-item dim uses --text-muted, and the row hover is an alpha
 * tint of --text (decoration, not glyph colour), both within the ramp law.
 */

.checklist {
    display: block;
}

.checklist-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2);
    margin-block-end: var(--space-2);
}

.checklist-label {
    margin: 0;
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
}

.checklist-progress {
    font-size: var(--text-xs);
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

.checklist-items {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
}

.checklist-item {
    border-radius: 7px;
    transition: background 100ms ease;
}

.checklist-item:hover {
    background: color-mix(in srgb, var(--text) 5%, transparent);
}

.checklist-item-label {
    display: flex;
    gap: 9px;
    align-items: flex-start;
    padding: 6px 8px;
    font-size: var(--text-sm);
    line-height: 1.45;
    cursor: pointer;
}

.checklist-box {
    margin-top: 2px;
    accent-color: var(--primary-500);
    flex-shrink: 0;
    cursor: pointer;
}

/* Ticked rows dim + strike through so progress reads at a glance. */
.checklist-item.is-done .checklist-item-text {
    color: var(--text-muted);
    text-decoration: line-through;
}

/* A whole-list read-only view: the native disabled boxes lose the pointer affordance. */
.checklist.is-disabled .checklist-item-label,
.checklist.is-disabled .checklist-box {
    cursor: default;
}

/* @toc Code block */
/*
 * Code block — a <pre> tokenised by code-block.js for IDE-style highlighting (no
 * library). Always-dark surface; every colour is a --code-block-* token from
 * config/components/code-block.php. Used by the docs and available as
 * <x-code-block language code>.
 */

.code-block {
    margin: 0;
    padding: 14px 16px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: var(--code-block-background-color);
    overflow-x: auto;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    line-height: 1.6;
    color: var(--code-block-color);
    white-space: pre;
}

/* Token colours — scoped to the code node so BOTH the default block and the file variant get them. */
.code-block-code .tok-comment {
    color: var(--code-block-comment);
    font-style: italic;
}

.code-block-code .tok-string { color: var(--code-block-string); }
.code-block-code .tok-keyword { color: var(--code-block-keyword); }
.code-block-code .tok-number { color: var(--code-block-number); }
.code-block-code .tok-property { color: var(--code-block-property); }
.code-block-code .tok-operator { color: var(--code-block-operator); }
.code-block-code .tok-punctuation { color: var(--code-block-punctuation); }
.code-block-code .tok-tag { color: var(--code-block-tag); }
.code-block-code .tok-attr { color: var(--code-block-attribute); }
.code-block-code .tok-blade { color: var(--code-block-blade); }

/* File-sized variant — the card-open Code pane portal: a filename header, a scrollable full-height window,
   and a line-number gutter. Same type metrics as .code-block so the gutter aligns with the code lines. */
.code-block-file {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
    border: 1px solid var(--line);
    border-radius: 8px;
    overflow: hidden;
    background: var(--code-block-background-color);
}

.code-block-head {
    flex: 0 0 auto;
    padding: 8px 14px;
    border-bottom: 1px solid color-mix(in oklab, var(--code-block-color) 18%, transparent);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    color: var(--code-block-color);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.code-block-scroll {
    flex: 1 1 auto;
    display: flex;
    align-items: flex-start;
    min-height: 0;
    overflow: auto;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    line-height: 1.6;
}

.code-block-gutter {
    flex: 0 0 auto;
    position: sticky;
    inset-inline-start: 0;
    padding-block: 14px;
    padding-inline: 14px 8px;
    text-align: end;
    white-space: pre;
    color: var(--code-block-comment);
    background: var(--code-block-background-color);
    user-select: none;
}

.code-block-file .code-block-pre {
    flex: 1 1 auto;
    margin: 0;
    padding-block: 14px;
    padding-inline: 8px 16px;
    background: transparent;
    color: var(--code-block-color);
    white-space: pre;
}

/* @toc Data table */
/* DataTable pattern - the chrome around the <x-table> primitive: a bulk-action bar
 * above and a footer (pager) below. The table itself is themed by table.css; this
 * file only lays out the surrounding grid + the injected selection column.
 * Component: app/View/Components/DataTable.php; view: data-table.blade.php. */

.data-table {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
}

.data-table-bulk {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
}

.data-table-footer {
    display: flex;
    flex-direction: column;
}

/* Selection column (added by `selectable`) - keep it to its checkbox width. */
.data-table .table th:first-child,
.data-table .table td.data-table-select {
    width: 1%;
    white-space: nowrap;
}

/* @toc Details */
/*
 * Details — a themed native <details>/<summary> disclosure. The native element
 * supplies the open/close behaviour; this file only themes the chrome. Every
 * --details-* token has a global-token fallback (config/components/details.php
 * is the theming surface). Stack several for an accordion. Dark-first.
 */

.details {
    border: 1px solid var(--details-border, var(--line));
    border-radius: var(--details-border-radius, var(--radius));
    background: var(--details-background-color, var(--surface-raised));
    color: var(--details-color, var(--text));
    overflow: hidden;
}

/* The summary is the disclosure trigger. list-style:none (+ the WebKit pseudo)
 * drops the UA's default triangle so our chevron is the only marker. */
.details-summary {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: var(--details-padding-y, 12px) var(--details-padding-x, 14px);
    font-size: var(--details-summary-font-size, var(--text-sm));
    font-weight: 500;
    list-style: none;
    cursor: pointer;
    user-select: none;
}

.details-summary::-webkit-details-marker {
    display: none;
}

.details-summary:hover {
    background: var(--details-summary-hover, var(--surface));
}

/* Keyboard focus ring on the summary (it's the focusable element). Mirrors the
 * other primitives: box-shadow ring + a transparent outline as the
 * forced-colors / high-contrast fallback. */
.details-summary:focus-visible {
    outline: 2px solid transparent;
    outline-offset: -2px;
    box-shadow: inset 0 0 0 2px var(--details-focus-ring, var(--primary));
}

/* The chevron points right when closed and rotates down when [open]. */
.details-marker {
    display: inline-flex;
    flex-shrink: 0;
    color: var(--details-marker-color, var(--text-muted));
    transition: transform 0.15s ease;
}

.details-marker svg {
    display: block;
    width: 16px;
    height: 16px;
}

.details[open] .details-marker {
    transform: rotate(90deg);
}

.details-summary-text {
    flex: 1;
    min-width: 0;
}

.details-body {
    padding: 0 var(--details-padding-x, 14px) var(--details-padding-y, 12px);
    color: var(--details-body-color, var(--text-muted));
    font-size: var(--details-body-font-size, var(--text-sm));
    line-height: 1.55;
}

.details-body :first-child {
    margin-top: 0;
}

.details-body :last-child {
    margin-bottom: 0;
}

/* @toc Device frame */
/* Device-frame (Editor E2) — the preview chrome + the scaled screen. The iframe selector is
 * descendant-qualified (.device-frame .device-frame-iframe) so the frame's own sizing beats x-embed's
 * `.embed` base by SPECIFICITY, not bundle order (device-frame.css emits before embed.css alphabetically).
 * Tokens only. --df-w / --df-h / --df-scale are set inline from props + updated by device-frame.js. */

.device-frame {
    display: block;
    max-width: 100%;
    /* Safety net: device-frame.js auto-fits the scale to the pane; this guarantees no body h-scroll even
       for the one frame before JS runs. */
    overflow-x: hidden;
}

/* Toolbar — the viewport-preset selector + the dimensions readout. */
.device-frame-bar {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin-block-end: var(--space-2);
}

.device-frame-presets {
    display: inline-flex;
    gap: 2px;
    padding: 2px;
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
}

.device-frame-preset {
    padding-block: 3px;
    padding-inline: var(--space-2);
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-weight: 600;
    cursor: pointer;
    transition: background 120ms ease, color 120ms ease;
}

.device-frame-preset:hover {
    color: var(--text);
}

.device-frame-preset.is-active {
    background: color-mix(in srgb, var(--primary) 16%, transparent);
    color: var(--primary);
}

/* AA on the 16% tint: bare --primary (primary-500) is only ~3:1 on the tint at 12px — step to a neighbouring
   rung per theme so the SELECTED viewport is readable (the ramp law: contrast fix = step a rung, own the flip). */
:root[data-theme="light"] .device-frame-preset.is-active {
    color: var(--primary-700);
}

:root[data-theme="dark"] .device-frame-preset.is-active {
    color: var(--primary-300);
}

.device-frame-preset:focus-visible {
    outline: 2px solid var(--primary-500);
    outline-offset: 1px;
}

.device-frame-dims {
    font-size: var(--text-xs);
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/* Device chrome — a bezel around the screen; sizes to the (scaled) screen it wraps. */
.device-frame-chrome {
    display: inline-block;
    max-width: 100%;
    padding: var(--device-frame-bezel);
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--device-frame-border-radius);
}

/* The screen reserves the SCALED size (w*scale × h*scale) so the transformed iframe fills its box exactly —
 * no overflow, no gap. */
.device-frame-screen {
    width: calc(var(--df-w) * var(--df-scale));
    height: calc(var(--df-h) * var(--df-scale));
    overflow: hidden;
    border-radius: var(--radius-md);
    background: var(--surface);
}

/* The iframe renders at its NATURAL preset size (its true internal viewport, so the framed page's @media
 * respond honestly) and is scaled DOWN for display only. Descendant-qualified to beat .embed by specificity. */
.device-frame .device-frame-iframe {
    width: var(--df-w);
    /* Override x-embed's inherited `.embed { max-width: 100% }`: the containing block is the SCALED screen
       (df-w * scale), so max-width:100% would clamp the iframe's layout width to the scaled value — and then
       transform:scale would shrink it AGAIN + drop the internal viewport to the wrong width (the narrowed-div
       lie this component exists to avoid). `none` restores the true df-w viewport; transform does the sizing. */
    max-width: none;
    height: var(--df-h);
    border: 0;
    display: block;
    transform: scale(var(--df-scale));
    transform-origin: top left;
    background: var(--surface);
}

/* @toc Dialog */
/*
 * Dialog. The native <dialog> element does the heavy lifting (centred-in-viewport
 * positioning, focus trap, Escape, top-layer stacking, the ::backdrop pseudo); this
 * file only themes the panel + backdrop. Every token is a --dialog-* var emitted from
 * config/components/dialog.php['css'], each with a global-token fallback so the panel
 * reads on both themes.
 */

.dialog {
    width: min(var(--dialog-width), calc(100vw - 32px));
    max-height: calc(100vh - 64px);
    padding: 0;
    border: 1px solid var(--dialog-border, var(--line));
    border-radius: var(--dialog-border-radius);
    background: var(--dialog-background-color, var(--surface-raised));
    color: var(--dialog-color, var(--text));
    box-shadow: var(--dialog-shadow);
    /* Open experience (Card-open v2): a fast, smooth entrance the close MIRRORS. The native <dialog>
     * is position:fixed / top-layer, so nothing on the page shifts. Base = the open state; the closed
     * :not([open]) state and @starting-style share the from-state, and the display/overlay
     * allow-discrete transitions keep the element painted through the EXIT so closing reverses the
     * open. Older engines (no @starting-style) fall back to an instant show/hide. */
    opacity: 1;
    transform: none;
    transition:
        opacity 0.18s ease,
        transform 0.18s cubic-bezier(0.2, 0.8, 0.2, 1),
        overlay 0.18s ease allow-discrete,
        display 0.18s ease allow-discrete;
}

/* Closed state = the exit end-state (allow-discrete keeps it painted while it animates out). */
.dialog:not([open]) {
    opacity: 0;
    transform: translateY(6px) scale(0.985);
}

/* Entrance from-state (the first paint, before-open). */
@starting-style {
    .dialog[open] {
        opacity: 0;
        transform: translateY(6px) scale(0.985);
    }
}

/* ::backdrop is the dimming layer the browser paints behind a modal dialog — tint it so the page
 * behind recedes, and fade it with the panel (open + close). */
.dialog::backdrop {
    background: var(--dialog-backdrop);
    opacity: 1;
    transition:
        opacity 0.18s ease,
        overlay 0.18s ease allow-discrete,
        display 0.18s ease allow-discrete;
}

.dialog:not([open])::backdrop {
    opacity: 0;
}

@starting-style {
    .dialog[open]::backdrop {
        opacity: 0;
    }
}

/* Reduced motion: instant swap — the shell appears immediately, no slide / scale / fade. */
@media (prefers-reduced-motion: reduce) {
    .dialog,
    .dialog::backdrop {
        transition: none;
    }

    .dialog:not([open]) {
        transform: none;
    }
}

/* Responsive padding — Nuxt's p-4 sm:p-6: 16px on mobile (the config default), 24px from 640px up. The
   header / body / footer all read --dialog-padding, so bumping it here re-spaces them together. */
@media (min-width: 640px) {
    .dialog {
        --dialog-padding: 24px;
    }
}

.dialog-panel {
    display: flex;
    flex-direction: column;
    position: relative;   /* anchors the absolute top-right close */
    min-height: 0;
    max-height: calc(100vh - 64px);
}

/* Header — parted from the body by a full-width rule (Nuxt's divide-y). Top-aligned; the close sits absolute
   in the corner (below), so the heading reserves right space to clear it. */
.dialog-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 6px;
    padding: var(--dialog-padding);
    padding-right: calc(var(--dialog-padding) + 32px);
    border-bottom: 1px solid var(--dialog-border, var(--line));
    flex-shrink: 0;
}

.dialog-heading {
    min-width: 0;
}

.dialog-title {
    margin: 0;
    font-size: var(--dialog-title-font-size);
    font-weight: 600;
    line-height: 1.3;
    color: var(--dialog-color, var(--text));
}

/* Subtitle under the title (the `description` prop) — Nuxt's mt-1 (4px, R10), 14px, muted. */
.dialog-description {
    margin: 4px 0 0;
    font-size: var(--text-sm);
    line-height: 1.45;
    color: var(--text-muted);
}

/* Close affordance — a borderless icon button in the header. */
.dialog-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: absolute;
    top: 16px;
    right: 16px;
    padding: 4px;
    border: none;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--dialog-close-color, var(--text-muted));
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
}

.dialog-close:hover {
    background: var(--dialog-close-background-color-hover, var(--line));
    color: var(--dialog-color, var(--text));
}

.dialog-close svg {
    display: block;
    width: 20px;
    height: 20px;
}

/* Keyboard focus ring — same box-shadow ring as the button, with a transparent
 * outline as the forced-colors / high-contrast fallback. */
.dialog-close:focus-visible {
    outline: 2px solid transparent;
    outline-offset: 2px;
    box-shadow: 0 0 0 3px var(--dialog-focus-ring, var(--primary));
}

.dialog-body {
    padding: var(--dialog-padding);
    overflow-y: auto;
    flex: 1 1 auto;
    min-height: 0;
    font-size: var(--text-sm);
    line-height: 1.6;
}

/* Flush body (padded=false) — full-bleed content (tabs, tables, media) sits edge-to-edge. */
.dialog-body-flush {
    padding: 0;
}

.dialog-body :first-child {
    margin-top: 0;
}

.dialog-body :last-child {
    margin-bottom: 0;
}

/* Footer — the actions area, parted from the body by a rule; actions sit end-aligned (the primary last). */
.dialog-foot {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    flex-wrap: wrap;
    gap: 8px;
    padding: var(--dialog-padding);
    border-top: 1px solid var(--dialog-border, var(--line));
    flex-shrink: 0;
}

/* Size variants — the primary axis. Each sets --dialog-width, read by `.dialog { width: min(...) }`; an
   id/class width override (e.g. #planner-card-modal) still wins by specificity, so bespoke widths are untouched.
   md keeps the config default width, so an un-sized dialog is unchanged. */
.dialog-sm { --dialog-width: 384px; }   /* Nuxt max-w-sm */
.dialog-md { --dialog-width: 512px; }   /* Nuxt max-w-lg (the default) */
.dialog-lg { --dialog-width: 672px; }   /* Nuxt max-w-2xl */

/* Fullscreen — edge-to-edge, no radius/border; overrides the width + height directly. */
.dialog-fullscreen {
    width: 100vw;
    max-width: 100vw;
    height: 100vh;
    max-height: 100vh;
    border: none;
    border-radius: 0;
}

.dialog-fullscreen .dialog-panel {
    height: 100vh;
    max-height: 100vh;
}

/* @toc Drawer */
/*
 * Drawer — the slide-out (overlay) / docked side-panel primitive. Promoted from the hand-rolled
 * off-canvas patterns (the app-shell mobile nav, the study-reader rail, the dev-tools overlay).
 *
 * A CUSTOM element, NOT the native <dialog> x-dialog rides — a docked drawer is an in-flow layout
 * column, which <dialog> cannot be. So the overlay's modal a11y (focus trap, Escape, focus-return,
 * scroll-lock) is wired in drawer.js, and the closed overlay hides with visibility:hidden (out of
 * the a11y tree AND the tab order) rather than the native display:none. The open state is the
 * .is-open CLASS (so it is assertable by the keyboard runner's class shape). Every token is a
 * --drawer-* var from config/components/drawer.php['css'], each with a global-token fallback.
 */

/* Shared panel surface (both modes). */
.drawer-panel {
    display: flex;
    flex-direction: column;
    min-height: 0;
    background: var(--drawer-background-color, var(--surface-raised));
    color: var(--drawer-color, var(--text));
}

/* The [hidden] guard (contract checklist #3): the panel sets display:flex, which DEFEATS the HTML
 * [hidden] attribute's UA display:none — so restore it explicitly, wherever a consumer (or a
 * framework) hard-hides the drawer without animation. */
.drawer[hidden],
.drawer-panel[hidden] {
    display: none;
}

/* ============================ OVERLAY MODE — the slide-out sheet ============================ */
.drawer-overlay {
    position: fixed;
    inset: 0;
    z-index: var(--z-modal);
    /* Closed state (checklist #2): visibility:hidden takes the whole overlay OUT of the a11y tree
     * AND the tab order — a translated-offscreen panel alone stays focusable. The flip is DELAYED
     * one duration so the panel still animates OUT (the app-shell nav-open trick); it flips to
     * visible instantly on open. */
    visibility: hidden;
    transition: visibility 0s linear var(--drawer-duration, 240ms);
}

.drawer-overlay.is-open {
    visibility: visible;
    transition: visibility 0s linear 0s;
}

/* Backdrop scrim — the shared, theme-flipping --overlay tint, faded with the panel. A pseudo-element
 * so it needs no extra DOM; a click on it lands on the .drawer root, which drawer.js reads as a
 * dismiss (the pseudo cannot be an event target, so the click registers on the host). */
.drawer-overlay::before {
    content: "";
    position: absolute;
    inset: 0;
    background: var(--drawer-backdrop, var(--overlay));
    opacity: 0;
    transition: opacity var(--drawer-duration, 240ms) ease;
}

.drawer-overlay.is-open::before {
    opacity: 1;
}

/* The sheet itself — pinned to an edge, full height, parked off-canvas when closed. The body scrolls,
 * not the panel. */
.drawer-overlay .drawer-panel {
    position: absolute;
    inset-block: 0;
    width: min(var(--drawer-width), 100vw);
    max-height: 100vh;
    box-shadow: var(--drawer-shadow);
    overflow: hidden;
    transition: transform var(--drawer-duration, 240ms) var(--drawer-ease, cubic-bezier(0.2, 0.8, 0.2, 1));
}

/* Side: the physical edge the sheet pins to and slides from (the prop is physical: left|right). */
.drawer-overlay.drawer-right .drawer-panel {
    right: 0;
    border-left: 1px solid var(--drawer-border, var(--line));
    transform: translateX(100%);
}

.drawer-overlay.drawer-left .drawer-panel {
    left: 0;
    border-right: 1px solid var(--drawer-border, var(--line));
    transform: translateX(-100%);
}

.drawer-overlay.is-open .drawer-panel {
    transform: none;
}

/* Full-width sheet under the drawer breakpoint (page chrome -> the --bp-* viewport seam, substituted
 * to px at emission by CssService; per the modernity contract this stays a viewport @media). */
@media (max-width: 768px) {
    .drawer-overlay .drawer-panel {
        width: 100vw;
        max-width: 100vw;
    }
}

/* Scroll-lock while ANY overlay drawer is open — pure CSS via :has() (a sanctioned modern feature),
 * so there is no JS scroll-lock bookkeeping to drift. Mirrors the app-shell body-overflow lock. */
html:has(.drawer-overlay.is-open) {
    overflow: hidden;
}

/* ============================ DOCKED MODE — an in-flow layout column ============================ */
.drawer-docked {
    width: min(var(--drawer-width), 100%);
}

.drawer-docked .drawer-panel {
    height: 100%;
    border: 1px solid var(--drawer-border, var(--line));
    border-radius: var(--drawer-border-radius);
    overflow: hidden;
}

/* ============================ HEADER / BODY (shared) ============================ */
.drawer-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-2);
    padding: var(--drawer-padding);
    border-bottom: 1px solid var(--drawer-border, var(--line));
    flex-shrink: 0;
}

.drawer-title {
    margin: 0;
    font-size: var(--drawer-title-font-size);
    font-weight: 600;
    line-height: 1.3;
    color: var(--drawer-color, var(--text));
}

/* Close — a borderless icon button. Nudged with a negative margin so its padding hangs into the
 * header padding, keeping the glyph aligned to the corner. */
.drawer-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    margin: -4px -4px -4px 0;
    padding: 4px;
    border: none;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--drawer-close-color, var(--text-muted));
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
}

.drawer-close:hover {
    background: var(--drawer-close-background-color-hover, var(--line));
    color: var(--drawer-color, var(--text));
}

/* Keyboard focus ring — the same box-shadow ring as the button, with a transparent outline as the
 * forced-colors / high-contrast fallback. */
.drawer-close:focus-visible {
    outline: 2px solid transparent;
    outline-offset: 2px;
    box-shadow: 0 0 0 3px var(--drawer-focus-ring, var(--primary));
}

.drawer-close svg {
    display: block;
    width: 20px;
    height: 20px;
}

.drawer-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: var(--drawer-padding);
    font-size: var(--text-sm);
    line-height: 1.6;
}

/* Flush body (padded=false) — full-bleed content sits edge-to-edge (a grouped schedule with rules,
 * a table, media). */
.drawer-body-flush {
    padding: 0;
}

.drawer-body :first-child {
    margin-top: 0;
}

.drawer-body :last-child {
    margin-bottom: 0;
}

/* ============================ SIZE VARIANTS — the panel width ============================ */
.drawer-sm { --drawer-width: 300px; }
.drawer-md { --drawer-width: 360px; }   /* the study-reader rail precedent (the config default) */
.drawer-lg { --drawer-width: 460px; }

/* ============================ REDUCED MOTION ============================ */
/* Instant show / hide — no slide, fade, or delayed-visibility flip (checklist #3). */
@media (prefers-reduced-motion: reduce) {
    .drawer-overlay,
    .drawer-overlay::before,
    .drawer-overlay .drawer-panel {
        transition: none;
    }
}

/* @toc Dropdown */
/* Dropdown — the APG menu-button. A relatively-positioned wrapper holds the trigger + an absolutely-
 * positioned flyout; closed = the flyout carries [hidden] (display:none — gone from the AT tree), open =
 * dropdown.js removes it and adds .is-open to the wrapper. Colours are --dropdown-* (config facet):
 * ramp-lawful surface/line, item text via semantic tokens, danger = the destructive token. Behaviour +
 * focus movement: dropdown.js. */

.dropdown {
    position: relative;
    display: inline-block;
}

.dropdown-trigger {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    appearance: none;
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    background: var(--surface);
    color: var(--text);
    font-family: inherit;
    font-size: var(--text-sm);
    font-weight: 500;
    padding: 7px 12px;
    cursor: pointer;
    transition: background 0.15s ease, border-color 0.15s ease;
}

.dropdown-trigger:hover {
    background: var(--dropdown-item-hover-background-color);
}

.dropdown-trigger-icon,
.dropdown-caret {
    display: inline-flex;
    width: 16px;
    height: 16px;
    flex-shrink: 0;
}

.dropdown-caret {
    color: var(--text-muted);
    transition: transform 0.15s ease;
}

.dropdown.is-open .dropdown-caret {
    transform: rotate(180deg);
}

.dropdown-trigger-icon svg,
.dropdown-caret svg,
.dropdown-item-icon svg,
.dropdown-item-check svg {
    display: block;
    width: 100%;
    height: 100%;
}

/* The flyout */
.dropdown-menu {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    z-index: var(--z-popover);
    min-width: var(--dropdown-min-width);
    margin: 0;
    padding: 4px;
    background: var(--dropdown-surface);
    border: 1px solid var(--dropdown-line);
    border-radius: var(--dropdown-border-radius);
    box-shadow: var(--dropdown-shadow);
}

.dropdown-menu[hidden] {
    display: none;
}

.dropdown[data-align="end"] .dropdown-menu {
    left: auto;
    right: 0;
}

/* Items */
.dropdown-item {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    box-sizing: border-box;
    appearance: none;
    border: none;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--dropdown-item-text);
    font-family: inherit;
    font-size: var(--text-sm);
    text-align: left;
    text-decoration: none;
    padding: 7px 10px;
    cursor: pointer;
    white-space: nowrap;
}

.dropdown-item:hover,
.dropdown-item:focus-visible {
    background: var(--dropdown-item-hover-background-color);
    outline: none;
}

.dropdown-item:focus-visible {
    box-shadow: inset 0 0 0 2px var(--primary);
}

.dropdown-item.is-danger {
    color: var(--dropdown-danger-text);
}

.dropdown-item[aria-disabled="true"] {
    color: var(--dropdown-item-text-muted);
    opacity: var(--disabled-opacity);
    cursor: default;
}

.dropdown-item-icon {
    display: inline-flex;
    width: 16px;
    height: 16px;
    flex-shrink: 0;
    color: var(--dropdown-item-text-muted);
}

/* The checkbox/radio tick — reserved space always; shown only when aria-checked. */
.dropdown-item.is-toggle {
    padding-left: 8px;
}

.dropdown-item-check {
    display: inline-flex;
    width: 16px;
    height: 16px;
    flex-shrink: 0;
    color: var(--primary);
    visibility: hidden;
}

.dropdown-item[aria-checked="true"] .dropdown-item-check {
    visibility: visible;
}

.dropdown-item-label {
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Groups + derived separators */
/* Label TREATMENT comes from the shared .ui-label recipe (R5-B, applied in dropdown.blade.php); this rule
   keeps only STRUCTURAL styles (never a label property — one-edit revert lives in base.css's .ui-label). */
.dropdown-group-label {
    margin: 0;
    padding: 6px 10px 2px;
}

.dropdown-sep {
    height: 1px;
    margin: 4px 0;
    background: var(--dropdown-line);
}

/* @toc Editor tools */
/*
 * The Editor tools pane (E3 / map-F6) — now the LIBRARY (#1310): the component catalog, promoted to the left
 * rail. A search box, then one taxonomy group per docs-group (Primitives · Patterns · Sections), each a flat
 * list of component rows. A row is a SELECT button (the type name → the Inspector) beside an INSERT button
 * (click, or drag via SortableJS → place an instance).
 *
 * Colour comes from the ramp only — semantic tokens first (--text-muted, --line, --surface-raised), never a
 * literal. The pane sizes to its host and scrolls its own overflow rather than pushing the page.
 *
 * SCOPED TO .editor-tools-pane, NOT .editor-tools: the editor frame's Tools REGION already carries
 * `class="editor-region editor-tools"` (resources/views/editor/show.blade.php), so styling the bare
 * `.editor-tools` here would reach up and restyle that region — this component's box, applied to its own host.
 * The tokens stay --editor-tools-* (they are named for the component, not the selector).
 *
 * THE [hidden] TRAP. The search (editor-tools.js) hides non-matching rows AND empty groups by toggling their
 * `hidden` property. An author `display` on either would WIN over the UA `[hidden] { display: none }` and defeat
 * the filter — so every `display` on a row or group is guarded `:not([hidden])`.
 */

/* The `:not([hidden])` guard is THE TRAP ABOVE, applied to the pane itself (#1318). The comment warned about
 * rows and groups because that is where the search filter bites — but the Inspector reuses this class on every
 * type→config pane (editor/_inspector.blade.php adds it for target scope), and editor-live.js hides those by
 * setting `hidden`. An unguarded author `display` beat the UA rule, so all ~70 stayed on screen: the landing
 * page's Config listed a BarBruiser study sheet, and the document ran 38,000px tall. The pane's own rule needed
 * the same guard its rows already had. */
.editor-tools-pane:not([hidden]) {
    display: flex;
    flex-direction: column;
    gap: var(--editor-tools-gap, var(--space-4));
    padding: var(--editor-tools-padding, var(--space-4));
    block-size: 100%;
    overflow-y: auto;
}

/* The Library sits tighter than the base pane: the group hairlines are the section dividers (James's "lines,
 * not cards"), so the inter-child gap shrinks and lets those borders do the separating. */
.editor-library-pane {
    --editor-tools-gap: var(--space-2);
}

/*
 * KILL THE CARD, KEEP THE DISCLOSURE (James, 2026-07-16): "I don't want the cards anymore, they take up too
 * much space and honestly aren't the language of the app... use lines to separate the sections."
 *
 * A taxonomy group is an <x-details>, and THE CARD IS THAT PRIMITIVE'S OWN SKIN — .details paints a border, a
 * radius and --surface-raised behind every disclosure. Strip it to a flat section divided by a hairline. Fill,
 * radius and padding come from the primitive's OWN --details-* theming surface (custom properties inherit, so
 * setting them on the row reaches .details-summary and .details-body without touching either). The border is a
 * real override because a merely transparent one would still occupy its 1px of layout.
 *
 * Scoped under .editor-tools-pane (0,2,0) so it beats .details (0,1,0) on SPECIFICITY, not on bundle order.
 * editor-tools.css does happen to bundle after details.css today, but that is an accident of the manifest's
 * order — a reshuffle must never silently restore the boxes.
 *
 * The group carries ONLY .editor-library-group, never also .editor-tools-group: that class sets display:flex,
 * which on a <details> both mislays the summary/body and (being an author display) defeats the search's hide.
 */
.editor-tools-pane .editor-library-group {
    --details-background-color: transparent;
    --details-border-radius: 0;
    --details-padding-x: 0;
    --details-padding-y: var(--space-2);
    border: 0;
    border-block-end: 1px solid var(--line);
    padding-block-end: 0;
}

/* The last group is unruled — the pane edge is the divider (the accordion's flat-divided-list language). */
.editor-tools-pane .editor-library-group:last-of-type {
    border-block-end: 0;
}

/* A flat list of component rows inside the group's disclosure body. role="list" in the markup keeps the list
 * semantics that list-style:none otherwise strips in some engines. */
.editor-library-list {
    margin: 0;
    padding: 0;
    list-style: none;
}

/* A component row: the type name grows, Insert sits at the end. Display is guarded :not([hidden]) so the
 * search's `item.hidden = true` genuinely hides the row. */
.editor-library-item:not([hidden]) {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

/* SELECT: the type name fills the row. Ghost buttons centre their label; a list row reads left-aligned, and the
 * item shrinks (min-inline-size:0) so a long label can't force the row wider than the pane. Its selected state
 * (aria-pressed, set by editor-live.js when this type owns the Inspector) gets a raised fill + weight. */
.editor-library-select {
    flex: 1;
    min-inline-size: 0;
    justify-content: flex-start;
    text-align: start;
}

.editor-library-select[aria-pressed="true"] {
    background: var(--surface-raised);
    font-weight: 600;
}

/* INSERT: the row's trailing action — never grows. It is also the SortableJS drag source (editor-drag.js binds
 * [data-editor-insert]); click stays the primary, keyboard-operable path (WCAG 2.2 AA), drag the enhancement. */
.editor-library-insert {
    flex: none;
}

/* The search's empty state. No `display` declared, so its markup `hidden` (and the JS toggling it) works. */
.editor-library-no-results {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--text-sm);
}

/* The tenant-has-nothing state (the @empty branch) — distinct from a no-search-match, and never hidden. */
.editor-tools-empty {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--text-sm);
}

/* @toc Embed */
/* Embed — the iframe primitive. Structural + a subtle load-time surface; an optional aspect-ratio
 * rides the --embed-ratio custom property the view sets per instance. The 4 migrated call sites keep
 * their own sizing class (.uploads-view-frame / .inv-view-embed / .uploads-modal-frame / the
 * .mps-recipe-video wrapper), each of which is authored AFTER this in the bundle (or more specific),
 * so their border / radius / background win — this base only fills the gaps a bare <iframe> leaves. */

.embed {
    display: block;
    width: 100%;
    max-width: 100%;
    border: 0;
    background: var(--embed-background-color);
    aspect-ratio: var(--embed-ratio, auto);
}

/* @toc Emoji picker */
/*
 * Emoji picker (#844) — a floating panel: search + tone above, category strip, then the scrolling result grid.
 *
 * Every --emoji-picker-* var is emitted from config/components/emoji-picker.php's css facet, each with a
 * semantic-token fallback so the component themes per tenant and still renders if a token is absent. The picker
 * is a RAISED surface (it floats over a page), unlike the flat editor chrome — that is the one place it departs
 * from the workspace's flatness, and deliberately.
 *
 * Only the GRID scrolls. The search, tone selector and category strip stay put, so the thing you are typing into
 * never scrolls away from you — the same containment lesson as the editor rail (#1298).
 */

.emoji-picker {
    display: flex;
    flex-direction: column;
    inline-size: var(--emoji-picker-width, 352px);
    max-inline-size: 100%;
    background: var(--emoji-picker-surface, var(--surface-raised));
    border: 1px solid var(--emoji-picker-border, var(--line));
    border-radius: var(--emoji-picker-border-radius, var(--radius-lg));
    overflow: hidden;
}

.emoji-picker-head {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-3);
    border-block-end: 1px solid var(--line);
}

.emoji-picker-search {
    flex: 1 1 auto;
    min-inline-size: 0;
}

.emoji-picker-search-input {
    inline-size: 100%;
    padding: var(--space-2) var(--space-3);
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    font-size: var(--text-sm);
}

.emoji-picker-search-input:focus-visible {
    outline: 2px solid transparent;
    outline-offset: 2px;
    box-shadow: 0 0 0 3px var(--emoji-picker-focus-ring, var(--primary));
}

/* ── skin tone ─────────────────────────────────────────────────────────────────────────────────── */

.emoji-picker-tones {
    display: flex;
    flex-shrink: 0;
    gap: var(--space-1);
}

.emoji-picker-tone {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-1);
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius);
    cursor: pointer;
}

.emoji-picker-tone:hover {
    background: var(--emoji-picker-cell-hover, var(--surface));
}

.emoji-picker-tone:focus-visible {
    outline: 2px solid transparent;
    outline-offset: 2px;
    box-shadow: 0 0 0 3px var(--emoji-picker-focus-ring, var(--primary));
}

/* The chosen tone reads as pressed — a border, not just a background, so it survives forced-colors mode. */
.emoji-picker-tone[aria-pressed="true"] {
    border-color: var(--emoji-picker-cell-selected, var(--primary));
}

/* The swatch IS the emoji at that tone (see the view) — no hand-authored colour, and it renders exactly as
   the platform will render the picked tone. */
.emoji-picker-tone-swatch {
    display: block;
    font-size: 15px;
    line-height: 1;
}


/* ── categories ────────────────────────────────────────────────────────────────────────────────── */

.emoji-picker-categories {
    display: flex;
    gap: var(--space-1);
    padding: var(--space-2) var(--space-3);
    overflow-x: auto;
    border-block-end: 1px solid var(--line);
}

.emoji-picker-category {
    flex-shrink: 0;
    padding: var(--space-1) var(--space-2);
    color: var(--text-muted);
    background: transparent;
    border: 0;
    border-radius: var(--radius);
    font-size: var(--text-xs);
    white-space: nowrap;
    cursor: pointer;
}

.emoji-picker-category:hover {
    color: var(--text);
    background: var(--emoji-picker-cell-hover, var(--surface));
}

.emoji-picker-category:focus-visible {
    outline: 2px solid transparent;
    outline-offset: -2px;
    box-shadow: inset 0 0 0 2px var(--emoji-picker-focus-ring, var(--primary));
}

.emoji-picker-category[aria-selected="true"] {
    color: var(--text);
    background: var(--surface);
}

/* ── the grid ──────────────────────────────────────────────────────────────────────────────────── */

.emoji-picker-grid {
    block-size: var(--emoji-picker-grid-height, 260px);
    padding: var(--space-2);
    overflow-y: auto;
}

.emoji-picker-section-label {
    margin: var(--space-2) 0 var(--space-1);
    padding-inline: var(--space-1);
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.02em;
    text-transform: uppercase;
}

.emoji-picker-row {
    display: flex;
}

.emoji-picker-cell {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: var(--emoji-picker-cell-size, 40px);
    block-size: var(--emoji-picker-cell-size, 40px);
    padding: 0;
    background: transparent;
    border: 0;
    border-radius: var(--emoji-picker-cell-border-radius, var(--radius));
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
}

.emoji-picker-cell:hover {
    background: var(--emoji-picker-cell-hover, var(--surface));
}

/* The roving-tabindex cell is the ONE tab stop; its focus ring is inset so it never clips against a neighbour. */
.emoji-picker-cell:focus-visible {
    outline: 2px solid transparent;
    outline-offset: -2px;
    box-shadow: inset 0 0 0 2px var(--emoji-picker-focus-ring, var(--primary));
}

.emoji-picker-message {
    margin: 0;
    padding: var(--space-4) var(--space-2);
    color: var(--text-muted);
    font-size: var(--text-sm);
    text-align: center;
}

/* Reduced motion: nothing here animates, stated for honesty rather than as a no-op — the picker's only motion
   would be a hover tint, which is instant by design. */

/* @toc Fieldset */
/* Fieldset. All --fieldset-* tokens come from config/components/fieldset.php['css'];
 * each var carries the matching global-token fallback so the file reads on its own. */

.fieldset {
    /* Outer spacing rides the --space-* scale (F5): the old 0 0 14px was off-scale and
     * contract-invisible; snapped to the nearest rung (--space-4 = 16px, matching the
     * component's own 16px padding) so a builder reading the contract sees a named token. */
    margin: 0 0 var(--space-4);
    padding: var(--fieldset-padding, 16px);
    border: 1px solid var(--fieldset-border, var(--line));
    border-radius: var(--fieldset-border-radius, 8px);
    background: var(--fieldset-background-color, var(--surface));
    min-inline-size: 0;
}

.fieldset-legend {
    padding: 0 var(--fieldset-legend-padding-x, 6px);
    color: var(--fieldset-legend-color, var(--text));
    font-size: var(--fieldset-legend-font-size, var(--text-sm));
    font-weight: var(--fieldset-legend-font-weight, 500);
}

.fieldset-body {
    display: flex;
    flex-direction: column;
    gap: var(--fieldset-gap, 4px);
}

/* Keyboard focus on a control inside the group lifts the border to the focus
 * colour as a grouping cue; the controls keep their own :focus-visible rings. */
.fieldset:focus-within {
    border-color: var(--fieldset-focus-border, var(--primary));
}

/* Native disabled cascades to nested controls; dim the group to match. */
.fieldset:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* @toc Figure */
/* Figure. All --figure-* tokens come from config/components/figure.php['css'];
 * structural rules aside, the config is the component's full theming surface. */

.figure {
    margin: 0;
    display: inline-block;
    max-width: 100%;
}

.figure-img {
    display: block;
    width: 100%;
    height: auto;
    border: 1px solid var(--figure-border, var(--line));
    border-radius: var(--figure-border-radius, 0);
    background: var(--figure-background-color, var(--surface-raised));
}

/* The rounded variant — opt-in soft corners on the image. */
.figure.is-rounded .figure-img {
    border-radius: var(--figure-border-radius-rounded, 12px);
}

.figure-caption {
    margin-top: var(--figure-caption-gap, 8px);
    color: var(--figure-caption-color, var(--text-muted));
    font-size: var(--figure-caption-font-size, var(--text-sm));
    line-height: 1.5;
    text-align: var(--figure-caption-align, start);
}

/* @toc Financials import */
/* Financials import widget - the account field + paste/drop CSV form, and the
 * recent-imports section on the page. Themes off shared tokens (cloned from
 * barbruiser-create.css). The recent-imports table reuses .fin-table from the ledger.
 * View: financials-import.blade.php; behaviour: financials-import.js. */

.financials-import {
    max-width: var(--max-w-3xl);
}

/* Drop zone - a dashed target that also opens the file picker on tap (it wraps a
 * hidden file input), so desktop drag and mobile tap both work. .is-over matches
 * the class toggled in financials-import.js. */
.fin-import-drop {
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 22px;
    margin-bottom: 12px;
    border: 2px dashed var(--line);
    border-radius: var(--radius-lg);
    background: var(--surface-raised);
    color: var(--text-muted);
    font-size: var(--text-sm);
    cursor: pointer;
    transition: border-color 0.15s ease, color 0.15s ease;
}

.fin-import-drop:hover,
.fin-import-drop.is-over {
    border-color: var(--primary);
    color: var(--text);
}

/* Keyboard focus — the file <input> is .sr-only (focusable but off-screen), so its focus ring must ride
 * the visible drop-zone label (WCAG 2.4.7). :focus-visible = keyboard only; the transparent outline is the
 * forced-colors fallback (box-shadow is dropped in high-contrast, so the outline gets promoted). */
.fin-import-drop:has(input:focus-visible) {
    border-color: var(--primary);
    color: var(--text);
    outline: 2px solid transparent;
    outline-offset: 2px;
    box-shadow: 0 0 0 3px var(--primary);
}

/* The CSV paste box is the textarea primitive; only override to a monospace face
 * so pasted columns line up (border / bg / focus come from textarea.css). */
.textarea-field.fin-import-textarea {
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    line-height: 1.5;
}

.fin-import-actions {
    margin-top: 12px;
}

/* Recent imports (page section below the widget) */
.fin-import-recent {
    margin-top: var(--space-10);
}

.fin-import-recent-h {
    margin: 0 0 var(--space-3);
    font-size: var(--text-lg);
    font-weight: 600;
    color: var(--text);
}

.fin-import-undo {
    display: inline;
}

/* @toc Financials ledger */
/* Financials ledger - the filter bar, the bulk-categorize controls, and the per-row
 * cells inside the DataTable pattern. The table chrome, sortable headers, selection
 * column and pager come from the table / data-table / pagination components; this
 * file styles only the Finance-specific bits: filter layout, amount colours, and the
 * category cell + its inline select. Colours from --financials-ledger-* (config).
 * View: financials-ledger.blade.php; JS: financials-ledger.js. */

.financials-ledger {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
}

/* Filter bar - a responsive row of primitive controls + apply / clear. */
.fin-filters {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-3);
    padding: var(--space-4);
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
}

.fin-filters .input,
.fin-filters .select {
    width: 100%;
}

.fin-filter-search {
    flex: 1 1 220px;
    min-width: 180px;
}

.fin-filter {
    flex: 0 1 160px;
}

.fin-filter-actions {
    display: flex;
    gap: var(--space-2);
    margin-left: auto;
}

/* Bulk-categorize bar (the DataTable `bulk` slot): the picker + count + save. */
.data-table-bulk .select {
    flex: 0 1 200px;
}

.fin-bulk-count {
    font-size: var(--text-sm);
    color: var(--text-muted);
}

.fin-bulk-save {
    margin-left: auto;
}

/* Amount cell - tabular, green for income / red for expense (right-aligned by the
 * table primitive's table-right class on the cell). */
.fin-cell-amount {
    font-variant-numeric: tabular-nums;
    font-weight: 600;
}

.fin-cell-amount.is-positive {
    color: var(--financials-ledger-amount-positive);
}

.fin-cell-amount.is-negative {
    color: var(--financials-ledger-amount-negative);
}

.fin-cell-desc {
    max-width: 360px;
    white-space: normal;
    color: var(--text);
}

.fin-cell-account {
    color: var(--text-muted);
}

/* Category cell - a colour dot + the inline select primitive. */
.fin-cell-cat {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.fin-cell-cat .select {
    flex: 1;
    min-width: 150px;
}

.fin-cat-dot {
    flex-shrink: 0;
    width: 9px;
    height: 9px;
    border-radius: var(--radius-full);
    background: var(--dot, var(--financials-ledger-chip-empty));
}

/* A per-row select changed since load (client-side) gets a subtle accent so unsaved
 * marks are visible before hitting Save. */
.select-field.fin-cat-select.is-dirty {
    border-color: var(--primary);
}

/* Empty state */
.fin-empty {
    padding: var(--space-12) var(--space-4);
    text-align: center;
    color: var(--text-muted);
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
}

@media (max-width: 768px) {
    .fin-filter,
    .fin-filter-search {
        flex-basis: 100%;
    }

    .fin-filter-actions {
        margin-left: 0;
        width: 100%;
    }
}

/* @toc Financials overview */
/* Financials overview — two tabs. Budget: a clean income / fixed-bills / flexible breakdown list.
 * Calendar: a scroll-back carousel of real month calendars (financials-calendar.blade.php). Theme
 * tokens so it flips light/dark. View: financials-overview.blade.php. */

.fin-overview {
    display: block;
}

.fin-overview .tabs-panel {
    padding-top: var(--space-6);
}

.fin-ov-muted {
    color: var(--text-muted);
    font-weight: 400;
    font-size: var(--text-sm);
}

/* --- BUDGET tab: the breakdown list --- */
.fin-bud2 {
    display: flex;
    flex-direction: column;
    gap: var(--space-7, 32px);
    max-width: 600px;
}

.fin-bud2-h {
    margin: 0 0 6px;
    font-size: var(--text-sm);
    font-weight: 700;
}

.fin-bud2-h span {
    font-weight: 400;
    color: var(--text-muted);
}

.fin-bud2-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 9px 0;
    border-bottom: 1px solid var(--line);
}

.fin-bud2-row.is-total {
    border-bottom: none;
    border-top: 1px solid var(--line);
    margin-top: 2px;
}

.fin-bud2-name {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 3px 9px;
    font-weight: 600;
    min-width: 0;
}

.fin-bud2-row.is-total .fin-bud2-name {
    font-weight: 700;
}

.fin-bud2-name small {
    font-weight: 400;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.fin-bud2-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    flex: none;
}

.fin-bud2-dot.is-in {
    background: var(--success);
}

.fin-bud2-dot.is-flex {
    background: var(--warning);
}

.fin-bud2-logo {
    width: 20px;
    height: 20px;
    border-radius: 5px;
    flex: none;
}

.fin-bud2-logo.is-blank {
    display: grid;
    place-items: center;
    font-size: 11px;
    font-weight: 700;
    color: #fff;
    background: var(--neutral-500, #737373);
}

.fin-bud2-amt {
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.fin-bud2-row.is-total.is-flex .fin-bud2-name,
.fin-bud2-row.is-total.is-flex .fin-bud2-amt {
    color: var(--warning);
}

/* --- CALENDAR tab: header + book key --- */
.fin-ov-calhead {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 14px;
}

.fin-ov-calhead .fin-ov-muted {
    margin: 0;
}

.fin-ov-key {
    display: flex;
    gap: 14px;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.fin-ov-key span {
    display: inline-flex;
    align-items: center;
    gap: 5px;
}

.fin-ov-key-dot {
    width: 9px;
    height: 9px;
    border-radius: 50%;
}

.fin-ov-key-dot.is-house {
    background: var(--neutral-400, #a3a3a3);
}

.fin-ov-key-dot.is-biz {
    background: var(--palette-indigo-500, #6366f1);
}

/* --- carousel: one big month per viewport (two only on very wide screens) --- */
.fin-car-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 10px;
}

.fin-car-hint {
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.fin-car-nav {
    display: flex;
    gap: 6px;
}

.fin-car-btn {
    width: 32px;
    height: 32px;
    display: grid;
    place-items: center;
    font-size: 18px;
    line-height: 1;
    background: var(--surface-raised, var(--neutral-100));
    border: 1px solid var(--line);
    border-radius: 8px;
    color: var(--text);
    cursor: pointer;
}

.fin-car-btn:hover {
    border-color: var(--primary-500, var(--palette-rose-500));
}

.fin-car-track {
    container-type: inline-size;
    display: flex;
    gap: 16px;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    padding-bottom: 10px;
}

.fin-car-item {
    flex: 0 0 100%;
    scroll-snap-align: start;
}

/* 1080px: the carousel TRACK's own inline size (a container query, not the app --bp-* viewport seam, with
   which it coexists) — the month cards go 2-up once the track itself is wide enough, so it responds to its
   slot rather than the window. .fin-car-track sets container-type above. */
@container (min-width: 1080px) {
    .fin-car-item {
        flex: 0 0 calc(50% - 8px);
    }
}

/* --- month calendar grid --- */
.fin-cal2-head {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 8px;
}

.fin-cal2-label {
    font-size: var(--text-base, 15px);
    font-weight: 700;
}

.fin-cal2-tag {
    font-size: 10px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding: 2px 7px;
    border-radius: 5px;
}

.fin-cal2-tag.is-actual {
    background: transparent;
    border: 1px solid var(--line);
    color: var(--text-muted);
}

.fin-cal2-tag.is-projected {
    background: transparent;
    border: 1px dashed var(--palette-indigo-500, #6366f1);
    color: var(--palette-indigo-500, #6366f1);
}

.fin-cal2-total {
    margin-left: auto;
    font-size: var(--text-sm);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
}

.fin-cal2-grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 4px;
}

.fin-cal2-dow {
    text-align: center;
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--text-muted);
    padding-bottom: 2px;
}

.fin-cal2-cell {
    position: relative;
    display: flex;
    flex-direction: column;
    min-height: 80px;
    padding: 4px 5px;
    border: 1px solid var(--line);
    border-radius: 7px;
}

.fin-cal2-cell.is-empty {
    border: none;
    min-height: 0;
}

.fin-cal2-cell.has-bills {
    background: var(--surface-raised, var(--neutral-100));
    cursor: help;
}

.fin-cal2-cell.is-heavy {
    background: color-mix(in srgb, var(--danger) 9%, var(--surface-raised, var(--neutral-100)));
}

.fin-cal2-cell.has-bills:hover,
.fin-cal2-cell.has-bills:focus-visible {
    outline: none;
    border-color: var(--primary-500, var(--palette-rose-500));
    z-index: 5;
}

.fin-cal2-num {
    font-size: 12px;
    font-weight: 600;
    color: var(--text-muted);
}

.fin-cal2-cell.has-bills .fin-cal2-num {
    color: var(--text);
}

.fin-cal2-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 3px;
    margin-top: 3px;
}

.fin-cal2-chip {
    width: 22px;
    height: 22px;
    border-radius: 6px;
    display: grid;
    place-items: center;
    background: var(--surface, #fff);
    box-shadow: 0 0 0 1.5px var(--cal-ring, var(--line));
}

.fin-cal2-chip.is-house {
    --cal-ring: var(--neutral-400, #a3a3a3);
}

.fin-cal2-chip.is-biz {
    --cal-ring: var(--palette-indigo-500, #6366f1);
}

.fin-cal2-chip img {
    width: 18px;
    height: 18px;
    border-radius: 4px;
}

.fin-cal2-blank {
    width: 18px;
    height: 18px;
    border-radius: 4px;
    display: grid;
    place-items: center;
    font-size: 10px;
    font-weight: 700;
    color: #fff;
    background: var(--neutral-500, #737373);
}

.fin-cal2-more {
    font-size: 11px;
    font-weight: 600;
    color: var(--text-muted);
    align-self: center;
}

.fin-cal2-daysum {
    margin-top: auto;
    padding-top: 3px;
    text-align: end;
    font-size: 11px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
}

.fin-cal2-cell.is-heavy .fin-cal2-daysum {
    color: var(--danger);
}

.fin-cal2-tip {
    position: absolute;
    bottom: calc(100% + 6px);
    left: 50%;
    transform: translateX(-50%);
    min-width: 160px;
    padding: 8px 11px;
    border-radius: 8px;
    background: var(--neutral-900, #18181b);
    color: #fff;
    font-size: var(--text-xs);
    display: flex;
    flex-direction: column;
    gap: 2px;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.12s ease;
    z-index: 20;
}

.fin-cal2-tip strong {
    font-weight: 700;
    margin-bottom: 3px;
    white-space: nowrap;
}

.fin-cal2-tip span {
    white-space: nowrap;
    color: rgba(255, 255, 255, 0.85);
}

.fin-cal2-cell.has-bills:hover .fin-cal2-tip,
.fin-cal2-cell.has-bills:focus-visible .fin-cal2-tip {
    opacity: 1;
}

/*
 * THE MONTH POSITION (#745) — settled vs projected net flow, above the budget breakdown. Two figures side by
 * side so the gap between "what has cleared" and "where the month lands" is the thing you read first. Colour
 * comes from the semantic tokens only: a negative net flow is --danger (the categorical status set), the
 * staleness note is --text-muted until it crosses the week mark, when it earns --warning.
 */
.fin-pos {
    /* The containment ROOT for the figures below — without it the @container query below matches nothing and
       silently never fires (a dead rule the css gates still pass). Container-first per the authoring law. */
    container-type: inline-size;
    margin-block-end: var(--space-5);
    padding-block-end: var(--space-4);
    border-bottom: 1px solid var(--line);
}

.fin-pos-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
    flex-wrap: wrap;
}

.fin-pos-stale {
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.fin-pos-stale.is-stale {
    color: var(--warning);
}

/* Two figures at ≥ 40rem of the pane's OWN width, stacked below it — container-first, never a viewport guess. */
.fin-pos-figs {
    display: grid;
    gap: var(--space-4);
    margin-block-start: var(--space-3);
}

@container (min-width: 40rem) {
    .fin-pos-figs {
        grid-template-columns: 1fr 1fr;
    }
}

.fin-pos-label {
    display: block;
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
}

.fin-pos-amt {
    display: block;
    margin-block-start: var(--space-1);
    font-size: var(--text-2xl);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.fin-pos-amt.is-neg {
    color: var(--danger);
}

.fin-pos-sub {
    display: block;
    margin-block-start: var(--space-1);
    font-size: var(--text-xs);
    color: var(--text-muted);
}

/* @toc Financials pnl */
/* Financials P&L - summary tiles + monthly and by-category tables. Colours from
 * --financials-pnl-* (config); the tables reuse the ledger's .fin-table classes.
 * View: financials-pnl.blade.php. */

.financials-pnl {
    display: flex;
    flex-direction: column;
    gap: var(--space-8);
}

/* Book toggle - a segmented control switching the eightythree (business) and Household views. */
.fin-book-toggle {
    display: inline-flex;
    align-self: flex-start;
    gap: 4px;
    padding: 4px;
    background: var(--surface-sunken, var(--neutral-200));
    border-radius: 10px;
}

.fin-book-toggle a {
    padding: 8px 18px;
    border-radius: 7px;
    font-weight: 700;
    font-size: var(--text-sm);
    text-decoration: none;
    color: var(--text-muted);
}

.fin-book-toggle a:hover {
    color: var(--text);
}

.fin-book-toggle a.is-active {
    background: var(--surface, var(--neutral-50));
    color: var(--text);
}

.fin-pnl-note {
    margin: 10px 2px 0;
    font-size: var(--text-sm);
    color: var(--text-muted);
    line-height: 1.5;
}

/* Summary tiles - total income / expense / net for the whole period. */
.fin-pnl-summary {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-4);
}

.fin-pnl-tile {
    flex: 1 1 160px;
    padding: var(--space-4) var(--space-5);
    background: var(--financials-pnl-tile-background-color);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
}

.fin-pnl-tile-label {
    margin: 0 0 var(--space-2);
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.4px;
    font-weight: 700;
    color: var(--text-muted);
}

.fin-pnl-tile-value {
    margin: 0;
    font-size: var(--text-2xl);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--text);
}

.fin-pnl-tile-value.is-income {
    color: var(--financials-pnl-income);
}

.fin-pnl-tile-value.is-expense {
    color: var(--financials-pnl-expense);
}

.fin-pnl-section-h {
    margin: 0 0 var(--space-3);
    font-size: var(--text-lg);
    font-weight: 600;
    color: var(--text);
}

/* A type subheading row inside the by-category table (cell-level, since the table
 * primitive renders rows from arrays - every cell in the row carries the class). */
td.fin-pnl-grouprow {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.4px;
    font-weight: 700;
    color: var(--text-muted);
    background: var(--surface-raised);
}

.fin-pnl-amount {
    text-align: right;
    font-variant-numeric: tabular-nums;
    font-weight: 600;
}

.fin-pnl-amount.is-income {
    color: var(--financials-pnl-income);
}

.fin-pnl-amount.is-expense {
    color: var(--financials-pnl-expense);
}

.fin-pnl-cat {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.fin-pnl-dot {
    flex-shrink: 0;
    width: 9px;
    height: 9px;
    border-radius: var(--radius-full);
    background: var(--dot, var(--text-muted));
}

td.fin-pnl-subtotal {
    font-weight: 700;
    color: var(--text);
    border-top: 2px solid var(--line);
}

/* @toc Flow */
/* Flow — server-rendered SVG flow diagram for the docs (our no-library take on
 * Mermaid). A vertical stack of connected nodes; every colour is a --flow-* token
 * from config/components/flow.php, so it themes like any component. No JS.
 * View: /resources/views/components/flow.blade.php. */

.flow {
    margin: 0;
}

.flow-svg {
    display: block;
    max-width: 100%;
    height: auto;
    font-family: var(--font-sans);
}

.flow-box {
    fill: var(--flow-node-background-color);
    stroke: var(--flow-node-border);
    stroke-width: 1;
}

.flow-box.is-terminal {
    fill: var(--flow-accent-background-color);
    stroke: var(--flow-accent-background-color);
}

.flow-label {
    fill: var(--flow-node-text);
    font-size: var(--text-sm);
    font-weight: 500;
}

.flow-label.is-terminal {
    fill: var(--flow-accent-text);
}

.flow-note {
    fill: var(--flow-note-text);
    font-size: var(--text-xs);
}

.flow-edge {
    fill: none;
    stroke: var(--flow-edge);
    stroke-width: 1.5;
}

.flow-arrowhead {
    fill: none;
    stroke: var(--flow-edge);
    stroke-width: 1.5;
}

.flow-caption {
    margin-top: 10px;
    font-size: var(--text-xs);
    color: var(--text-muted);
    text-align: center;
}

/* @toc Flowchart */
/* Flowchart — a no-library, server-laid-out SVG flow diagram. Nodes in top-down
 * layers with shapes (process / start-end pill / decision diamond / cylinder) and
 * curved connectors; every colour is a --flowchart-* token (config). The viewport
 * is a window flowchart.js upgrades to pan/zoom (without JS it just scrolls).
 * View: /resources/views/components/flowchart.blade.php. */

.flowchart {
    margin: 0;
}

.flowchart-viewport {
    position: relative;
    overflow: auto;
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--flowchart-background-color);
}

.flowchart.is-grab .flowchart-viewport {
    cursor: grab;
    overflow: hidden;
}

.flowchart.is-grabbing .flowchart-viewport {
    cursor: grabbing;
}

.flowchart-canvas {
    transform-origin: 0 0;
}

.flowchart-svg {
    display: block;
    max-width: 100%;
    height: auto;
    font-family: var(--font-sans);
}

.flowchart-node {
    fill: var(--flowchart-node-background-color);
    stroke: var(--flowchart-node-border);
    stroke-width: 1.5;
}

.flowchart-node.is-accent {
    fill: var(--flowchart-accent-background-color);
    stroke: var(--flowchart-accent-background-color);
}

.flowchart-cylinder-top {
    fill: none;
    stroke: var(--flowchart-node-border);
    stroke-width: 1.5;
}

.flowchart-label {
    fill: var(--flowchart-node-text);
    font-size: var(--text-sm);
    font-weight: 500;
}

.flowchart-label.is-accent {
    fill: var(--flowchart-accent-text);
}

.flowchart-note {
    fill: var(--flowchart-note-text);
    font-size: var(--text-xs);
}

.flowchart-note.is-accent {
    fill: var(--flowchart-accent-text);
}

.flowchart-edge {
    fill: none;
    stroke: var(--flowchart-edge);
    stroke-width: 1.5;
}

.flowchart-arrowhead {
    fill: none;
    stroke: var(--flowchart-edge);
    stroke-width: 1.5;
}

.flowchart-edge-label {
    fill: var(--flowchart-edge-label);
    font-size: var(--text-xs);
}

.flowchart-caption {
    margin-top: 10px;
    font-size: var(--text-xs);
    color: var(--text-muted);
    text-align: center;
}

/* @toc Footer */
.footer {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: var(--footer-gap, 1rem);
    padding: var(--footer-padding, 2rem 1rem);
    color: var(--text-muted);
    font-size: 0.85rem;
}

.footer-link {
    color: inherit;
}

/* @toc Game catalog */
/*
 * x-game-catalog is mobile-first and container-first. Its immutable HTML reflows on the component's own width;
 * no line clamp hides long game copy, and every state keeps a visible text label independent of colour.
 */
.game-catalog {
    container-type: inline-size;
    color: var(--text);
}

.game-catalog-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-5);
}

.game-catalog-card {
    position: relative;
    min-width: 0;
    overflow: visible;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--global-border-radius);
}

.game-catalog-card-link,
.game-catalog-card-body {
    display: flex;
    flex-direction: column;
    height: 100%;
    color: inherit;
    text-decoration: none;
}

.game-catalog-card-link {
    border-radius: inherit;
}

.game-catalog-card-link:hover .game-catalog-title {
    color: var(--link);
    text-decoration: underline;
    text-underline-offset: 0.15em;
}

.game-catalog-card-link:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 3px;
}

.game-catalog-decoration {
    position: absolute;
    z-index: 1;
    inset-block-start: var(--space-3);
    inset-inline-end: var(--space-3);
    width: 5rem;
    height: auto;
    color: var(--text-muted);
    pointer-events: none;
}

.game-catalog-media {
    display: grid;
    min-height: 10rem;
    place-items: center;
    overflow: hidden;
    background: var(--surface-muted);
    border-block-end: 1px solid var(--line);
    border-start-start-radius: var(--global-border-radius);
    border-start-end-radius: var(--global-border-radius);
}

.game-catalog-image {
    display: block;
    width: 100%;
    height: auto;
    max-height: 20rem;
    object-fit: contain;
}

.game-catalog-no-image {
    max-width: 18ch;
    margin: 0;
    padding: var(--space-6) var(--space-4);
    color: var(--text-muted);
    font-size: var(--text-sm);
    font-weight: 600;
    text-align: center;
}

.game-catalog-copy {
    display: flex;
    flex: 1;
    flex-direction: column;
    gap: var(--space-3);
    min-width: 0;
    padding: var(--space-5);
}

.game-catalog-eyebrow,
.game-catalog-title,
.game-catalog-summary,
.game-catalog-production-label {
    margin: 0;
    overflow-wrap: anywhere;
}

.game-catalog-eyebrow {
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
}

.game-catalog-title {
    color: var(--text);
    font-size: var(--text-xl);
    line-height: 1.2;
}

.game-catalog-summary {
    color: var(--text-muted);
    line-height: 1.6;
}

.game-catalog-chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin: 0;
    padding: 0;
    list-style: none;
}

.game-catalog-chip {
    display: inline-flex;
    align-items: center;
    min-height: 1.75rem;
    padding-inline: var(--space-2);
    background: var(--surface-muted);
    border: 1px solid var(--line);
    border-radius: 999px;
    color: var(--text);
    font-size: var(--text-xs);
    font-weight: 700;
}

.game-catalog-chip.is-info,
.game-catalog-chip.is-primary {
    background: var(--info-surface);
    border-color: var(--info);
}

.game-catalog-chip.is-success {
    background: var(--success-surface);
    border-color: var(--success);
}

.game-catalog-chip.is-warning {
    background: var(--warning-surface);
    border-color: var(--warning);
}

.game-catalog-chip.is-danger {
    background: var(--danger-surface);
    border-color: var(--danger);
}

.game-catalog-production {
    display: grid;
    gap: var(--space-2);
    margin-block-start: auto;
    padding-block-start: var(--space-2);
    border-block-start: 1px solid var(--line);
}

.game-catalog-production-label {
    color: var(--text-muted);
    font-size: var(--text-sm);
    font-weight: 600;
}

.game-catalog-progress {
    width: 100%;
    accent-color: var(--primary);
}

.game-catalog-empty {
    margin: 0;
    padding: var(--space-6);
    color: var(--text-muted);
    text-align: center;
    background: var(--surface-muted);
    border: 1px dashed var(--line);
    border-radius: var(--global-border-radius);
}

@container (min-width: 40rem) {
    .game-catalog-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

@container (min-width: 64rem) {
    .game-catalog-grid {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }
}

@media (forced-colors: active) {
    .game-catalog-chip,
    .game-catalog-card,
    .game-catalog-empty {
        border-color: CanvasText;
    }
}

/* @toc Game */
/* <x-game> — the canvas-island chrome (X-GAME-EMBED-CONTRACT.md §7). Non-threaded, so ZERO page headers
   (§8). The island is authored container-first: the aspect box reserves space before boot (no layout shift
   when a multi-MB export loads), and the chrome reflows on the island's OWN box via @container — never a
   viewport media query. Colors come only from the ramp/semantic tokens; every --game-* token has a fallback
   so the island themes even before `game` is added to a domain manifest. */

.game {
    display: block;
    container-type: inline-size;
    inline-size: 100%;
}

.game-viewport {
    position: relative;
    inline-size: 100%;
    aspect-ratio: var(--game-aspect, 16 / 9);
    max-block-size: 80vh;
    overflow: hidden;
    border-radius: var(--game-border-radius, 8px);
    background: var(--game-poster-background-color, var(--surface));
    color: var(--game-poster-color, var(--text));
}

/* The game surface — shown only once running; never in the tab order, focusable for the Escape hatch. */
.game-canvas {
    position: absolute;
    inset: 0;
    display: none;
    inline-size: 100%;
    block-size: 100%;
    touch-action: none;
    outline: none;
}

/* Poster + overlays fill the box and stack their content centered; the state machine reveals exactly one. */
.game-poster,
.game-overlay {
    position: absolute;
    inset: 0;
    display: none;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--game-chrome-gap, var(--space-2));
    padding: var(--game-chrome-padding, var(--space-3));
    text-align: center;
}

.game-poster {
    background-color: var(--game-poster-background-color, var(--surface));
    background-size: cover;
    background-position: center;
}

/* A scrim so text/controls read over a poster image — bg color-mix on a ramp token, within the color law. */
.game-poster::before {
    content: "";
    position: absolute;
    inset: 0;
    background: color-mix(in oklab, var(--game-overlay, var(--neutral-950)) 45%, transparent);
}

.game-poster-body {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--game-chrome-gap, var(--space-2));
    max-inline-size: 44ch;
}

.game-name {
    margin: 0;
    font-weight: 600;
    font-size: clamp(1rem, 2.5cqi, 1.375rem);
}

.game-prose {
    margin: 0;
    color: var(--text-muted);
    font-size: clamp(0.8125rem, 1.8cqi, 0.9375rem);
    line-height: 1.5;
}

.game-overlay {
    background: color-mix(in oklab, var(--surface) 86%, transparent);
    backdrop-filter: blur(2px);
}

.game-overlay-text {
    margin: 0;
    font-weight: 600;
}

.game-error-text {
    color: var(--danger);
    max-inline-size: 46ch;
}

/* Loading bar — determinate while a total is known, native indeterminate animation otherwise (§3). */
.game-progress {
    inline-size: min(60%, 20rem);
    block-size: 0.5rem;
    appearance: none;
    border: none;
    border-radius: var(--radius-full, 999px);
    background: var(--game-progress-track, var(--line));
    overflow: hidden;
}

.game-progress::-webkit-progress-bar {
    background: var(--game-progress-track, var(--line));
}

.game-progress::-webkit-progress-value {
    background: var(--game-progress-fill, var(--primary));
    transition: inline-size 0.15s linear;
}

.game-progress::-moz-progress-bar {
    background: var(--game-progress-fill, var(--primary));
}

/* Chrome — real <x-button>s, top-inline-end; shown while running/paused so it rides fullscreen. */
.game-chrome {
    position: absolute;
    inset-block-start: var(--game-chrome-padding, var(--space-3));
    inset-inline-end: var(--game-chrome-padding, var(--space-3));
    display: none;
    gap: var(--game-chrome-gap, var(--space-2));
    z-index: 2;
}

.game-icon {
    display: block;
    inline-size: 1.25rem;
    block-size: 1.25rem;
}

/* Paused host control — non-covering, explicit player intent; the game's own menu still owns the canvas. */
.game-paused-control {
    position: absolute;
    inset-block-start: var(--game-chrome-padding, var(--space-3));
    inset-inline-start: var(--game-chrome-padding, var(--space-3));
    display: none;
    align-items: center;
    gap: var(--game-chrome-gap, var(--space-2));
    padding-block: var(--space-1);
    padding-inline: var(--space-2);
    border-radius: var(--game-border-radius, 8px);
    background: color-mix(in oklab, var(--surface) 85%, transparent);
    color: var(--text);
    font-size: 0.75rem;
    font-weight: 600;
    z-index: 2;
}

.game-paused-badge {
    margin: 0;
}

/* The keyboard hint; flashes on the Escape release (§4). */
.game-hint {
    position: absolute;
    inset-block-end: var(--game-chrome-padding, var(--space-3));
    inset-inline-start: 50%;
    transform: translateX(-50%);
    margin: 0;
    padding-block: var(--space-1);
    padding-inline: var(--space-2);
    border-radius: var(--game-border-radius, 8px);
    background: color-mix(in oklab, var(--surface) 85%, transparent);
    color: var(--text-muted);
    font-size: 0.75rem;
    white-space: nowrap;
    opacity: 0;
    transition: opacity 0.2s ease;
    pointer-events: none;
}

.game[data-state="poster"] .game-hint,
.game[data-state="running"] .game-hint {
    opacity: 0.85;
}

.game:fullscreen .game-hint {
    opacity: 0.85;
}

.game-hint.is-flash {
    animation: game-hint-flash 1.1s ease;
}

@keyframes game-hint-flash {
    0% {
        opacity: 1;
        transform: translateX(-50%) scale(1.04);
    }

    100% {
        opacity: 0.85;
        transform: translateX(-50%) scale(1);
    }
}

/* The state machine — one visible layer per state. */
.game[data-state="poster"] .game-poster {
    display: flex;
}

.game[data-state="booting"] .game-loading,
.game[data-state="loading"] .game-loading {
    display: flex;
}

.game[data-state="running"] .game-canvas,
.game[data-state="pausing"] .game-canvas,
.game[data-state="paused"] .game-canvas,
.game[data-state="resuming"] .game-canvas {
    display: block;
}

.game[data-state="paused"] .game-paused-control {
    display: flex;
}

.game[data-state="exited"] .game-exited {
    display: flex;
}

.game[data-state="error"] .game-error {
    display: flex;
}

.game[data-state="running"] .game-chrome,
.game[data-state="pausing"] .game-chrome,
.game[data-state="paused"] .game-chrome,
.game[data-state="resuming"] .game-chrome {
    display: flex;
}

/* Fullscreen: the OUTER island goes fullscreen so the live status stays in the top layer (§7.3). */
.game:fullscreen {
    inline-size: 100%;
    block-size: 100%;
    background: var(--game-poster-background-color, var(--surface));
}

.game:fullscreen .game-viewport {
    max-block-size: none;
    aspect-ratio: auto;
    inline-size: 100%;
    block-size: 100%;
    border-radius: 0;
    background: var(--game-poster-background-color, var(--surface));
}

/* Screen-reader-only status line (announces state transitions). */
.game-status {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* Chrome reflow on the island's own box (component-internal responsiveness — the modernity contract). */
@container (max-width: 420px) {
    .game-poster-body {
        gap: var(--space-1);
    }

    .game-hint {
        font-size: 0.6875rem;
        white-space: normal;
        max-inline-size: 80cqi;
        text-align: center;
    }
}

@media (prefers-reduced-motion: reduce) {
    .game-hint,
    .game-hint.is-flash {
        animation: none;
        transition: none;
    }

    .game-progress::-webkit-progress-value {
        transition: none;
    }
}

/* ---- the rotate prompt (#1458) --------------------------------------------------------------

   A landscape game on a portrait phone. The engine gates play itself and draws its own message,
   but inside its letterboxed band — on a 19.5:9 screen that band is about a fifth of the height,
   so the instruction ends up as small as the problem. This one is the page's, across the whole
   island, at page text size.

   Driven by `data-rotate` on the wrapper rather than a media query, because the condition is
   "coarse pointer AND portrait" — a narrow desktop window is still a mouse and must never see it,
   and CSS alone cannot express the pairing as cheaply as the island already computes it. */

.game[data-rotate] .game-rotate {
    display: flex;
    /* Above every other layer: it is the reason nothing else is worth reading right now. */
    z-index: 3;
    /* ★ IT INFORMS, IT DOES NOT INTERCEPT (#1460). Covering the island made it swallow every tap
       underneath — including the fullscreen button, which on a portrait iPhone is the one control the
       player needs to reach. Found by driving the real button in WebKit: the click never landed because
       this element was on top of it. There is nothing interactive in here, so it has no business taking
       pointer events at all. */
    pointer-events: none;
    /* OPAQUE, not a scrim. Over the poster the Play button and the hint ghosted through behind the
       message — two sets of instructions at once, one of which you cannot act on yet. The same
       token the poster uses, so the island keeps one background. */
    background: var(--game-poster-background-color, var(--surface));
}

.game-rotate-detail {
    margin: var(--space-2) 0 0;
    color: var(--text-muted);
    font-size: var(--text-sm);
    text-align: center;
    text-wrap: balance;
}

/* ---- immersive mode (#1460) -------------------------------------------------------------------

   iOS Safari implements no element Fullscreen API at all — not the standard one, not a prefixed
   one (measured on WebKit, with Chromium as a control). So on an iPhone the fullscreen button was
   a silent no-op. This is the fallback the button uses wherever the API is missing: not a
   simulation of fullscreen, just the game filling the viewport, which is what was actually asked
   for.

   ★ 100dvh, NEVER 100vh. On iOS `100vh` is the viewport with the URL bar COLLAPSED, so a 100vh
   layer is taller than what you can see and its bottom edge sits behind the browser chrome — which
   is precisely where the touch controls live. `dvh` tracks the dynamic viewport, so the controls
   stay on screen while the bar shows and hides. */

.game[data-immersive] {
    position: fixed;
    inset: 0;
    z-index: 999;
    width: 100%;
    height: 100dvh;
    max-width: none;
    margin: 0;
    border-radius: 0;
    background: var(--neutral-950);
}

/* The aspect box is what makes the island a letterboxed card in the page. In immersive the viewport
   IS the box, so the ratio is released and the canvas takes the whole layer. */
.game[data-immersive] .game-viewport {
    aspect-ratio: auto;
    width: 100%;
    height: 100%;
    /* ★ RELEASE THE 80vh CAP — THIS ONE LINE IS THE WHOLE FULLSCREEN WIN (#1463).
       The base rule carries `max-block-size: 80vh` so an embedded island never dominates a scrolling page.
       In immersive the island IS the page, and that cap was still applying: MEASURED at 844x390, the
       immersive layer correctly filled 100dvh = 390, and the viewport inside it was 312 — exactly 80% of
       390. Those 78 CSS px are OURS, not browser chrome, and they are not cosmetic.
       Integer scaling picks floor(min(bufferW/960, bufferH/540)). At 312 css the buffer is 936 device px,
       936/540 = 1.7, so scale 1 → the picture renders 960x540. At the full 390 the buffer is 1170,
       1170/540 = 2.2, so scale 2 → 1920x1080. One CSS line quadruples the picture's area, with 960x540,
       120px Dante and integer scaling all untouched. */
    max-block-size: none;
}

/* THE WAY OUT AND THE HINT BOTH LIVE INSIDE THE SAFE AREA. Without this the exit control sits under
   the notch on a landscape iPhone and the hint under the home indicator — the button that gets you
   out being the one the hardware covers.

   ★ THIS OVERRIDES THE PADDING SEAM, NOT THE POSITIONS. Setting `left`/`right` here broke the hint,
   which centres itself with `inset-inline-start: 50%` + a translate — the override fought the
   centring and left the text clipped against the edge (caught in the iPhone screenshot). Both the
   chrome and the hint already read `--game-chrome-padding`, so feeding that one value is enough and
   cannot fight their own layout. One conservative value covers every edge: the largest inset the
   device reports, so nothing lands under the notch, the home indicator, or a rounded corner. */
.game[data-immersive] {
    --game-chrome-padding: max(
        var(--space-3),
        env(safe-area-inset-top),
        env(safe-area-inset-right),
        env(safe-area-inset-bottom),
        env(safe-area-inset-left)
    );
}

/* The page behind must not scroll under the layer while it is up. */
:root:has(.game[data-immersive]) body {
    overflow: hidden;
}

/* ---- the indeterminate loading bar (#1460) ----------------------------------------------------

   James: "the loading bar doesn't even work when you click play". It does not, and this is why.

   Godot's `onProgress` reports total=0 whenever the response carries no Content-Length (gzip and
   chunked transfers, which is the normal case over a network), so the island correctly drops the
   `value` attribute and the element becomes INDETERMINATE. But `appearance: none` above — needed to
   style the bar at all — also removes the browser's own indeterminate animation, and an
   indeterminate <progress> has no value for `::-webkit-progress-value` to fill. The result is a
   static empty track for the entire download: on a ~40 MB payload over mobile data that is
   indistinguishable from a hang, and he closes the tab.

   Measured, not assumed: a real boot traced `indeterminate` for every sample from click to running.

   So the indeterminate state gets its own motion. It cannot say HOW far along it is — we genuinely
   do not know — but it must say that something is happening, which is the honest claim. */

.game-progress:not([value]) {
    background-image: linear-gradient(
        90deg,
        transparent 0%,
        var(--game-progress-fill, var(--primary)) 50%,
        transparent 100%
    );
    background-repeat: no-repeat;
    background-size: 40% 100%;
    animation: game-progress-sweep 1.1s linear infinite;
}

@keyframes game-progress-sweep {
    from { background-position: -40% 0; }
    to { background-position: 140% 0; }
}

/* Reduced motion still needs to distinguish "working" from "stalled", so it keeps a visible fill
   and drops only the movement. */
@media (prefers-reduced-motion: reduce) {
    .game-progress:not([value]) {
        animation: none;
        background-position: 50% 0;
    }
}

/* ★ [hidden] MUST BEAT x-button, AT EVERY WIDTH (#1462). `<x-button>` sets `display: inline-flex` as an
   AUTHOR rule, and an author rule beats the UA stylesheet's `[hidden] { display: none }` — so a control
   the island had hidden still measured `inline-flex` and still took space. The first fix put this
   neutraliser inside a `max-width` media block, which meant it simply did not apply in landscape: the
   controls were hidden in portrait and visible-but-wrong above 768px. Top level, no media query. */
.game-chrome [hidden] {
    display: none;
}

/* ★ THE CHROME ICONS HAD NO SIZE (#1462). `.game-icon` sets 1.25rem but nothing ever applied that class —
   MediaService::icon() emits a bare <svg>, so every chrome button rendered as an EMPTY PILL. Pre-existing,
   not new: the fullscreen control has been a blank capsule since it shipped, and it only became obvious
   once three more buttons sat beside it. Sized here rather than by threading a class through the seam, so
   the rule holds for whatever the icon seam emits next. */
.game-chrome svg {
    inline-size: 1.25rem;
    block-size: 1.25rem;
}

/* @toc Generation candidates */
/*
 * <x-generation-candidates> (#1375) — the candidate-slot grid and the verdict pass.
 *
 * MOBILE FIRST, AND THE BAR IS 320 CSS PIXELS. James reviews on a phone, so the base rules ARE the small layout
 * and wider screens are the enhancement. Every token is read with a fallback, so the component still renders
 * legibly on a host that has not manifested it yet — the failure mode that makes a surface look structurally
 * broken when it is only unthemed.
 *
 * Colour comes from the ramp: semantic tokens first, and the checker squares are neutral rungs rather than
 * hand-mixed greys.
 */

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

/* ── DELIVERY: the numbered slot grid ─────────────────────────────────────────────────────────────────────── */

/* TWO UP AT 320 is the base, declared as an explicit track count rather than auto-fill: auto-fill re-derives the
   column count from whatever width it is given, and at 320 minus page padding that silently collapses to one —
   which is a list, not a grid. An explicit count cannot drift when a container's padding changes. */
.gencand-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-3);
    margin: 0;
    padding: 0;
    list-style: none;
}

@media (min-width: 600px) {
    .gencand-grid {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }
}

@media (min-width: 900px) {
    .gencand-grid {
        grid-template-columns: repeat(4, minmax(0, 1fr));
    }
}

.gencand-slot {
    display: flex;
    min-inline-size: 0;
}

/*
 * THE SLOT YOU CAN ACTUALLY DELIVER TO TAKES THE WHOLE ROW. Found by looking at 320, not by measuring: the
 * native file control needs ~198px of content and a half-width cell gives it 114, so "No file chosen" was
 * clipped mid-word and the control read as broken. It never OVERFLOWED — every parent-overflow check passed —
 * which is exactly why the picture was the thing that caught it.
 *
 * Widening the one actionable slot is also the honest hierarchy: it is the slot being worked on, so it earns
 * the room, and every other slot stays a scannable tile.
 */
.gencand-slot-wide {
    grid-column: 1 / -1;
}

/*
 * A CLOSED slot is EXCLUDED, not outstanding. The owner deliberately ended the round without it, so nothing
 * will ever arrive — and it counts as resolved for the job state. Dimming it says "settled, and not part of
 * the set" in a way a neutral badge alone does not; a closed slot that still looks pending sends someone
 * hunting for a delivery that is never coming.
 *
 * Dimmed, never hidden: the slot and its thesis remain readable, because "we deliberately skipped this one"
 * is part of the round's record and removing it would quietly rewrite what was explored.
 */
.gencand-slot-closed .gencand-slot-body {
    opacity: 0.6;
}

.gencand-slot-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    inline-size: 100%;
    min-inline-size: 0;
    padding: var(--space-3);
    background: var(--surface-raised);
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
}

.gencand-slot-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
}

.gencand-slot-number {
    font-size: var(--text-sm);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
}

/* A SQUARE, RESERVED picture box. It holds its size whether or not a candidate has landed, so a grid mid-delivery
   does not reflow under the reader as images arrive one slot at a time. */
.gencand-thumb {
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    border-radius: var(--radius-md);
    background-color: var(--neutral-100);
    background-image:
        linear-gradient(45deg, var(--neutral-200) 25%, transparent 25%),
        linear-gradient(-45deg, var(--neutral-200) 25%, transparent 25%),
        linear-gradient(45deg, transparent 75%, var(--neutral-200) 75%),
        linear-gradient(-45deg, transparent 75%, var(--neutral-200) 75%);
    background-size: 16px 16px;
    background-position: 0 0, 0 8px, 8px -8px, -8px 0;
}

.gencand-thumb img {
    inline-size: 100%;
    block-size: 100%;
    object-fit: contain;
}

.gencand-thumb-empty {
    padding: var(--space-2);
    font-size: var(--text-xs);
    font-weight: 600;
    text-align: center;
    color: var(--text-muted);
}

.gencand-thesis {
    margin: 0;
    font-size: var(--text-sm);
    line-height: 1.4;
    color: var(--text);
}

.gencand-claim {
    margin: 0;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.gencand-qa-list {
    margin: 0;
    padding-inline-start: var(--space-4);
    font-size: var(--text-xs);
}

.gencand-slot-actions {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin-block-start: auto;
}

/* 44px is the acceptance floor for a touch target, so it is a min-block-size rather than padding that a longer
   label could quietly eat into. */
.gencand-tap {
    min-block-size: 44px;
}

.gencand-deliver {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

/* ── REVIEW: one candidate at a time ──────────────────────────────────────────────────────────────────────── */

.gencand-review {
    display: flex;
    flex-direction: column;
}

.gencand-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.gencand-actor {
    margin: 0;
    font-size: var(--text-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
}

.gencand-thesis-lead {
    font-size: var(--text-base);
    font-weight: 600;
}

/* The candidate sits over a transparency checker because the output contract REQUIRES a real alpha channel — a
   flat backdrop would conceal exactly the defect the reviewer is checking for. */
.gencand-stage {
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    border-radius: var(--radius-md);
    border: 1px solid var(--line);
    background-color: var(--neutral-100);
    background-image:
        linear-gradient(45deg, var(--neutral-200) 25%, transparent 25%),
        linear-gradient(-45deg, var(--neutral-200) 25%, transparent 25%),
        linear-gradient(45deg, transparent 75%, var(--neutral-200) 75%),
        linear-gradient(-45deg, transparent 75%, var(--neutral-200) 75%);
    background-size: 20px 20px;
    background-position: 0 0, 0 10px, 10px -10px, -10px 0;
}

.gencand-stage img {
    inline-size: 100%;
    block-size: 100%;
    object-fit: contain;
}

/*
 * THE VERDICT BAR STAYS IN THUMB REACH AND ABOVE THE DEVICE SAFE AREA (acceptance item 1). It is sticky rather
 * than fixed so it cannot overlay the shortlist below it, and the bottom padding is max(space, safe-area) so a
 * phone with a home indicator does not bury the No button under the gesture strip.
 *
 * The three actions share the row equally: they are pressed by thumb, repeatedly, and an uneven row makes the
 * middle one the accidental default.
 */
.gencand-verdicts {
    position: sticky;
    inset-block-end: 0;
    z-index: 1;
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--space-2);
    padding-block: var(--space-2);
    padding-block-end: max(var(--space-2), env(safe-area-inset-bottom));
    background: var(--surface);
    border-block-start: 1px solid var(--line);
}

.gencand-verdict {
    inline-size: 100%;
}

/* ── the closing shortlist ────────────────────────────────────────────────────────────────────────────────── */

.gencand-shortlist-head {
    margin: var(--space-4) 0 var(--space-2);
    font-size: var(--text-sm);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
}

.gencand-shortlist-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin: 0;
    padding: 0;
    list-style: none;
}

.gencand-shortlist-item {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
    padding: var(--space-2) var(--space-3);
    background: var(--surface-raised);
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
}

.gencand-shortlist-thesis {
    flex: 1 1 8rem;
    min-inline-size: 0;
    font-size: var(--text-sm);
}

.gencand-full img {
    inline-size: 100%;
    block-size: auto;
}

.gencand-full figcaption {
    margin-block-start: var(--space-2);
    font-size: var(--text-sm);
    color: var(--text-muted);
}

/* @toc Input */
/* Input. All --input-* tokens come from config/components/input.php['css']. */

.input {
    margin-bottom: 14px;
}

.input-label {
    display: block;
    margin-bottom: 6px;
    color: var(--input-label-color);
    font-size: var(--input-label-font-size);
}

.input-field {
    width: 100%;
    padding: var(--input-padding-y) var(--input-padding-x);
    border: 1px solid var(--input-border);
    border-radius: var(--input-border-radius);
    background: var(--input-background-color);
    color: var(--input-color);
    font-size: var(--input-font-size);
    font-family: var(--font-sans);
}

/* opacity:1 stops the UA re-fading the placeholder below the token's contrast. */
.input-field::placeholder {
    color: var(--text-muted);
    opacity: 1;
}

/* Invalid / hint / required affordances. Error colour is the categorical --danger
 * token (red-700 light / red-400 dark) — self-flipping and menu-hover-certified, so
 * the text clears 4.5:1 and the border 3:1 on every surface without a per-theme rule. */
.input-field[aria-invalid="true"] {
    border-color: var(--danger);
}

.input-error {
    margin-top: 6px;
    font-size: var(--text-xs);
    color: var(--danger);
}

.input-hint {
    margin-top: 6px;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.input-required {
    margin-left: 2px;
    color: var(--danger);
}

/* Keyboard focus recolors the border as a secondary cue; the global :focus-visible
 * ring (base.css) supplies the primary indicator. :focus-visible (not :focus) so a
 * mouse click doesn't trigger either. */
.input-field:focus-visible {
    border-color: var(--input-focus-border);
}

/* Disabled — a native attribute (it arrives via the passthrough channel). Dim + not-allowed so the field
 * reads inert against the tokened bg/fg instead of the UA default. No color literals: opacity carries it,
 * and disabled controls are exempt from the contrast minimum (WCAG 1.4.3). */
.input-field:disabled {
    opacity: var(--disabled-opacity);
    cursor: not-allowed;
}

/* @toc Invoice doc */
/* Invoice document — the print-ready 83 Consulting invoice + engineering log, authored as a themed
 * page: real palette vars (var(--neutral-*)/var(--primary-*)) from PaletteService, so it themes per
 * tenant and stays consistent on screen and on paper. Rasterized to a TAGGED PDF by browser/render.js.
 * Neutral ink on white; only the brand mark carries primary. @media print drops the app chrome. */

.invoice-doc {
    max-width: 780px;
    margin: 0 auto;
    padding: 40px 44px;
    background: var(--paper);
    color: var(--neutral-900);
    border: 1px solid var(--line);
    border-radius: 12px;
    font-size: 14px;
    line-height: 1.5;
    font-variant-numeric: tabular-nums;
}

.invoice-doc p {
    margin: 0;
}

/* Page 1 fills the printable area (letter content box = 792pt - 2·54pt margin ≈ 684pt; trimmed a hair
 * to avoid a rounding overflow onto a blank page), so the footer can sit at the foot of the page. */
.inv-page {
    display: flex;
    flex-direction: column;
    min-height: 672pt;
}

/* --- header --- */
.inv-head {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 24px;
    padding-bottom: 22px;
    border-bottom: 1.5px solid var(--neutral-900);
}

.inv-brand {
    display: flex;
    align-items: center;
    gap: 13px;
}

.inv-brand .brand-logo svg,
.inv-log-brand .brand-logo svg {
    display: block;
    width: 40px;
    height: 40px;
}

.inv-issuer-name {
    font-size: 17px;
    font-weight: 700;
}

.inv-issuer {
    text-align: right;
    color: var(--neutral-500);
    font-size: 12px;
    line-height: 1.5;
}

.inv-title {
    font-size: 26px;
    font-weight: 800;
    color: var(--neutral-900);
    letter-spacing: -0.01em;
    margin-bottom: 6px !important;
}

/* --- parties + meta --- */
.inv-parties {
    display: flex;
    justify-content: space-between;
    gap: 32px;
    margin: 26px 0 30px;
}

.inv-label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--neutral-500);
    margin-bottom: 8px !important;
}

.inv-client {
    font-weight: 700;
    font-size: 15px;
}

.inv-billto p {
    color: var(--neutral-700);
}

.inv-meta {
    min-width: 250px;
    margin: 0;
    font-size: 13px;
}

.inv-meta > div {
    display: flex;
    justify-content: space-between;
    gap: 28px;
    padding: 4px 0;
}

.inv-meta dt {
    margin: 0;
    color: var(--neutral-500);
}

.inv-meta dd {
    margin: 0;
    text-align: right;
    color: var(--neutral-700);
    white-space: nowrap;
}

/* --- items: the line-items table is now <x-table>; re-dress its .table markup as the invoice
   document table (invoice-scoped, so the app data-table look never leaks in and vice-versa). The
   print surface forces the light theme, so the neutral rungs read as a dark bar on white paper. --- */
.invoice-doc .table-wrap {
    overflow: visible;
}

.invoice-doc .table {
    width: 100%;
    border-collapse: collapse;
    font-size: inherit;
}

.invoice-doc .table th {
    padding: 8px 14px;
    font-size: 10.5px;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    text-align: left;
    color: var(--paper);
    background: var(--neutral-900);
    border-bottom: none;
    white-space: nowrap;
}

.invoice-doc .table th.table-right {
    text-align: right;
}

.invoice-doc .table td {
    padding: 13px 14px;
    border-bottom: 1px solid var(--neutral-200);
    vertical-align: top;
}

.invoice-doc .table td.table-right {
    text-align: right;
    white-space: nowrap;
}

/* The stacked "Items" cell (Cell::stacked): the description bold over the muted period. */
.invoice-doc .cell-stacked > div:first-child {
    font-weight: 700;
}

.invoice-doc .cell-stacked > div + div {
    margin-top: 2px;
    color: var(--neutral-500);
    font-size: 13px;
}

/* --- totals --- */
.inv-totals-wrap {
    display: flex;
    justify-content: flex-end;
    margin-top: 20px;
}

.inv-totals {
    min-width: 250px;
    margin: 0;
}

.inv-total-row {
    display: flex;
    justify-content: space-between;
    gap: 24px;
    padding: 5px 14px;
}

.inv-total-row dt {
    margin: 0;
    color: var(--neutral-500);
}

.inv-total-row dd {
    margin: 0;
    text-align: right;
    color: var(--neutral-700);
    white-space: nowrap;
}

/* amount due — a highlighted bar (gray fill, no border) spanning the label + figure row */
.inv-totals-due {
    margin-top: 5px;
    padding-top: 10px;
    padding-bottom: 10px;
    background: var(--neutral-100);
}

.inv-totals-due dt {
    color: var(--neutral-900);
    font-weight: 700;
    font-size: 15px;
}

.inv-totals-due dd {
    color: var(--neutral-900);
    font-weight: 800;
    font-size: 15px;
}

/* --- footer (foot of the invoice page) --- */
.inv-foot {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    gap: 24px;
    margin-top: auto;
    padding-top: 14px;
    border-top: 1px solid var(--neutral-200);
    font-size: 11px;
    color: var(--neutral-500);
}

.inv-foot-issuer {
    text-align: right;
    font-weight: 600;
}

/* --- engineering log — its own page --- */
.inv-log {
    break-before: page;
    padding-top: 4px;
}

.inv-log-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 26px;
}

.inv-log-brand {
    display: inline-flex;
    align-items: center;
    gap: 11px;
    font-size: 13px;
    font-weight: 700;
}

.inv-log-brand .brand-logo svg {
    width: 30px;
    height: 30px;
}

.inv-log-num {
    color: var(--neutral-500);
    font-size: 12px;
}

.inv-log-h {
    margin: 0 0 4px;
    font-size: 22px;
    font-weight: 800;
    color: var(--neutral-900);
}

.inv-log-sub {
    margin: 0 0 18px !important;
    padding-bottom: 14px;
    border-bottom: 1px solid var(--neutral-200);
    color: var(--neutral-500);
    font-size: 12px;
}

.inv-log-entry {
    margin-bottom: 16px;
    break-inside: avoid;
}

.inv-log-title {
    font-weight: 700;
    font-size: 13px;
    margin-bottom: 5px !important;
}

.inv-log-bullets {
    margin: 0;
    padding-left: 18px;
    color: var(--neutral-700);
    font-size: 12px;
    line-height: 1.5;
}

.inv-log-bullets li {
    margin-bottom: 3px;
}

/* --- screen chrome: toolbar, list, form (all theme-tokened; hidden or irrelevant on print) --- */
.inv-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: var(--space-5);
}

.inv-tb-back {
    font-size: var(--text-sm);
    color: var(--text-muted);
    text-decoration: none;
}

.inv-tb-back:hover {
    color: var(--text);
}

.inv-tb-actions {
    display: flex;
    align-items: center;
    gap: 8px;
}

.inv-tb-actions form {
    margin: 0;
}

.inv-tb-btn {
    padding: 8px 14px;
    font-size: var(--text-sm);
    font-weight: 600;
    background: var(--surface-raised, var(--neutral-100));
    border: 1px solid var(--line);
    border-radius: 8px;
    color: var(--text);
    text-decoration: none;
    cursor: pointer;
}

.inv-tb-btn:hover {
    border-color: var(--line-strong, var(--neutral-400));
}

.inv-tb-btn.is-primary {
    background: var(--primary-600, var(--palette-rose-600));
    border-color: transparent;
    color: var(--on-primary);
}

/* The undo/revert action (Revert to draft) — lower-emphasis than the solid forward
   buttons: muted text + a dashed border set it apart as a step back, not forward. */
.inv-tb-btn.is-undo {
    color: var(--text-muted);
    border-style: dashed;
}

.inv-tb-btn.is-undo:hover {
    color: var(--text);
}

.inv-status {
    display: inline-block;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding: 3px 9px;
    border-radius: 20px;
}

.inv-status.is-draft {
    background: var(--neutral-200, #e5e5e5);
    color: var(--text-muted);
}

.inv-status.is-sent {
    background: var(--warning-surface);
    color: var(--warning);
}

.inv-status.is-paid {
    background: var(--success-surface);
    color: var(--success);
}

.inv-list-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: var(--space-5);
}

.inv-list {
    display: flex;
    flex-direction: column;
}

.inv-row {
    display: block;
    padding: 14px 12px;
    border-bottom: 1px solid var(--line);
    border-radius: 8px;
    color: var(--text);
    text-decoration: none;
}

.inv-row:hover,
.inv-row:active {
    background: var(--surface-raised, var(--neutral-100));
}

.inv-row-main {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
}

.inv-row-num {
    font-size: 15px;
    font-weight: 700;
}

.inv-row-amt {
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.inv-row-sub {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    flex-wrap: wrap;
    margin-top: 4px;
    font-size: var(--text-sm);
    color: var(--text-muted);
}

.inv-form {
    max-width: 720px;
}

.inv-form-head {
    display: flex;
    align-items: baseline;
    gap: 10px;
    margin-bottom: var(--space-4);
}

.inv-form-num {
    font-size: 20px;
    font-weight: 800;
}

.inv-form-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 14px;
    margin-bottom: 16px;
}

.inv-form label {
    display: flex;
    flex-direction: column;
    gap: 5px;
    font-size: var(--text-sm);
    font-weight: 600;
}

.inv-form input,
.inv-form textarea {
    padding: 9px 12px;
    font-size: var(--text-sm);
    font-weight: 400;
    background: var(--surface-raised, var(--neutral-100));
    border: 1px solid var(--line);
    border-radius: 8px;
    color: var(--text);
    font-family: inherit;
}

.inv-form input:focus,
.inv-form textarea:focus {
    outline: none;
    border-color: var(--primary-500, var(--palette-rose-500));
}

.inv-form-log {
    display: flex;
    flex-direction: column;
    gap: 5px;
}

.inv-form-log textarea {
    resize: vertical;
    line-height: 1.5;
}

.inv-form-actions {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
    margin-top: 18px;
}

/* --- invoice view page: mobile-friendly summary + desktop PDF embed --- */
.inv-view {
    max-width: 720px;
}

.inv-view-card {
    padding: 20px 22px;
    background: var(--surface-raised, var(--neutral-100));
    border: 1px solid var(--line);
    border-radius: 14px;
}

.inv-view-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 14px;
}

.inv-view-logo {
    display: inline-flex;
}

.inv-view-logo svg {
    display: block;
    width: 44px;
    height: auto;
    border-radius: 8px;
}

.inv-view-num {
    margin: 0;
    font-size: 22px;
    font-weight: 800;
}

.inv-view-sub {
    margin: 2px 0 20px;
    color: var(--text-muted);
}

.inv-view-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px;
    margin: 0 0 22px;
}

.inv-view-amtbox {
    grid-column: 1 / -1;
}

.inv-view-grid dt {
    margin-bottom: 4px;
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
}

.inv-view-grid dd {
    margin: 0;
    font-weight: 600;
}

.inv-view-amt {
    font-size: 26px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}

.inv-view-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
}

.inv-view-actions form {
    margin: 0;
}

.inv-view-back {
    margin: 16px 0 0;
}

.inv-view-back a {
    color: var(--text-muted);
    font-size: var(--text-sm);
    text-decoration: none;
}

.inv-view-embed {
    display: none;
    width: 100%;
    border: 1px solid var(--line);
    border-radius: 12px;
}

/* 900px: invoice document layout breakpoint; document-specific, not the app seam. */
@media (min-width: 900px) {
    .inv-view {
        max-width: 900px;
    }

    .inv-view-embed {
        display: block;
        height: 1000px;
        margin-top: 24px;
    }
}

/* --- print: drop the app chrome, print just the document --- */
@media print {
    .app-shell-bar,
    .sidebar-nav,
    .app-shell-backdrop,
    .app-header-bar,
    .inv-toolbar {
        display: none !important;
    }

    .app-shell-main,
    .app-shell-wrap {
        padding: 0 !important;
        margin: 0 !important;
    }

    .invoice-doc {
        max-width: none;
        border: none;
        border-radius: 0;
        padding: 0;
    }
}

/* @toc Issue card */
/* Issue card (BarBruiser reader core-four #2) — the D4 merged issue. The card is a native disclosure
 * (x-details); this sheet is the card's own skin over that. STRUCTURE is locked; the ring/radius/padding +
 * teach typography are TBD-direction (they tune at James's A/B/C pick — B seams cards as sections inside ONE
 * ring container, a shell concern, so the per-card contract here is unchanged). Tokens only. */

/* Qualified with the element (the card root IS a <details> from x-details) so the card's own ring/radius/bg
 * beat x-details' `.details` chrome by SPECIFICITY (0,1,1 > 0,1,0), not by bundle order — otherwise the
 * override (and the trap ring below) would silently depend on issue-card.css emitting after details.css. */
details.issue-card {
    border: 1px solid var(--issue-card-ring-color);
    border-radius: var(--issue-card-border-radius);
    background: var(--surface);
    overflow: hidden;
}

/* We compose x-details, whose .details-body already carries its own padding. Zero it so the card's own
 * .issue-card-body is the SINGLE padded container — otherwise the two nest and the body gutter compounds. */
.issue-card .details-body {
    padding: 0;
}

/* The summary header — the weight badge + the "Issue {n}: {label}" name, laid inline. x-details owns the
 * chevron + the toggle; we only lay out the label row. */
.issue-card-head {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
}

.issue-card-name {
    font-weight: 600;
    color: var(--text);
}

/* Trap identity: the danger badge + triangle lead it (locked); the card takes a quiet danger-tinted ring so
 * the trap reads at a glance without a second colour decision. */
details.issue-card-trap {
    border-color: color-mix(in srgb, var(--danger) 32%, var(--line));
}

/* The body sits below the summary, inside the card padding, as a stack of the rule list, the teach, the
 * evidence chips, and the note. */
.issue-card-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    padding: var(--issue-card-padding);
    padding-block-start: var(--space-2);
}

/* Rules — the black-letter list; each row is the rule text with an optional trailing law-mark. */
.issue-card-rules {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.issue-card-rule {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    flex-wrap: wrap;
}

.issue-card-rule-text {
    color: var(--text);
    line-height: 1.5;
}

/* Law-mark — the success-family affordance (Addendum D row 4). A success-tinted CHIP (text on -surface, the
 * AA-vetted pair — bare success text is not menu-certified), leading with the book icon. Emits issuecard:law. */
.issue-card-lawmark {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding-block: 2px;
    padding-inline: var(--space-2);
    border: 1px solid transparent;
    border-radius: var(--radius-control);
    background: var(--success-surface);
    color: var(--success);
    font-size: var(--text-xs);
    font-weight: 600;
    cursor: pointer;
}

.issue-card-lawmark:hover {
    border-color: color-mix(in srgb, var(--success) 40%, transparent);
}

.issue-card-lawmark:focus-visible {
    outline: 2px solid var(--success);
    outline-offset: 2px;
}

.issue-card-lawmark-icon {
    display: inline-flex;
}

.issue-card-lawmark-icon svg {
    width: 14px;
    height: 14px;
}

/* Teach — the IRAC (rule/apply/conclude) or the trap string pair; the reading rhythm is the tunable knob. */
.issue-card-teach {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    line-height: var(--issue-card-teach-line-height);
    color: var(--text);
}

.issue-card-irac {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.issue-card-irac-label {
    font-size: var(--text-2xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
}

/* Lang-pair (following the x-lang-pair contract): both nodes render; the RU node carries lang="ru" and is
 * shown by default (teach surfaces default en-ru). The reader-level data-lang toggle (set by the shell on
 * the reader root) collapses to EN only. */
.issue-card-lang {
    display: block;
}

.issue-card-lang-ru {
    color: var(--text-muted);
}

:where([data-lang="en"]) .issue-card-lang-ru {
    display: none;
}

/* Evidence chips — the answers this issue was seen in; neutral badges (never status colours). */
.issue-card-evidence {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
}

.issue-card-evidence-label {
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--text-muted);
}

.issue-card-note {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--text-muted);
    font-style: italic;
}

/* @toc Legend schedule */
/* Legend schedule — the span-bearing issues as a collapsible, activatable schedule (number · swatch ·
 * label · call). STRUCTURE is locked; skin (fills, treatments) waits on the direction pick. The swatch reads
 * the emitted --annotation-<n> slot token by data-annotation — NEVER a per-component hue (PaletteService is
 * the governed home). Config: config/components/legend-schedule.php (--legend-schedule-swatch-size / -radius).
 * View: legend-schedule.blade.php; row dispatch: legend-schedule.js. */

.legend-schedule {
    border: 1px solid var(--line);
    border-radius: var(--legend-schedule-border-radius, var(--radius-control));
    background: var(--surface-raised);
    overflow: hidden;
}

.legend-summary {
    padding: var(--space-2) var(--space-3);
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--text);
    cursor: pointer;
}

.legend-summary-static {
    margin: 0;
    cursor: default;
}

.legend-count {
    color: var(--text-muted);
    font-weight: 400;
}

.legend-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--text-sm);
}

.legend-table thead th {
    padding: var(--space-1) var(--space-3);
    font-size: var(--text-xs);
    font-weight: 600;
    text-align: start;
    color: var(--text-muted);
    border-block: 1px solid var(--line);
}

.legend-cell-issue {
    padding: 0;
    font-weight: 400;
    text-align: start;
    border-bottom: 1px solid var(--line);
}

.legend-call {
    padding-inline: var(--space-3);
    text-align: start;
    color: var(--text-muted);
    white-space: nowrap;
    border-bottom: 1px solid var(--line);
}

/* The row activation target — a full-width button (swatch · number · label). */
.legend-row {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    width: 100%;
    padding: var(--space-2) var(--space-3);
    background: transparent;
    border: 0;
    color: var(--text);
    font: inherit;
    text-align: start;
    cursor: pointer;
}

.legend-row:hover {
    background: color-mix(in srgb, var(--text) 6%, transparent);
}

.legend-number {
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    color: var(--text-muted);
}

.legend-label {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The swatch — decorative; its hue IS the annotation slot token (data-annotation = the marker number 1..6),
 * so it reads --annotation-<n> from PaletteService. --line is the out-of-range fallback. */
.legend-swatch {
    flex-shrink: 0;
    width: var(--legend-schedule-swatch-size, 12px);
    height: var(--legend-schedule-swatch-size, 12px);
    border-radius: var(--radius-sm);
    background: var(--line);
}

.legend-swatch[data-annotation="1"] { background: var(--annotation-1); }
.legend-swatch[data-annotation="2"] { background: var(--annotation-2); }
.legend-swatch[data-annotation="3"] { background: var(--annotation-3); }
.legend-swatch[data-annotation="4"] { background: var(--annotation-4); }
.legend-swatch[data-annotation="5"] { background: var(--annotation-5); }
.legend-swatch[data-annotation="6"] { background: var(--annotation-6); }

/* @toc Link */
/*
 * Link. A themed native <a>: accent colour, underline-on-hover, a visible
 * :focus-visible ring, and a trailing external-link icon. Every token is a
 * --link-* var (emitted from config/components/link.php['css']) with a global
 * token fallback, so the config is the full theming surface. Dark-first.
 */

.link {
    color: var(--link-color, var(--link));
    text-decoration: none;
    border-radius: var(--link-border-radius, 2px);
    transition: color 0.15s ease, text-decoration-color 0.15s ease;
}

.link:hover {
    text-decoration: underline;
    text-decoration-thickness: var(--link-underline-thickness, 1px);
    text-underline-offset: var(--link-underline-offset, 2px);
}

/* muted — sits quietly inline; reads as text until hovered, then it accents. */
.link-muted {
    color: var(--link-muted-color, var(--text-muted));
}

.link-muted:hover {
    color: var(--link-muted-color-hover, var(--text));
}

/* The visible ring is a box-shadow (hugs the text box). The transparent outline
 * is a forced-colors fallback: box-shadow is dropped in high-contrast mode, so
 * the outline gets promoted to a system colour and focus stays visible. */
.link:focus-visible {
    outline: 2px solid transparent;
    outline-offset: 2px;
    box-shadow: 0 0 0 3px var(--link-focus-ring, var(--primary));
}

/* External affordance — a trailing icon that inherits the link colour, aligned
 * to the text baseline and never wrapping away from the last word. */
.link-external-icon {
    display: inline-block;
    margin-left: 2px;
    vertical-align: -0.125em;
    white-space: nowrap;
}

.link-external-icon svg {
    display: inline-block;
    width: var(--link-icon-size, 0.9em);
    height: var(--link-icon-size, 0.9em);
}

/* @toc Login */
/* Login component. All --login-* tokens from config/components/login.php['css']. */

.login {
    width: 100%;
    max-width: 360px;
    padding: var(--login-padding);
    background: var(--login-background-color);
    color: var(--login-color);
    border: 1px solid var(--login-border);
    border-radius: var(--login-border-radius);
}

.login-title {
    margin: 0 0 18px;
    font-size: var(--text-xl);
    font-weight: 500;
}

.login-error {
    margin-bottom: 12px;
    color: var(--login-error-color);
    font-size: var(--text-sm);
}

.login .checkbox {
    margin: 14px 0 4px;
    font-size: var(--text-sm);
}

.login .button {
    width: 100%;
    justify-content: center;
    margin-top: 6px;
}

@keyframes login-shake {
    0%, 100% { transform: translateX(0); }
    25% { transform: translateX(-6px); }
    75% { transform: translateX(6px); }
}

/* @toc Media preview */
/* MediaPreview — the full-screen inspector. The <dialog> chrome comes from dialog.css; everything here is
 * the stage, the image transform and the backing controls.
 *
 * The stage fills the dialog and clips, so a zoomed image is inspected through a fixed window rather than
 * growing the page. `touch-action: none` is load-bearing: without it the browser claims the drag for its own
 * scroll/pinch and neither pan nor pinch-zoom ever reaches the island.
 */

.media-preview .dialog-panel {
    block-size: 100dvh;
    display: flex;
    flex-direction: column;
    inline-size: 100vw;
    max-block-size: 100dvh;
    max-inline-size: 100vw;
}

.media-preview-stage {
    align-items: center;
    cursor: grab;
    display: flex;
    flex: 1 1 auto;
    justify-content: center;
    min-block-size: 0;
    overflow: hidden;
    touch-action: none;
}

.media-preview-stage:active {
    cursor: grabbing;
}

/* THE THREE BACKINGS — an alpha-edge instrument. A pale fringe invisible on checker is obvious on black,
   which is the entire reason these exist. */
.media-preview-stage[data-backing='checker'] {
    background-color: var(--media-preview-checker-base);
    background-image:
        linear-gradient(45deg, var(--media-preview-checker) 25%, transparent 25%),
        linear-gradient(-45deg, var(--media-preview-checker) 25%, transparent 25%),
        linear-gradient(45deg, transparent 75%, var(--media-preview-checker) 75%),
        linear-gradient(-45deg, transparent 75%, var(--media-preview-checker) 75%);
    background-position: 0 0, 0 12px, 12px -12px, -12px 0;
    background-size: 24px 24px;
}

.media-preview-stage[data-backing='white'] {
    background: var(--media-preview-white);
}

.media-preview-stage[data-backing='black'] {
    background: var(--media-preview-black);
}

/* transform-origin centre + no transition: a transition on transform makes a drag lag behind the pointer. */
.media-preview-image {
    display: block;
    max-block-size: none;
    max-inline-size: none;
    transform-origin: center center;
    will-change: transform;
}

.media-preview-controls {
    align-items: center;
    border-block-start: 1px solid var(--media-preview-line, var(--line));
    display: flex;
    flex-wrap: wrap;
    gap: var(--media-preview-gap, 8px);
    justify-content: space-between;
    padding: var(--media-preview-controls-padding, 12px);
}

.media-preview-backings {
    border: 0;
    display: flex;
    gap: var(--media-preview-gap, 8px);
    margin: 0;
    padding: 0;
}

.media-preview-unbuilt {
    color: var(--media-preview-unbuilt-color, var(--text-muted));
    margin: 0;
    padding: var(--media-preview-controls-padding, 12px);
}

/* ── THE PROVENANCE PANEL (#1457) ──────────────────────────────────────────────────────────────────────
 * ONE element, placed by CSS at the container width: a bottom sheet on a phone, a side drawer on desktop.
 * The DOM never changes across widths (the Section-Authoring Law), which is also what keeps the open/closed
 * state from being lost when a rotation crosses the breakpoint.
 *
 * Mobile-first, so the base rules ARE the sheet.
 */
.media-preview-provenance {
    background: var(--media-preview-panel-background);
    border-block-start: 1px solid var(--media-preview-line);
    inline-size: 100%;
    max-block-size: 60dvh;
    overflow-y: auto;
    padding: var(--media-preview-controls-padding);
}

.media-preview-provenance[hidden] {
    display: none;
}

@container (min-width: 48rem) {
    /* Desktop: the same element pinned to the inline end as a side drawer. */
    .media-preview-provenance {
        border-block-start: 0;
        border-inline-start: 1px solid var(--media-preview-line);
        block-size: 100%;
        inline-size: var(--media-preview-panel-width);
        inset-block: 0;
        inset-inline-end: 0;
        max-block-size: none;
        position: absolute;
    }
}

.media-preview-actions {
    display: flex;
    gap: var(--media-preview-gap);
}

.media-preview-icon {
    block-size: 1em;
    inline-size: 1em;
}

.media-preview-h {
    color: var(--media-preview-heading-color);
    font-size: var(--media-preview-heading-font-size);
    margin-block: var(--media-preview-gap) 4px;
}

/* pre-wrap, because a prompt is authored text whose line breaks carry meaning — and it must wrap rather
   than force the panel to scroll sideways on a phone. */
.media-preview-prompt {
    background: var(--media-preview-prompt-background);
    border-radius: 6px;
    font-size: var(--media-preview-body-font-size);
    margin: 0;
    padding: var(--media-preview-gap);
    white-space: pre-wrap;
    word-break: break-word;
}

.media-preview-fields {
    display: grid;
    gap: 4px;
    margin: var(--media-preview-gap) 0 0;
}

.media-preview-field {
    display: flex;
    font-size: var(--media-preview-body-font-size);
    gap: var(--media-preview-gap);
    justify-content: space-between;
}

.media-preview-field dt {
    color: var(--media-preview-label-color);
}

.media-preview-field dd {
    margin: 0;
    overflow-wrap: anywhere;
    text-align: end;
}

/* A recorded absence, styled as information rather than as a gap. */
.media-preview-null {
    color: var(--media-preview-label-color);
    font-style: italic;
}

.media-preview-empty,
.media-preview-note {
    color: var(--media-preview-label-color);
    font-size: var(--media-preview-body-font-size);
    margin: 4px 0 0;
}

.media-preview-references {
    font-size: var(--media-preview-body-font-size);
    margin: 0;
    padding-inline-start: 1.2em;
}

/* @toc Media strip */
/* MediaStrip — one asset row: required slots above, generated candidates below.
 *
 * MOBILE-FIRST AND CONTAINER-FIRST, per the Section-Authoring Law: the base rules ARE the 390px layout and
 * the tile grows with the container rather than the viewport, so the row previews truthfully at any pane
 * width. The tile is sized in `cqi` with a clamp, which is what keeps an image large enough to JUDGE on a
 * phone — the whole point of the surface — without a media query.
 *
 * ★ THE TWO TRACKS SHARE ONE TILE RULE. Required boxes and candidates are the same geometry by construction,
 * not by two rules kept in sync; the row's entire meaning is the visual comparison between them.
 */

.media-strip {
    container-type: inline-size;
    display: flex;
    flex-direction: column;
    gap: var(--media-strip-gap, 8px);
    padding-block: var(--media-strip-row-padding, 16px);
    border-block-end: 1px solid var(--media-strip-line, var(--line));
}

.media-strip-head {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.media-strip-name {
    color: var(--media-strip-name-color, var(--text));
    font-size: var(--media-strip-name-font-size, var(--text-sm));
    font-weight: 600;
    margin: 0;
}

.media-strip-purpose {
    color: var(--media-strip-purpose-color, var(--text-muted));
    font-size: var(--media-strip-purpose-font-size, var(--text-xs));
    margin: 0;
}

/* The row scrolls sideways INSIDE itself. `min-width: 0` is what stops a long track forcing the page wide —
   without it a flex child refuses to shrink below its content and the whole page scrolls horizontally, which
   the card explicitly forbids. */
.media-strip-track {
    display: flex;
    gap: var(--media-strip-gap, 8px);
    min-width: 0;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    padding-block-end: 4px;
}

.media-strip-tile {
    display: flex;
    flex: 0 0 auto;
    flex-direction: column;
    gap: 4px;
    scroll-snap-align: start;
}

/* ONE geometry, both strips. */
.media-strip-frame {
    aspect-ratio: 1;
    background-color: var(--media-strip-tile-background, var(--surface-raised));
    border: 1px solid var(--media-strip-line, var(--line));
    border-radius: var(--media-strip-border-radius, 8px);
    display: block;
    /* SIZED SO A PHONE CAN JUDGE. At 390px this lands around 160px — roughly two tiles in view, which is the
       deliberate trade: a strip that fits four tiles on a phone shows four images too small to tell apart, and
       scanning sideways is cheap while squinting is not. The ceiling stops a desktop row becoming a wall. */
    inline-size: clamp(132px, 46cqi, 200px);
    overflow: hidden;
    padding: 0;
}

/* Checker is the ORDINARY tile background — it is how an alpha edge reads at a glance, before anyone opens
   the preview. The preview's white/black controls exist to interrogate the same edge more harshly. */
.media-strip-tile img {
    background-image:
        linear-gradient(45deg, var(--media-strip-checker) 25%, transparent 25%),
        linear-gradient(-45deg, var(--media-strip-checker) 25%, transparent 25%),
        linear-gradient(45deg, transparent 75%, var(--media-strip-checker) 75%),
        linear-gradient(-45deg, transparent 75%, var(--media-strip-checker) 75%);
    background-position: 0 0, 0 8px, 8px -8px, -8px 0;
    background-size: 16px 16px;
    block-size: 100%;
    inline-size: 100%;
    object-fit: contain;
}

button.media-strip-frame {
    cursor: zoom-in;
}

button.media-strip-frame:focus-visible {
    outline: 2px solid var(--media-strip-focus, var(--primary-500));
    outline-offset: 2px;
}

/* An unfilled required slot: a dashed outline over nothing. `transparent` is a keyword rather than a token
   because there is no colour here to theme — the tile is deliberately empty, and a token would imply a
   configurable fill that should never be set. */
.media-strip-tile-empty .media-strip-frame {
    background-color: transparent;
    border-style: dashed;
}

/* OUTSIDE the artwork, lower-right — never painted onto the image being judged. */
.media-strip-number {
    color: var(--media-strip-number-color, var(--text-muted));
    font-size: var(--media-strip-number-font-size, var(--text-xs));
    font-variant-numeric: tabular-nums;
    margin: 0;
    text-align: end;
}

.media-strip-empty {
    color: var(--media-strip-purpose-color, var(--text-muted));
    font-size: var(--media-strip-purpose-font-size, var(--text-xs));
    margin: 0;
}

/* @toc Mps ingredient */
/* Meal Prep Sundays — the Food Library (index + detail). Green/light, mobile-first.
 * Colours from the --palette-* catalog so it themes under the eightythree.com host. */

/* --- index --- */
.mps-ings {
    padding: 40px 24px 72px;
}

.mps-ings-title {
    margin: 0 0 10px;
    font-size: 36px;
    font-weight: 900;
    color: var(--palette-slate-900);
}

.mps-ings-lead {
    margin: 0 0 28px;
    max-width: 60ch;
    font-size: 18px;
    line-height: 1.5;
    color: var(--palette-slate-500);
}

.mps-ings-cat {
    margin: 28px 0 12px;
    font-size: 14px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--palette-green-700);
}

/* THE WALL (#1365) — mobile-first, and the phone IS the test: James reviews on one.
   TWO COLUMNS AT 375 IS THE BASE, declared explicitly rather than left to auto-fill. The previous
   `auto-fill, minmax(180px, 1fr)` gave ONE column on a phone — 2 tracks needed 372px and the content box is
   327px at 375 (24px of .mps-ings padding each side) — so the wall rendered as a single-file list, which is
   what this card exists to fix. An explicit track count cannot drift if that padding changes; auto-fill would
   silently fall back to one column again. Widths climb from there. */
.mps-ings-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 12px;
}

@media (min-width: 560px) {
    .mps-ings-grid {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }
}

@media (min-width: 820px) {
    .mps-ings-grid {
        grid-template-columns: repeat(4, minmax(0, 1fr));
    }
}

@media (min-width: 1100px) {
    .mps-ings-grid {
        grid-template-columns: repeat(5, minmax(0, 1fr));
    }
}

/* The card is PICTURE-LED: a square image is the card, the name sits beneath it. It was a 48px thumb beside
   the name, which reads as a list row — "cards, not a list" (James). The picture carries the recognition, so
   the wall is scanned by sight rather than read line by line. */
.mps-ing-card {
    display: flex;
    flex-direction: column;
    background: #fff;
    border: 1px solid var(--palette-slate-200);
    border-radius: 12px;
    overflow: hidden;
    text-decoration: none;
    transition: border-color 0.12s ease, transform 0.12s ease;
}

.mps-ing-card:hover {
    border-color: var(--palette-green-400);
    transform: translateY(-2px);
}

/* A SQUARE picture slot: aspect-ratio keeps every card the same height whether the picture is there or not,
   so a row never goes ragged where the 35 artwork-less staples fall. */
.mps-ing-card-thumb {
    display: flex;
    align-items: center;
    justify-content: center;
    aspect-ratio: 1 / 1;
    overflow: hidden;
    background: var(--palette-green-50);
}

.mps-ing-card-thumb img {
    inline-size: 100%;
    block-size: 100%;
    object-fit: cover;
}

/* NO PHOTO YET — an admitted gap, not a blank tile. A bare empty square reads as broken or still-loading,
   which states something false about the ingredient; naming the absence is the picture-side equivalent of
   omitting the calorie line. Muted on purpose: it must recede behind the real photographs beside it. */
.mps-ing-card-nophoto {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    color: var(--palette-green-700);
    opacity: 0.55;
}

.mps-ing-card-nophoto-icon svg {
    inline-size: 22px;
    block-size: 22px;
}

.mps-ing-card-nophoto-text {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.02em;
}

.mps-ing-card-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-inline-size: 0;
    padding: 10px 12px 12px;
}

/* The longest name in the set is 22 characters ("Whole wheat bread loaf"), which wraps to two lines in a
   ~150px card — so the name is allowed to wrap and the card grows; it is never truncated, because a clipped
   ingredient name is a worse failure than an uneven row. */
.mps-ing-card-name {
    font-size: 15px;
    font-weight: 800;
    line-height: 1.25;
    color: var(--palette-slate-900);
}

.mps-ing-card-cal {
    font-size: 13px;
    color: var(--palette-slate-500);
}

/* --- detail --- */
.mps-ing {
    padding: 28px 24px 72px;
}

.mps-ing-back {
    display: inline-block;
    margin-bottom: 16px;
    font-size: 14px;
    font-weight: 700;
    color: var(--palette-green-700);
    text-decoration: none;
}

.mps-ing-back:hover {
    text-decoration: underline;
}

.mps-ing-cat {
    margin: 0 0 4px;
    font-size: 13px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--palette-green-600);
}

.mps-ing-title {
    margin: 0 0 8px;
    font-size: 38px;
    font-weight: 900;
    color: var(--palette-slate-900);
}

.mps-ing-aliases {
    margin: 0 0 8px;
    font-size: 15px;
    color: var(--palette-slate-500);
}

.mps-ing-cols {
    display: grid;
    grid-template-columns: 1fr;
    gap: 28px;
    margin-top: 24px;
}

/* 880px: MPS content-column breakpoint (intentional, shared across the recipe layout); not the app --bp-* seam. */
@media (min-width: 880px) {
    .mps-ing-cols {
        grid-template-columns: 1fr 280px;
    }
}

.mps-ing-h2 {
    margin: 28px 0 10px;
    font-size: 20px;
    font-weight: 800;
    color: var(--palette-slate-900);
}

.mps-ing-h2 span {
    font-weight: 500;
    font-size: 15px;
    color: var(--palette-slate-400);
}

.mps-ing-note {
    margin: 0 0 10px;
    max-width: 62ch;
    font-size: 16px;
    line-height: 1.6;
    color: var(--palette-slate-600);
}

.mps-ing-src {
    color: var(--palette-green-700);
    font-weight: 700;
    text-decoration: underline;
    text-underline-offset: 2px;
}

.mps-ing-src:hover {
    color: var(--palette-green-800);
}

/* Nutrition — a <dl> now rather than divs of <b>/<span>: each cell is a value and its label, which is what a
   description list is for, and it reads correctly to a screen reader instead of as four bold orphans.
   TWO ACROSS ON A PHONE, four once there is room: at 375px four cells give ~78px each, which crushes
   "protein (g)" onto three lines. Mobile-first, so two is the base and four is the enhancement. */
.mps-ing-nut {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
    max-width: 480px;
    margin: 0;
}

.mps-ing-nut-cell {
    display: flex;
    flex-direction: column;
    padding: 14px;
    background: var(--palette-green-50);
    border: 1px solid var(--palette-green-100);
    border-radius: 12px;
}

/* dd first, dt second — the number leads and the label explains it. Reset the dd's UA indent. */
.mps-ing-nut-cell dd {
    margin: 0;
    font-size: 24px;
    font-weight: 900;
    color: var(--palette-green-700);
    font-variant-numeric: tabular-nums;
}

.mps-ing-nut-cell dt {
    font-size: 12px;
    color: var(--palette-slate-500);
}

@media (min-width: 480px) {
    .mps-ing-nut {
        grid-template-columns: repeat(4, minmax(0, 1fr));
    }
}

/* By weight — also a <dl>, replacing a two-column <table>. It was never tabular data: it is a list of terms
   ("1 slice") and their definitions ("≈ 28 g"), and the table also tripped the raw-markup census. */
.mps-ing-units {
    margin: 12px 0 0;
    max-width: 320px;
}

.mps-ing-unit {
    display: flex;
    justify-content: space-between;
    gap: 16px;
    padding: 8px 0;
    border-bottom: 1px solid var(--palette-slate-100);
}

.mps-ing-unit dt {
    color: var(--palette-slate-700);
}

.mps-ing-unit dd {
    margin: 0;
    font-weight: 700;
    color: var(--palette-slate-900);
    font-variant-numeric: tabular-nums;
}

/* 880px: MPS content-column breakpoint (intentional, shared across the recipe layout); not the app --bp-* seam. */
@media (min-width: 880px) {
    .mps-ing-side {
        position: sticky;
        top: 88px;
        align-self: start;
    }
}

/* Picture slot */
/* SQUARE, because the artwork is square (256/512/1024 are all 1:1). The old 16/10 slot cropped a third off a
   square photo of an ingredient, which is the part of the picture you actually want. Capped so it does not
   dominate a phone screen: the picture introduces the ingredient, the words are the page. */
.mps-ing-photo {
    aspect-ratio: 1 / 1;
    max-inline-size: 22rem;
    margin-bottom: 18px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--palette-green-100);
    border-radius: 16px;
    overflow: hidden;
}

.mps-ing-photo img {
    inline-size: 100%;
    block-size: 100%;
    object-fit: cover;
    display: block;
}

.mps-ing-photo-empty {
    font-size: 14px;
    font-weight: 700;
    color: var(--palette-green-700);
    opacity: 0.7;
}

/* Blurb */
.mps-ing-blurb {
    margin: 0 0 8px;
    max-width: 64ch;
    font-size: 18px;
    line-height: 1.6;
    color: var(--palette-slate-700);
}

/* Tips rail (pick / store / cook / grow) */
.mps-ing-side {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.mps-ing-tip {
    padding: 16px;
    background: #fff;
    border: 1px solid var(--palette-slate-200);
    border-radius: 14px;
}

.mps-ing-tip-h {
    margin: 0 0 6px;
    font-size: 12px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--palette-green-700);
}

.mps-ing-tip-b {
    margin: 0;
    font-size: 14px;
    line-height: 1.55;
    color: var(--palette-slate-600);
}

/* --- recipes index (#1355) ---------------------------------------------------------------------
 * The browse page that did not exist, so a "Recipes" nav item had somewhere to point. Reuses the
 * .mps-recipe-card the home grid already renders (mps-recipe.css); only the page frame lives here, and the
 * grid is a rule rather than a style attribute so this page and home can converge later without unpicking
 * inline CSS. */

.mps-recipes {
    padding: 40px 24px 72px;
}

.mps-recipes-title {
    margin: 0 0 10px;
    font-size: 36px;
    font-weight: 900;
    color: var(--palette-slate-900);
}

.mps-recipes-lead {
    margin: 0 0 28px;
    max-width: 60ch;
    font-size: 17px;
    line-height: 1.55;
    color: var(--palette-slate-500);
}

.mps-recipes-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: 16px;
}

.mps-recipes-empty {
    margin: 0;
    color: var(--palette-slate-500);
}

/* @toc Mps page */
/* Meal Prep Sundays tenant shell — Satoshi font, light/green theme (data-primary="green"
 * + data-neutral="slate"), a sticky header and a 2-tier footer. Scoped to body.mps so it
 * never touches eightythree.com. Brand greens come from the always-on --palette-green-*
 * catalog (the orange accent will use --palette-orange-* / --palette-amber-*). */

@font-face {
    font-family: 'Satoshi';
    src: url('/fonts/satoshi/Satoshi-Variable.woff2') format('woff2'),
         url('/fonts/satoshi/Satoshi-Variable.ttf') format('truetype');
    font-weight: 300 900;
    font-style: normal;
    font-display: swap;
}

/* #1361 — MPS renders inside <x-shell> now, so the BODY LAYOUT belongs to the shell and this rule keeps only
   the tenant's skin: type, ink, ground.
   What was removed and why it mattered: `display:flex; flex-direction:column; min-height:100vh` stacked the old
   header/main/footer document. `body.app-shell` sets `flex-direction:row; height:100vh` for the sidebar-beside-
   main frame — and both selectors are (0,1,1), so the winner is decided by BUNDLE ORDER alone. CssService globs
   `css/components/*.css` alphabetically, which puts mps-page.css after app-shell.css: the column would have won
   and dropped the sidebar above the content on every MPS page. Deleting the layout here is the fix; overriding
   it back would have left two rules fighting over one box. */
body.mps {
    margin: 0;
    font-family: 'Satoshi', system-ui, -apple-system, sans-serif;
    color: var(--palette-slate-800, #1e293b);
    background: var(--palette-green-50, #f0fdf4);
    -webkit-font-smoothing: antialiased;
}

/* #1361 — the brand mark in the shell's two chrome slots (the desktop sidebar, the phone header bar).
   Sized by HEIGHT with an auto inline-size, the same way BarBruiser sizes its own: the shell's defaults are
   `.app-shell-brand .brand-logo svg { width: 30px }` and `.sidebar-nav-brand .brand-logo svg { width: 40px }`,
   which are right for eightythree's SQUARE mark and wrong for a WORDMARK — the MPS logo is 223x20, so a fixed
   width squashes it to a few pixels tall.

   THE SELECTOR CARRIES .brand-logo TOO, on purpose. Without it these read (0,2,1) — exactly the specificity of
   the shell's own rules — so the winner would be decided by BUNDLE ORDER, and CssService globs
   `css/components/*.css` alphabetically: mps-page.css lands BEFORE sidebar-nav.css and AFTER app-shell.css.
   The header brand would size correctly and the sidebar brand would silently keep its 40px, rendering the
   wordmark squashed to 40x17 against a natural 187x17 — which is precisely what it did until this was measured.
   Adding the class makes these (0,3,1): they win on merit, in both files, whatever the manifest order becomes. */
.sidebar-nav-brand .brand-logo.mps-brand svg,
.app-shell-brand .brand-logo.mps-brand svg {
    display: block;
    block-size: 1.05rem;
    inline-size: auto;
    max-inline-size: 100%;
}

.mps-shell {
    width: 100%;
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 24px;
}

/* Header */
.mps-header {
    position: sticky;
    top: 0;
    z-index: 20;
    background: #fff;
    border-bottom: 2px solid var(--palette-slate-900, #0f172a);
}

.mps-header-inner {
    height: 72px;
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.mps-logo {
    display: inline-flex;
    align-items: center;
    text-decoration: none;
}

/* The wordmark scales with the viewport, clamped to a sensible min/max. */
.mps-logo img {
    display: block;
    width: clamp(150px, 22vw, 196px);
    height: auto;
}

.mps-nav {
    display: flex;
    gap: 28px;
}

.mps-nav a {
    font-weight: 700;
    font-size: 16px;
    text-decoration: none;
    color: var(--palette-slate-900, #0f172a);
}

.mps-nav a:hover {
    color: var(--palette-green-600, #16a34a);
}

/* Main — grows to fill so the footer sits at the bottom, never a gap below it. */
.mps-main {
    flex: 1 0 auto;
}

/* Footer */
.mps-footer {
    flex-shrink: 0;
}

.mps-footer-links {
    background: var(--palette-green-800, #166534);
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 14px 28px;
    padding: 28px 24px;
}

.mps-footer-links a {
    color: #fff;
    font-size: 14px;
    text-decoration: none;
}

.mps-footer-links a:hover {
    text-decoration: underline;
}

.mps-footer-copy {
    background: var(--palette-green-900, #14532d);
    color: rgba(255, 255, 255, 0.85);
    text-align: center;
    font-size: 12px;
    padding: 14px 24px;
}

@media (max-width: 640px) {
    .mps-header-inner {
        height: 56px;
    }

    .mps-shell {
        padding: 0 16px;
    }
}

/* --- responsive menu: inline nav on desktop, a hamburger drawer on mobile --- */
.mps-burger {
    display: none;
    flex-direction: column;
    justify-content: center;
    gap: 5px;
    width: 44px;
    height: 44px;
    padding: 11px 9px;
    cursor: pointer;
    background: #fff;
    border: 2px solid var(--palette-slate-900, #0f172a);
    border-radius: 8px;
    box-shadow: 2px 2px 0 var(--palette-slate-900, #0f172a);
}

.mps-burger span {
    display: block;
    height: 2px;
    background: var(--palette-slate-900, #0f172a);
    border-radius: 2px;
    transition: transform 0.18s, opacity 0.18s;
}

.mps-burger[aria-expanded="true"] span:nth-child(1) {
    transform: translateY(7px) rotate(45deg);
}

.mps-burger[aria-expanded="true"] span:nth-child(2) {
    opacity: 0;
}

.mps-burger[aria-expanded="true"] span:nth-child(3) {
    transform: translateY(-7px) rotate(-45deg);
}

.mps-drawer {
    display: flex;
    flex-direction: column;
    background: #fff;
    border-bottom: 2px solid var(--palette-slate-900, #0f172a);
}

.mps-drawer[hidden] {
    display: none;
}

.mps-drawer a {
    padding: 14px 24px;
    font-weight: 800;
    font-size: 18px;
    color: var(--palette-slate-900, #0f172a);
    text-decoration: none;
    border-top: 1px solid var(--palette-green-100, #dcfce7);
}

.mps-drawer a:hover {
    background: var(--palette-green-50, #f0fdf4);
}

@media (max-width: 768px) {
    .mps-nav {
        display: none;
    }

    .mps-burger {
        display: flex;
    }
}

@media (min-width: 769px) {
    .mps-drawer {
        display: none;
    }
}

/* --- neo-brutalist contrast (MPS only): thick near-black borders + hard offset shadows, no blur,
   so the cards/buttons/table have real punch. Scoped to body.mps; eightythree.com is untouched. --- */
body.mps {
    --mps-ink: var(--palette-slate-900, #0f172a);
    --mps-shadow: 4px 4px 0 var(--palette-slate-900, #0f172a);
    --mps-shadow-sm: 2px 2px 0 var(--palette-slate-900, #0f172a);
}

body.mps .mps-scaler,
body.mps .mps-nutrition,
body.mps .mps-mealkit,
body.mps .mps-scale-anchor,
body.mps .mps-modal,
body.mps .table-wrap {
    border: 2px solid var(--mps-ink);
    border-radius: 10px;
    box-shadow: var(--mps-shadow);
}

body.mps .mps-stepper-btn,
body.mps .mps-quick,
body.mps .mps-mealkit-cta,
body.mps .mps-modal-submit,
body.mps .mps-modal-cancel {
    border: 2px solid var(--mps-ink);
    box-shadow: var(--mps-shadow-sm);
}

body.mps .mps-stepper-btn:active,
body.mps .mps-quick:active,
body.mps .mps-mealkit-cta:active,
body.mps .mps-modal-submit:active,
body.mps .mps-modal-cancel:active {
    box-shadow: none;
    transform: translate(2px, 2px);
}

body.mps .table thead th {
    border-bottom: 2px solid var(--mps-ink);
    color: var(--mps-ink);
}

/* Tabs — a black rule under the bar, a thicker indicator, bolder active tab. */
body.mps .tabs-nav {
    border-bottom: 2px solid var(--mps-ink);
}

body.mps .tabs-tab.is-active {
    color: var(--mps-ink);
}

body.mps .tabs-tab.is-active .tabs-tab-label {
    font-weight: 800;
}

body.mps .tabs-indicator {
    height: 3px;
    background: var(--palette-green-600, #16a34a);
}

/* --- distinctive recipe card (the gallery): bordered, hard-shadow, image-top, highlighter badge --- */
.mps-recipe-card {
    display: block;
    background: #fff;
    border: 2px solid var(--palette-slate-900, #0f172a);
    border-radius: 12px;
    box-shadow: 4px 4px 0 var(--palette-slate-900, #0f172a);
    text-decoration: none;
    overflow: hidden;
    transition: transform 0.12s, box-shadow 0.12s;
}

.mps-recipe-card:hover {
    transform: translate(-2px, -2px);
    box-shadow: 6px 6px 0 var(--palette-slate-900, #0f172a);
}

.mps-recipe-card-img {
    display: block;
    width: 100%;
    aspect-ratio: 3 / 2;
    object-fit: cover;
    border-bottom: 2px solid var(--palette-slate-900, #0f172a);
    background: var(--palette-green-100, #dcfce7);
}

.mps-recipe-card-body {
    padding: 16px 18px;
}

.mps-recipe-card-title {
    display: block;
    font-weight: 900;
    font-size: 20px;
    line-height: 1.15;
    color: var(--palette-slate-900, #0f172a);
}

/* Green highlighter badge — the signature MPS marker swipe. */
.mps-recipe-card-badge {
    display: inline-block;
    margin-top: 10px;
    font-weight: 800;
    font-size: 13px;
    color: var(--palette-green-900, #14532d);
    background: var(--palette-green-200, #bbf7d0);
    padding: 3px 8px;
}

/* --- "Grow" tab: the recipe's home-growable produce (the vegetable class) --- */
.mps-grow-lead {
    margin: 0 0 12px;
    font-weight: 800;
    color: var(--palette-slate-900, #0f172a);
}

.mps-grow-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.mps-grow-list li {
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 16px;
    color: var(--palette-slate-800, #1e293b);
}

.mps-grow-class {
    font-size: 12px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--palette-green-800, #166534);
    background: var(--palette-green-100, #dcfce7);
    padding: 2px 7px;
    border-radius: 999px;
}

/* @toc Mps recipe */
/* Meal Prep Sundays — the recipe (weight-scaler) page. Light/green, mobile-first, two
 * columns on desktop (recipe + a sticky nutrition rail). Colours from the --palette-*
 * catalog so it themes correctly even when served under the eightythree.com host. */

.mps-recipe {
    padding: 28px 24px 72px;
}

.mps-recipe-back {
    display: inline-block;
    margin-bottom: 16px;
    font-size: 14px;
    font-weight: 700;
    color: var(--palette-green-700);
    text-decoration: none;
}

.mps-recipe-back:hover {
    text-decoration: underline;
}

.mps-recipe-title {
    margin: 0 0 10px;
    font-size: 36px;
    font-weight: 900;
    line-height: 1.1;
    color: var(--palette-slate-900);
}

.mps-recipe-desc {
    margin: 0 0 12px;
    max-width: 60ch;
    font-size: 18px;
    line-height: 1.5;
    color: var(--palette-slate-600);
}

.mps-recipe-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.mps-tag {
    font-size: 12px;
    font-weight: 700;
    color: var(--palette-green-700);
    background: var(--palette-green-100);
    padding: 3px 10px;
    border-radius: 999px;
}

.mps-recipe-video {
    aspect-ratio: 16 / 9;
    margin: 20px 0;
    border-radius: 14px;
    overflow: hidden;
}

.mps-recipe-video iframe {
    width: 100%;
    height: 100%;
    border: 0;
}

.mps-recipe-body {
    display: grid;
    grid-template-columns: 1fr;
    gap: 28px;
    margin-top: 24px;
}

/* 880px: MPS content-column breakpoint (intentional, shared across the recipe layout); not the app --bp-* seam. */
@media (min-width: 880px) {
    .mps-recipe-body {
        grid-template-columns: 1fr 300px;
    }
}

/* --- the scaler control --- */
.mps-scaler {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 12px 16px;
    margin-bottom: 8px;
    padding: 16px;
    background: #fff;
    border: 1px solid var(--palette-slate-200);
    border-radius: 14px;
}

.mps-scaler-label {
    font-weight: 700;
    color: var(--palette-slate-700);
}

.mps-stepper {
    display: flex;
    align-items: center;
    gap: 6px;
}

.mps-stepper-btn {
    width: 40px;
    height: 40px;
    font-size: 22px;
    font-weight: 700;
    color: #fff;
    background: var(--palette-green-600);
    border: 0;
    border-radius: 10px;
    cursor: pointer;
}

.mps-stepper-btn:hover {
    background: var(--palette-green-700);
}

.mps-stepper-val {
    min-width: 120px;
    text-align: center;
    font-weight: 800;
    color: var(--palette-slate-900);
}

.mps-stepper-val output {
    font-size: 22px;
    font-variant-numeric: tabular-nums;
}

.mps-scaler-quick {
    display: flex;
    gap: 6px;
}

.mps-quick {
    width: 38px;
    height: 38px;
    font-weight: 700;
    color: var(--palette-green-700);
    background: var(--palette-green-50);
    border: 1px solid var(--palette-green-200);
    border-radius: 10px;
    cursor: pointer;
}

.mps-quick:hover {
    background: var(--palette-green-100);
}

.mps-recipe-h2 {
    margin: 28px 0 12px;
    font-size: 22px;
    font-weight: 800;
    color: var(--palette-slate-900);
}

/* --- ingredient cells — the table chrome (scroll, header, zebra, borders) comes from the shared
   .table component; here we only style the recipe-specific cell content + the tap-to-scale affordance. */

/* Amount = imperial · metric in one cell, and the tap-to-scale target. */
.mps-ing-measure {
    white-space: nowrap;
    cursor: pointer;
    border-radius: 8px;
    font-weight: 700;
    color: var(--palette-slate-900);
}

.mps-ing-measure.is-volume {
    color: var(--palette-green-700);
}

.mps-ing-measure:hover,
.mps-ing-measure:focus-visible {
    background: var(--palette-green-50);
    outline: none;
}

/* Count = the friendly piece count (green). When a size equivalent exists it's a clickable button. */
.mps-ing-count {
    font-weight: 800;
    color: var(--palette-green-700);
}

.mps-ing-count.is-clickable {
    font: inherit;
    font-weight: 800;
    color: var(--palette-green-700);
    background: none;
    border: 0;
    padding: 0;
    cursor: pointer;
    text-decoration: underline dotted var(--palette-green-400);
    text-underline-offset: 3px;
}

/* The revealed size equivalent ("about a baseball"). */
.mps-ing-size {
    display: block;
    margin-top: 2px;
    font-size: 12px;
    color: var(--palette-slate-500);
}

/* Placeholder copy for the stubbed tabs (History / Grow / empty Procedure). */
.mps-tab-empty {
    padding: 24px 4px;
    color: var(--palette-slate-500);
    font-size: 14px;
}

/* "Now scaling off X" banner, shown after a scale-by-ingredient. */
.mps-scale-anchor {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 0 0 16px;
    padding: 10px 14px;
    background: var(--palette-green-100);
    border: 1px solid var(--palette-green-300);
    border-radius: 10px;
    font-weight: 700;
    color: var(--palette-green-800);
}

.mps-scale-anchor[hidden] {
    display: none;
}

.mps-scale-reset {
    margin-left: auto;
    font-size: 13px;
    font-weight: 700;
    color: var(--palette-green-700);
    background: none;
    border: none;
    cursor: pointer;
    text-decoration: underline;
}

/* Scale-by-ingredient modal (native <dialog>). */
.mps-modal {
    border: none;
    border-radius: 16px;
    padding: 0;
    max-width: 380px;
    width: calc(100% - 32px);
    background: #fff;
    box-shadow: 0 20px 60px rgba(15, 23, 42, 0.25);
}

.mps-modal::backdrop {
    background: rgba(15, 23, 42, 0.5);
}

.mps-modal-card {
    padding: 24px;
}

.mps-modal-title {
    margin: 0 0 6px;
    font-size: 20px;
    font-weight: 900;
    color: var(--palette-slate-900);
}

.mps-modal-title span {
    color: var(--palette-green-700);
    text-transform: capitalize;
}

.mps-modal-sub {
    margin: 0 0 14px;
    font-size: 14px;
    line-height: 1.5;
    color: var(--palette-slate-500);
}

.mps-modal-fields {
    display: flex;
    gap: 12px;
}

.mps-modal-field {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.mps-modal-unit {
    font-size: 11px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--palette-slate-500);
}

.mps-modal-input {
    width: 100%;
    box-sizing: border-box;
    font-size: 18px;
    font-weight: 700;
    padding: 10px 12px;
    border: 1px solid var(--palette-slate-300);
    border-radius: 10px;
}

.mps-modal-input:focus {
    outline: none;
    border-color: var(--palette-green-500);
}

.mps-modal-actions {
    display: flex;
    gap: 10px;
    justify-content: flex-end;
    margin-top: 16px;
}

.mps-modal-cancel,
.mps-modal-submit {
    font-weight: 700;
    padding: 9px 16px;
    border-radius: 10px;
    cursor: pointer;
    border: 1px solid transparent;
}

.mps-modal-cancel {
    background: none;
    color: var(--palette-slate-600);
    border-color: var(--palette-slate-300);
}

.mps-modal-submit {
    background: var(--palette-green-600);
    color: #fff;
}

.mps-modal-submit:hover {
    background: var(--palette-green-700);
}

.mps-ing-hint {
    margin: 0 0 14px;
    font-size: 13px;
    color: var(--palette-slate-500);
}

.mps-ing-name {
    display: block;
    color: var(--palette-slate-900);
    text-transform: capitalize;
}

/* Prep ("diced") — a quiet cooking note under the name, separated from the WHAT you shop for. */
.mps-ing-prep {
    display: block;
    margin-top: 1px;
    font-size: 13px;
    color: var(--palette-slate-500);
}

/* Ingredient -> its Food Library page. Subtle (inherits colour) — a green underline signals
   it's clickable without turning the list into a wall of links. */
.mps-ing-link {
    color: inherit;
    text-decoration: underline;
    text-decoration-color: var(--palette-green-300);
    text-underline-offset: 2px;
}

.mps-ing-link:hover {
    text-decoration-color: var(--palette-green-600);
}

.mps-ing-buy {
    display: inline-block;
    margin-top: 6px;
    font-size: 12px;
    font-weight: 700;
    color: var(--palette-green-700);
    border: 1px solid var(--palette-green-300);
    border-radius: 8px;
    padding: 4px 10px;
    text-decoration: none;
}

.mps-ing-buy:hover {
    background: var(--palette-green-50);
}

/* --- instructions --- */
.mps-steps {
    margin: 0;
    padding-left: 22px;
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.mps-steps li {
    padding-left: 4px;
    color: var(--palette-slate-700);
    line-height: 1.55;
}

/* --- nutrition + meal-kit rail --- */
.mps-recipe-side {
    display: flex;
    flex-direction: column;
    gap: 16px;
}

/* 880px: MPS content-column breakpoint (intentional, shared across the recipe layout); not the app --bp-* seam. */
@media (min-width: 880px) {
    .mps-recipe-side {
        position: sticky;
        top: 88px;
        align-self: start;
    }
}

.mps-nutrition {
    padding: 18px;
    background: var(--palette-green-700);
    color: #fff;
    border-radius: 14px;
}

.mps-nutrition-h {
    margin: 0 0 12px;
    font-size: 14px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.mps-nutrition-h span {
    font-weight: 500;
    text-transform: none;
    letter-spacing: 0;
    opacity: 0.8;
}

.mps-nutrition-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
}

.mps-nut {
    display: flex;
    flex-direction: column;
}

.mps-nut-v {
    font-size: 26px;
    font-weight: 900;
    font-variant-numeric: tabular-nums;
}

.mps-nut-k {
    font-size: 12px;
    opacity: 0.85;
}

.mps-mealkit {
    padding: 18px;
    background: #fff;
    border: 1px solid var(--palette-slate-200);
    border-radius: 14px;
}

.mps-mealkit-lead {
    margin: 0 0 4px;
    font-weight: 800;
    color: var(--palette-slate-900);
}

.mps-mealkit-body {
    margin: 0 0 12px;
    font-size: 14px;
    line-height: 1.5;
    color: var(--palette-slate-600);
}

.mps-mealkit-cta {
    display: block;
    text-align: center;
    font-weight: 800;
    color: #fff;
    background: var(--palette-amber-500);
    padding: 11px;
    border-radius: 10px;
    text-decoration: none;
}

.mps-mealkit-cta:hover {
    background: var(--palette-amber-600);
}

.mps-mealkit-disc {
    margin: 8px 0 0;
    font-size: 11px;
    text-align: center;
    color: var(--palette-slate-400);
}

/* @toc Multiple choice */
/*
 * Multiple choice.
 *
 * FIVE TIERS, read top to bottom: the stem asks · the answer cards offer · the verdict answers · the
 * explanation teaches · the note qualifies. Each is a distinct object with its own weight and breathing room —
 * the hierarchy is the point, because the reader meets this at hour nine of a study day.
 *
 * Colour comes from the categorical status tokens (--success / --danger / --info with their AA-vetted -surface
 * tints), never a hand-authored value — so a right/wrong marking is compliant by construction and flips with
 * the theme.
 *
 * The reveal is CSS reacting to ONE attribute the JS sets (data-mc-state per option). Nothing here is a
 * JS-authored style, so the revealed state is reachable server-side by any caller that wants to render it
 * already open — which is also why the ✓/✕ marks are real heroicons in the markup rather than CSS glyphs.
 *
 * Correctness is never carried by colour ALONE (WCAG 1.4.1): the revealed card carries an ICON and the WORDS
 * "Correct answer" / "Your answer", and the rationale says why.
 *
 * BILINGUAL IS ONE RESPONSIVE DESIGN, not two. Both languages are always in the DOM; @container decides how
 * they sit. Narrow: English leads and Russian is on tap (the toggle). Wide: the pair stands as two co-equal
 * columns, because a translation read BESIDE its original is a different — better — study act than one read
 * underneath it. @container, not @media: the component reflows to its OWN box. It sits in a card, in a rail,
 * on a docs page, and the viewport tells it nothing useful about the width it actually has.
 */

.multiple-choice {
    display: flex;
    flex-direction: column;
    gap: var(--multiple-choice-gap, var(--space-4));

    /* The query container for every @container below — the component responds to its own box. */
    container-type: inline-size;
    container-name: mc;
}

/* ---- TIER 1 — the stem ----------------------------------------------------------------------------- */

.multiple-choice-stem {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.multiple-choice-question {
    margin: 0;
    color: var(--text);
    font-size: var(--text-lg);
    font-weight: 600;
    line-height: 1.5;
    text-wrap: pretty;
}

/* ---- the language control -------------------------------------------------------------------------- */

.multiple-choice-langs {
    display: inline-flex;
    align-self: start;
    gap: 2px;
    padding: 2px;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
}

.multiple-choice-lang {
    padding: 4px 10px;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-muted);
    font: inherit;
    font-size: var(--text-xs);
    font-weight: 600;
    cursor: pointer;
}

.multiple-choice-lang:hover {
    color: var(--text);
}

/* aria-pressed IS the state — styling from it means the pressed look and the announced state cannot drift. */
.multiple-choice-lang[aria-pressed="true"] {
    background: var(--surface-raised);
    color: var(--text);
}

.multiple-choice-lang:focus-visible {
    outline: 2px solid var(--link);
    outline-offset: 1px;
}

/* ---- TIER 2 — the answer cards --------------------------------------------------------------------- */

/* The fieldset is the radiogroup; it needs no chrome of its own here. */
.multiple-choice-options {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    border: 0;
    margin: 0;
    padding: 0;
}

/* A card, not text-with-a-dot: a bordered object you can aim at, scan, and tell apart at a glance. */
.multiple-choice-option {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: start;
    gap: var(--space-3);
    padding: var(--space-3);
    background: var(--surface-raised);
    border: 1px solid var(--line);
    border-radius: var(--multiple-choice-option-border-radius, 8px);
}

.multiple-choice-option:hover {
    border-color: var(--text-muted);
}

/* The card the reader is on. :has() reaches the card from the input's OWN state, so the platform stays the
   source of truth for selection — no JS class, nothing to keep in sync.

   :not([data-mc-state]) because this ring is a PRE-REVEAL affordance: it says "you are on this one", and once
   the verdict is in, the verdict owns the card. Without it the selection ring outlives its meaning and beats
   the reveal on specificity — `:has(input:checked)` scores (0,2,1) against `[data-mc-state="wrong"]`'s (0,2,0)
   — which paints the reader's wrong answer with a danger BACKGROUND inside a link-blue BORDER. Caught in the
   browser: it renders as a muddle that says two things at once. */
.multiple-choice-option:has(input:checked):not([data-mc-state]) {
    border-color: var(--link);
}

.multiple-choice-option:has(input:focus-visible) {
    outline: 2px solid var(--link);
    outline-offset: 2px;
}

/* The A–D medallion. */
.multiple-choice-letter {
    display: grid;
    place-items: center;
    inline-size: 28px;
    block-size: 28px;
    border-radius: 50%;
    background: var(--surface);
    border: 1px solid var(--line);
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-weight: 700;
    line-height: 1;
}

/* Same reasoning as the ring above: the medallion's "you are here" fill yields to the verdict's once it lands. */
.multiple-choice-option:has(input:checked):not([data-mc-state]) .multiple-choice-letter {
    background: var(--link);
    border-color: var(--link);
    color: var(--surface);
}

.multiple-choice-option-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    min-inline-size: 0;
}

/* ---- TIER 3 — the verdict -------------------------------------------------------------------------- */

.multiple-choice-result {
    margin: 0;
    font-size: var(--text-base);
    font-weight: 700;
}

.multiple-choice-result:empty {
    display: none;
}

.multiple-choice[data-mc-revealed] .multiple-choice-result {
    color: var(--text);
}

/* ---- TIER 4 — the explanation ---------------------------------------------------------------------- */

.multiple-choice-explanation {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding: var(--space-4);
    background: var(--surface-raised);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
}

.multiple-choice-explanation-text {
    margin: 0;
    color: var(--text);
    font-size: var(--text-sm);
    line-height: 1.7;
}

/* ---- TIER 5 — the jurisdiction note ---------------------------------------------------------------- */

/* x-callout carries the --info accent; this only seats it as the last tier inside the revealed block. */
.multiple-choice-note {
    margin: 0;
}

.multiple-choice-note-text {
    margin: 0;
    color: var(--text);
    font-size: var(--text-sm);
    line-height: 1.6;
}

/* ---- the reveal ------------------------------------------------------------------------------------ */

.multiple-choice-option[data-mc-state="correct"] {
    border-color: var(--success);
    background: var(--success-surface);
}

.multiple-choice-option[data-mc-state="correct"] .multiple-choice-letter {
    background: var(--success);
    border-color: var(--success);
    color: var(--success-surface);
}

.multiple-choice-option[data-mc-state="wrong"] {
    border-color: var(--danger);
    background: var(--danger-surface);
}

.multiple-choice-option[data-mc-state="wrong"] .multiple-choice-letter {
    background: var(--danger);
    border-color: var(--danger);
    color: var(--danger-surface);
}

/* The marks ship in the markup and are shown by state — never built by JS. An option the reader did not pick
   and that is not correct (state="other") shows neither: it teaches through its rationale, without carrying a
   verdict it has no business carrying. */
.multiple-choice-mark {
    display: none;
    align-items: center;
    gap: 6px;
    font-size: var(--text-xs);
    font-weight: 700;
    white-space: nowrap;
}

.multiple-choice-option[data-mc-state="correct"] .multiple-choice-mark-correct {
    display: inline-flex;
    color: var(--success);
}

.multiple-choice-option[data-mc-state="wrong"] .multiple-choice-mark-wrong {
    display: inline-flex;
    color: var(--danger);
}

.multiple-choice-mark-icon {
    display: inline-flex;
}

.multiple-choice-mark-icon svg {
    inline-size: 16px;
    block-size: 16px;
}

.multiple-choice-why {
    margin: 0;
    padding-block-start: var(--space-2);
    border-block-start: 1px dashed var(--line);
}

.multiple-choice-why-text {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--text-sm);
    line-height: 1.6;
}

.multiple-choice-check {
    align-self: start;
    margin-block-start: var(--space-2);
}

/* ---- bilingual: ONE responsive design -------------------------------------------------------------- */

/* The toggle hides text that is ALWAYS present — nothing is fetched, and no state is mirrored client-side. */
.multiple-choice[data-mc-lang="en"] [lang="ru"],
.multiple-choice[data-mc-lang="ru"] [lang="en"] {
    display: none;
}

/* NARROW (the base — mobile-first): English leads, Russian follows quieter. The pair reads as one thing,
   stacked, and the toggle is how Russian is summoned or dismissed. */
.multiple-choice-option-alt {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--text-sm);
    line-height: 1.5;
}

.multiple-choice-question[lang="ru"],
.multiple-choice-why-text[lang="ru"],
.multiple-choice-explanation-text[lang="ru"],
.multiple-choice-note-text[lang="ru"] {
    color: var(--text-muted);
}

.multiple-choice-langpair {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

/* WIDE — the pair stands as two co-equal columns. ONLY when both are showing: narrowed to one language, a
   two-column grid would leave a half-empty row, so `both` is the only value that opens the columns. */
@container mc (min-width: 640px) {
    .multiple-choice[data-mc-lang="both"] .multiple-choice-stem,
    .multiple-choice[data-mc-lang="both"] .multiple-choice-langpair,
    .multiple-choice[data-mc-lang="both"] .multiple-choice-why,
    .multiple-choice[data-mc-lang="both"] .multiple-choice-explanation {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: var(--space-4);
        align-items: start;
    }

    /* The note spans the explanation's columns: it qualifies BOTH languages, so it is not a third column. */
    .multiple-choice[data-mc-lang="both"] .multiple-choice-note {
        grid-column: 1 / -1;
    }

    /* Co-equal: read side by side, the translation is not the junior partner it is when stacked underneath. */
    .multiple-choice[data-mc-lang="both"] .multiple-choice-option-alt,
    .multiple-choice[data-mc-lang="both"] .multiple-choice-question[lang="ru"],
    .multiple-choice[data-mc-lang="both"] .multiple-choice-why-text[lang="ru"],
    .multiple-choice[data-mc-lang="both"] .multiple-choice-explanation-text[lang="ru"] {
        color: var(--text);
    }

    .multiple-choice[data-mc-lang="both"] .multiple-choice-option-alt {
        font-size: var(--text-base);
    }

    .multiple-choice[data-mc-lang="both"] .multiple-choice-question[lang="ru"] {
        font-weight: 600;
    }
}

/* @toc Page section */
/*
 * x-page-section — the shared section chassis (Section-Authoring Law: mobile-first + container-first).
 * .page-section is the full-bleed container-query CONTEXT; .page-section-inner is the centered, gutter-padded
 * content column, a flex column whose 32px rhythm spaces the header / items / body / footer blocks. The
 * @container (min-width) queries ENHANCE the vertical padding and the items grid on the section's OWN inline
 * size (never the viewport — no @media, no --bp-*). Type is fluid via clamp() + cqi. Every colour is a
 * semantic token so the section themes per tenant. --page-section-* vars come from the css facet.
 */

.page-section {
    container-type: inline-size;
}

.page-section-inner {
    display: flex;
    flex-direction: column;
    gap: var(--space-8);
    max-width: var(--page-section-max-width);
    margin-inline: auto;
    padding-block: var(--space-16);
    padding-inline: var(--space-6);
}

/* Desktop is a pure ENHANCEMENT — the section reflows on its own box width (min-width polarity, standardized).
   Vertical padding 64 (base) → 96 (sm ≥640) → 128 (lg ≥1024). */
@container (min-width: 640px) {
    .page-section-inner {
        padding-block: var(--space-24);
    }
}

@container (min-width: 1024px) {
    .page-section-inner {
        padding-block: var(--space-32);
    }
}

/* The header — one grammar for every section: eyebrow · title · description · actions. Left-aligned (start)
   by default; a section that centres (e.g. a CTA band) overrides text-align + align-items in its own CSS. */
.page-section-head {
    display: flex;
    flex-direction: column;
    align-items: start;
    text-align: start;
}

.page-section-eyebrow {
    margin: 0;
    font-size: var(--text-sm);
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.page-section-eyebrow + .page-section-title {
    margin-block-start: var(--space-3);
}

.page-section-title {
    margin: 0;
    font-size: clamp(1.875rem, 1.5rem + 2cqi, 3rem);
    font-weight: 700;
    letter-spacing: -0.025em;
    line-height: 1.1;
    text-wrap: balance;
    color: var(--text);
}

.page-section-description {
    max-width: var(--max-w-2xl);
    margin-block: var(--space-4) 0;
    font-size: clamp(1rem, 0.95rem + 0.4cqi, 1.125rem);
    line-height: 1.6;
    color: var(--text-muted);
}

.page-section-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3) var(--space-6);
    margin-block-start: var(--space-6);
}

/* The items grid — the standard repeating-content region (features, stats, pricing all compose it): 1 → 2 → 3
   columns on the section's own width. A section overrides the desktop column count via --page-section-items-cols
   (the author-facing knob), so it composes the chassis with zero chassis edits. */
.page-section-items {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-8);
}

@container (min-width: 640px) {
    .page-section-items {
        grid-template-columns: repeat(2, 1fr);
    }
}

@container (min-width: 1024px) {
    .page-section-items {
        grid-template-columns: repeat(var(--page-section-items-cols, 3), 1fr);
    }
}

.page-section-footer {
    color: var(--text-muted);
}

/* @toc Pagination */
/* Pagination — count line + prev/page/next, laid out as a row. Reuses .button
 * (button-ghost + is-disabled) for the controls; only layout + muted text here. */

.pagination {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
    margin-top: 16px;
}

.pagination-info {
    color: var(--text-muted);
    font-size: var(--text-sm);
}

.pagination-controls {
    display: flex;
    align-items: center;
    gap: 10px;
}

.pagination-page {
    color: var(--text-muted);
    font-size: var(--text-sm);
}

/* @toc Panel */
/* x-panel (1211) — the house panel grammar. A FLAT, hairline-ruled headed region: no border, radius, shadow, or
   background of its own (it composes into workspace columns/rails + reader/planner layouts that own the outer
   chrome). The HEADER BAR is a flex row [optional collapse toggle · the .ui-label title · an optional right-aligned
   actions slot] over a thin --line rule; the body flows beneath and — when collapsible — folds behind the toggle
   (the grid-rows disclosure technique, mirroring x-accordion). Structural only: colour is semantic tokens, never a
   per-panel knob (a layout container has no skin to theme). The one motion knob (--panel-duration) is set by the
   island (panel.js) from window.PANEL_CONFIG, with a 200ms fallback here. */
.panel {
    display: flex;
    flex-direction: column;
    min-height: 0;
}

/* The header bar: a single flex row. The title grows so an actions slot is pushed to the end; the toggle and the
   actions keep their natural size. Rendered whenever there is a title, actions, or a collapse toggle. */
.panel-head {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-block-end: var(--space-2);
    padding-block-end: var(--space-2);
    border-block-end: 1px solid var(--line);
}

/* The title rides the shared .ui-label recipe (a word); this resets the heading margin and lets it grow so an
   actions slot right-aligns. */
.panel-title {
    flex: 1;
    min-width: 0;
    margin: 0;
}

/* The collapse toggle: a small square button holding a decorative chevron. A native <button>, so Enter/Space
   activation comes free; open state lives on aria-expanded (the chevron only mirrors it). 28px clears the WCAG
   2.5.8 target-size minimum. */
.panel-toggle {
    display: inline-flex;
    flex: none;
    align-items: center;
    justify-content: center;
    width: var(--space-7);
    height: var(--space-7);
    padding: 0;
    border: none;
    border-radius: var(--radius-control);
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
}

.panel-toggle:hover {
    /* A whisper-subtle neutral tint — color-mix on a token, so it stays theme-safe (no literal colour). */
    background: color-mix(in srgb, var(--text) 4%, transparent);
    color: var(--text);
}

.panel-toggle:focus-visible {
    outline: 2px solid transparent;
    outline-offset: -2px;
    box-shadow: inset 0 0 0 2px var(--primary);
}

.panel-marker {
    display: inline-flex;
    transition: transform var(--panel-duration, 200ms) ease;
}

.panel-marker svg {
    width: 1.25rem;
    height: 1.25rem;
}

/* Collapsed → the chevron points right (a disclosure triangle); expanded → it points down. */
.panel-toggle[aria-expanded="false"] .panel-marker {
    transform: rotate(-90deg);
}

/* The actions slot: a right-aligned row of surface-owned controls (ghost icon buttons, a count badge). x-panel
   provides the row + spacing only; each control carries its own label + behaviour (C9 — injected, never owned).
   `margin-inline-start: auto` right-aligns the slot UNCONDITIONALLY — a no-op when the flex:1 title is present, but
   it holds the contract for a titleless header (actions-only, or toggle + actions) where nothing else grows. */
.panel-actions {
    display: flex;
    flex: none;
    align-items: center;
    gap: var(--space-2);
    margin-inline-start: auto;
}

.panel-body {
    min-height: 0;
}

/* Collapsible fold: the body animates open/close via a grid-template-rows 0fr → 1fr tween (mirroring x-accordion);
   panel.js adds/removes .is-open and toggles the `hidden` attribute at the animation boundary (a collapsed body is
   `hidden` = out of layout AND the AT tree). Only a collapsible panel gets the grid — a plain panel's body stays an
   ordinary block. */
.panel[data-panel-collapsible] .panel-body {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows var(--panel-duration, 200ms) ease;
}

.panel[data-panel-collapsible] .panel-body.is-open {
    grid-template-rows: 1fr;
}

/* Required: the author display:grid above otherwise overrides the UA [hidden]{display:none}, leaving a clipped
   sliver visible. panel.js un-hides BEFORE animating open and re-hides AFTER the collapse tween, so the body is
   never `hidden` mid-animation. */
.panel[data-panel-collapsible] .panel-body[hidden] {
    display: none;
}

/* The inner wrapper clips content during the 0fr phase (the grid-rows technique needs an overflow:hidden child). */
.panel[data-panel-collapsible] .panel-body-inner {
    overflow: hidden;
}

/* Reduced motion: instant fold and no chevron spin (the global animations.css blanket already zeroes transitions;
   this local block states the intent for honesty, per the accordion precedent). */
@media (prefers-reduced-motion: reduce) {
    .panel[data-panel-collapsible] .panel-body,
    .panel-marker {
        transition: none;
    }
}

/* @toc Planner board */
/* Planner board — a Trello-style kanban. Lists scroll horizontally (scroll-snap one
 * per screen on mobile); cards are dragged with a single unified pointer drag (mouse +
 * touch) that leaves a placeholder gap; the card back is a full detail dialog. Colours
 * ride the semantic tokens; layout knobs come through --planner-board-* with fallbacks.
 * View: planner-board.blade.php · JS: planner-board.js */

.planner-board {
    --planner-lane-min: 16rem;
    --planner-lane-max: 18rem;
    display: flex;
    container: planner-board / inline-size;
    gap: var(--space-4);
    align-items: flex-start;
    overflow-x: auto;
    padding-block-end: var(--space-3);
    scroll-padding-inline: var(--space-4);
    /* The board scrolls horizontally, lists scroll vertically. A drag captures its
     * pointer (setPointerCapture in the JS) the moment it starts, so neither scroller
     * can steal the gesture mid-drag. The scrollbar itself is the global themed one (base.css). */
}

/* While a drag is live, kill text selection + native scroll gestures board-wide (the
 * descendants too, so a touch can't start a scroll mid-drag). */
.planner-board.is-dragging,
.planner-board.is-dragging * {
    touch-action: none;
    user-select: none;
}

.planner-board.is-dragging {
    cursor: grabbing;
    /* Let the edge auto-scroll glide across lists mid-drag — scroll-snap would fight it. */
    scroll-snap-type: none;
}

/* --- a list (lane) — an elevated panel that sits above the page --- */
.planner-list {
    flex: 0 0 clamp(var(--planner-lane-min), 24cqi, var(--planner-lane-max));
    display: flex;
    flex-direction: column;
    min-width: 0;
    max-height: calc(100dvh - 184px);
}

.planner-list > .planner-list-panel {
    max-height: inherit;
}

.planner-list > .planner-list-panel > .panel-head {
    margin: 0;
    padding: var(--space-2) var(--space-3);
}

.planner-list > .planner-list-panel > .panel-body {
    display: flex;
    flex: 1;
    min-height: 0;
}

.planner-list > .planner-list-panel > .panel-body > .panel-body-inner {
    display: flex;
    flex: 1;
    flex-direction: column;
    min-width: 0;
    min-height: 0;
}

.planner-list-count {
    font-size: var(--text-xs);
    font-weight: 600;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
    min-width: 1.5rem;
    text-align: center;
}

.planner-list-cards {
    flex: 1 1 auto;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 0;
    padding: 0;
    min-height: var(--space-3);
}

/* --- a card — elevated above the list, with a tactile hover lift + press --- */
.planner-card {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    width: 100%;
    min-width: 0;
    padding: var(--space-2) var(--space-3);
    background: transparent;
    border: 0;
    border-block-end: 1px solid var(--line);
    border-radius: 0;
    cursor: grab;
    /* iOS: the long-press that starts a drag must NOT trigger text selection or the
     * copy/define callout magnifier; also kill the grey tap-flash. */
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
    -webkit-tap-highlight-color: transparent;
    /* Two stacked shadows (contact + ambient) + a top highlight so it reads as a real
     * raised card, not a flat tile. */
    box-shadow: none;
    transition: background-color 130ms ease, border-color 130ms ease;
}

.planner-card:hover {
    background: color-mix(in srgb, var(--primary) 5%, transparent);
    border-block-end-color: color-mix(in srgb, var(--primary) 42%, var(--line));
}

/* Press it and it pushes in — comes after :hover so the push wins while held. */
.planner-card:active {
    background: color-mix(in srgb, var(--primary) 8%, transparent);
}

.planner-card:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: -2px;
}

/* Settle animation when a card lands from a drop. */
.planner-card.just-dropped {
    background: color-mix(in srgb, var(--primary) 7%, transparent);
}

/* Live board: a card whose lane just changed (or that just appeared) gets a fading primary ring,
   so a move the poller swapped in is visible. Decoration only (ramp rung + alpha via color-mix) —
   the primary ramp flips per tenant, so this themes for free. */
@keyframes planner-card-live-move {
    0% {
        box-shadow: 0 0 0 2px var(--primary-500), 0 0 14px 2px color-mix(in srgb, var(--primary-500) 45%, transparent);
    }
    100% {
        box-shadow: 0 0 0 2px transparent, 0 0 0 0 transparent;
    }
}

.planner-card.is-live-moved {
    animation: planner-card-live-move 1.6s ease-out;
}

@media (prefers-reduced-motion: reduce) {
    .planner-card.is-live-moved {
        animation: none;
        box-shadow: 0 0 0 2px var(--primary-500);
    }
}

/* (The old Trello colour-bar "label dot" retired with Tag model v2 — the project is now a full chip.) */

.planner-card-title {
    margin: 0;
    font-size: var(--text-sm);
    font-weight: 500;
    line-height: 1.35;
    color: var(--text);
    word-break: break-word;
    display: -webkit-box;
    overflow: hidden;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
}

/* Card foot — the bottom meta row: checklist progress + the "has a description" glyph. */
.planner-card-identity {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
}

.planner-card-number {
    flex: none;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--text-muted);
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: 12px;
    font-variant-numeric: tabular-nums;
    line-height: 1.4;
}

.planner-card-footer {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-1) var(--space-2);
}

.planner-card-footer-start {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
}

.planner-card-footer-end {
    display: inline-flex;
    min-width: 0;
    margin-inline-start: auto;
}

.planner-card-foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin-block-start: var(--space-1);
}

/* Checklist progress (Card anatomy v2) — a scannable done/total badge on the card face. */
.planner-card-progress {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    font-size: 11px;
    font-weight: 600;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

.planner-card-progress svg {
    width: 13px;
    height: 13px;
}

/* Checklist badge — three honest states by colour (Card redesign, item 3): complete = green, partial =
   orange, none-done = red. Purely the checklist's own state (independent of lane). Each pairs its status
   token with its AA-vetted -surface tint (the categorical status set — compliant by construction, NO new
   colours); the done/total number carries the state too, so the tint reinforces rather than being the sole
   signal. */
.planner-card-progress.is-complete {
    color: var(--success);
}

.planner-card-progress.is-partial {
    color: var(--warning);
}

.planner-card-progress.is-none {
    color: var(--danger);
}

/* --- Card redesign: four-corner metadata (reporter · assignee top; checklist · commit bottom). Each row
   is a 2-slot grid so the right corner stays pinned right even when the left is empty (an empty corner is
   the honest "not yet" signal). --- */
.planner-card-corners {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: 8px;
}

/* The bottom row carries THREE slots — checklist (left) · file-count (middle) · commit (right, Done only) —
   so it's a 3-column grid; the top row stays the 2-column reporter/assignee split. */
.planner-card-corners-foot {
    grid-template-columns: 1fr auto 1fr;
    margin-top: 2px;
}

.planner-corner-start {
    justify-self: start;
    min-width: 0;
}

.planner-corner-mid {
    justify-self: center;
}

.planner-corner-end {
    justify-self: end;
    text-align: end;
}

/* File-count badge (bottom-middle) — a file icon + the ticket's files[] count; a glance-able size signal. */
.planner-card-filecount {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    font-size: 11px;
    font-weight: 600;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

.planner-card-filecount svg {
    width: 13px;
    height: 13px;
    flex-shrink: 0;
}

/* Shared actor badge (reporter + assignee) — a SECONDARY pill: a quiet neutral-tinted chip with the actor
   icon + name (planner._actor-badge). "De-throned" means secondary, NOT stripped to plain text — it keeps the
   badge affordance but drops the accent-hero treatment the old holder badge had. POSITION (top-left vs
   top-right) carries the role, so no overline label. Names truncate. Mirrors the .planner-chip tint (AA-safe). */
.planner-actor {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    max-width: 100%;
    padding: 0;
    border: 0;
    border-radius: 0;
    font-size: 11px;
    font-weight: 600;
    line-height: 1.5;
    color: var(--text-muted);
    background: transparent;
}

.planner-actor > span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* One consistent icon size across every actor icon (person / sparkle / lock) — fixes the oversized lock. */
.planner-actor svg {
    width: 13px;
    height: 13px;
    flex-shrink: 0;
}

/* The assignee POPS in the tenant PRIMARY (James's ruling, ticket 1046) — the primary of the two people,
   and primary (not a status hue) themes per tenant + avoids status-semantics collision. Mirrors the vetted
   .planner-chip mix, primary-substituted: AA text via the blend toward --text, a light primary surface tint,
   a primary border. The live-checkout LOCK icon inherits this colour (currentColor). Reporter stays quiet. */
.planner-actor-assignee {
    color: color-mix(in srgb, var(--primary) 46%, var(--text));
    background: transparent;
    border-color: transparent;
}

/* Commit corner (Done only) — the short hash over its git date, stacked and right-aligned. */
.planner-card-commit {
    display: inline-flex;
    flex-direction: column;
    align-items: flex-start;
    line-height: 1.25;
}

.planner-card-commit code {
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: 11px;
    color: var(--success);
}

.planner-card-commit-date {
    font-size: 10px;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/* Bottom block (Done) — one coherent row: checklist · commit message · commit id. Non-Done cards show the
   lone progress badge; both get a little top breathing room from the chips. */
.planner-card > .planner-card-progress,
.planner-card-done {
    margin-top: 7px;
}

.planner-card-done {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
}

.planner-card-commit-msg {
    flex: 1 1 100%;
    min-width: 0;
    overflow: hidden;
    font-size: 11px;
    line-height: 1.4;
    color: var(--text-muted);
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* "Has a description" hint glyph. */
.planner-card-desc-flag {
    display: inline-flex;
    color: var(--text-muted);
}

.planner-card-desc-flag svg {
    width: 14px;
    height: 14px;
}

/* The card being dragged: hidden in place, a clone (ghost) follows the pointer. */
.planner-card.is-source {
    display: none;
}

/* The gap the ghost will drop into — a recessed slot, not a dashed outline. */
.planner-card-placeholder {
    border-radius: var(--radius-control);
    background: color-mix(in srgb, var(--primary) 9%, var(--surface-muted));
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--primary) 28%, var(--line));
    flex: 0 0 auto;
}

/* The element that follows the pointer. Its transform (position + velocity tilt + lift
 * scale) is driven per-frame by the JS — don't set transform here. */
.planner-card-ghost {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 1000;
    width: var(--ghost-w, 260px);
    margin: 0;
    pointer-events: none;
    background: var(--surface-raised);
    border: 1px solid color-mix(in srgb, var(--primary) 45%, var(--line));
    border-radius: var(--radius-control);
    box-shadow: var(--shadow-lg);
}

/* --- composer (add a card) --- */
.planner-list-composer {
    padding: var(--space-2) var(--space-3) var(--space-3);
}

.planner-list-add {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--space-7);
    height: var(--space-7);
    padding: 0;
    font-size: 1.125rem;
    line-height: 1;
}

.planner-composer {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.planner-composer[hidden] {
    display: none;
}

.planner-composer-input {
    width: 100%;
    resize: none;
    font: inherit;
    font-size: var(--text-sm);
    line-height: 1.35;
    color: var(--text);
    background: var(--surface-raised);
    border: 1px solid var(--primary-500);
    border-radius: 10px;
    padding: 9px 11px;
    box-shadow: var(--shadow-sm);
}

.planner-composer-input::placeholder {
    color: var(--text-muted);
}

.planner-composer-input:focus-visible {
    outline: none;
}

.planner-composer-actions {
    display: flex;
    align-items: center;
    gap: 8px;
}

.planner-composer-add {
    font: inherit;
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--on-primary);
    background: var(--primary-600);
    border: none;
    border-radius: 8px;
    padding: 7px 14px;
    cursor: pointer;
}

.planner-composer-add:hover {
    background: var(--primary-700);
}

.planner-composer-cancel {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    color: var(--text-muted);
    background: transparent;
    border: none;
    border-radius: 8px;
    cursor: pointer;
}

.planner-composer-cancel:hover {
    color: var(--text);
    background: color-mix(in srgb, var(--text) 8%, transparent);
}

.planner-composer-cancel svg {
    width: 20px;
    height: 20px;
}

/* --- card back (the detail dialog) --- */
/* Card-open v2 — the card back widens on desktop to hold two columns. It's a composed x-dialog, so we
 * set --dialog-width and let `.dialog { width: min(var(--dialog-width), calc(100vw - 32px)) }` read it.
 * NO #id `width:` override here — id specificity would beat the var() and pin the old single-column
 * width (the bug an earlier pass shipped). Mobile full-page sizing is set in the max-width:768px block
 * below. (The card-detail MODAL sizes itself by #planner-card-modal in the modal block near the foot.) */
#planner-card-back {
    --dialog-width: 840px;
}

/* Two views — the content (title + description + checklist) and the structural rail (tier · project ·
   move · actions). On desktop they sit side-by-side; on mobile they are <x-tabs> panels (Details |
   Info & actions), so a tall card back is two short tabs, not one long scroll. Tabs are the default
   (mobile) presentation; the min-width rule below UNWRAPS them into the two-column grid — hides the
   tablist, lays both panels out side-by-side. */
.planner-back {
    min-width: 0;
}

.planner-back-content,
.planner-back-rail {
    display: flex;
    flex-direction: column;
    gap: 12px;
    min-width: 0;
}

/* Desktop unwrap. The seam is 768/769 — the app's ONE breakpoint (sidebar-nav.js's drawer, the board's
   mobile rules, all use 768). Above it the card back is two columns (tablist hidden, panels gridded
   side-by-side); at/below it the panels are real <x-tabs> (the max-width:768px block below). The same
   769/768 pair governs the card-back editor's full-page takeover. */
@media (min-width: 769px) {
    .planner-back .tabs-nav {
        display: none;
    }

    .planner-back .tabs-body {
        display: grid;
        grid-template-columns: minmax(0, 1.85fr) minmax(0, 1fr);
        gap: 22px;
        align-items: start;
        padding-top: 0;
    }

    .planner-back .tabs-panel {
        display: block;
    }

    .planner-back-rail {
        padding-left: 20px;
        border-left: 1px solid var(--line);
    }
}

.planner-back-title {
    width: 100%;
    resize: none;
    overflow: hidden;
    font: inherit;
    font-size: var(--text-lg);
    font-weight: 700;
    line-height: 1.3;
    color: var(--text);
    background: transparent;
    border: 1px solid transparent;
    border-radius: 8px;
    padding: 6px 8px;
    margin: 0;
}

.planner-back-title:hover {
    background: color-mix(in srgb, var(--text) 5%, transparent);
}

.planner-back-title:focus-visible {
    outline: none;
    border-color: var(--primary-500);
    background: var(--surface-raised);
}

.planner-back-section-label {
    margin: 0;
    font-size: var(--text-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: var(--text-muted);
}

.planner-back-labels,
.planner-back-tier {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: -6px;
}

/* Card-back picker buttons (Tier + Project) — the PILL pattern: at rest, adaptive category-tinted text on
   a faint category bg (color-mix toward --text, so it self-flips and clears AA in BOTH themes); SELECTED
   (.is-active) fills solid with the family's --planner-active-fill + white text + a white ring. The old
   white-text-at-opacity-0.72 shape failed AA — dimmed white floors at ~2.5:1 over a light surface — so
   adaptive text is the color-law-correct fix, matching .planner-pill (which already passes). */
.planner-back-chip {
    font: inherit;
    font-size: var(--text-xs);
    font-weight: 600;
    color: color-mix(in srgb, var(--planner-cat, var(--text-muted)) 45%, var(--text));
    background: color-mix(in srgb, var(--planner-cat, var(--text-muted)) 12%, var(--surface-raised));
    border: 2px solid color-mix(in srgb, var(--planner-cat, var(--text-muted)) 30%, var(--line));
    border-radius: 8px;
    padding: 7px 14px;
    cursor: pointer;
    transition: color 100ms ease, background 100ms ease, border-color 100ms ease;
}

.planner-back-chip:hover {
    color: color-mix(in srgb, var(--planner-cat, var(--text-muted)) 62%, var(--text));
    border-color: color-mix(in srgb, var(--planner-cat, var(--text-muted)) 52%, var(--line));
}

.planner-back-chip.is-active {
    color: var(--on-primary);
    background: var(--planner-active-fill, var(--text-muted));
    border-color: color-mix(in srgb, var(--on-primary) 65%, transparent);
}

.planner-back-chip-none {
    color: var(--text);
    background: color-mix(in srgb, var(--text-muted) 30%, transparent);
}

.planner-back-desc {
    width: 100%;
    resize: vertical;
    min-height: 96px;
    font: inherit;
    font-size: var(--text-sm);
    line-height: 1.45;
    color: var(--text);
    background: var(--surface-raised);
    border: 1px solid var(--line);
    border-radius: 10px;
    padding: 10px 12px;
    margin-top: -6px;
}

.planner-back-desc:focus-visible {
    outline: none;
    border-color: var(--primary-500);
}

/* Card-back checklist (Card anatomy v2) — clickable rows derived from the `- [ ]` lines above. */
.planner-back-checklist {
    display: flex;
    flex-direction: column;
    gap: 1px;
    margin-top: -6px;
}

.planner-back-check {
    display: flex;
    align-items: flex-start;
    gap: 9px;
    padding: 5px 7px;
    border-radius: 7px;
    font-size: var(--text-sm);
    line-height: 1.4;
    color: var(--text);
    cursor: pointer;
}

.planner-back-check:hover {
    background: color-mix(in srgb, var(--text) 5%, transparent);
}

.planner-back-check input {
    margin: 2px 0 0;
    accent-color: var(--primary-500);
    cursor: pointer;
    flex-shrink: 0;
}

.planner-back-check.is-done span {
    color: var(--text-muted);
    text-decoration: line-through;
}

.planner-back-move {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: -6px;
}

.planner-back-move-btn {
    font: inherit;
    font-size: var(--text-sm);
    color: var(--text);
    background: color-mix(in srgb, var(--text) 7%, transparent);
    border: 1px solid var(--line);
    border-radius: 8px;
    padding: 6px 14px;
    cursor: pointer;
}

.planner-back-move-btn:hover {
    background: color-mix(in srgb, var(--text) 12%, transparent);
}

/* The "you are here" lane button. Its primary-tinted text must clear 4.5:1 on the 12%-primary wash over
   BOTH surfaces, so the rung owns the light/dark flip (color-law rule 2): primary-700 on the light wash,
   primary-400 on the dark one — raw primary-500 sat at ~3.2 (light) / ~4.2 (dark). */
.planner-back-move-btn.is-current {
    color: var(--primary-700);
    border-color: var(--primary-500);
    background: color-mix(in srgb, var(--primary-500) 12%, transparent);
    cursor: default;
}

:root[data-theme="dark"] .planner-back-move-btn.is-current {
    color: var(--primary-400);
}

.planner-back-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-top: 4px;
    padding-top: 14px;
    border-top: 1px solid var(--line);
}

.planner-back-delete {
    font: inherit;
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--danger);
    background: transparent;
    border: 1px solid color-mix(in srgb, var(--danger) 45%, transparent);
    border-radius: 8px;
    padding: 7px 14px;
    cursor: pointer;
}

.planner-back-delete:hover {
    background: color-mix(in srgb, var(--danger) 12%, transparent);
}

.planner-back-save {
    font: inherit;
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--on-primary);
    /* The on-primary token over primary-600 clears 4.5:1 and flips per tenant. */
    background: var(--primary-600);
    border: none;
    border-radius: 8px;
    padding: 8px 20px;
    cursor: pointer;
}

.planner-back-save:hover {
    background: var(--primary-700);
}

/* --- mobile: one list per screen, swipe between; full-screen card back --- */
@media (max-width: 768px) {
    .planner-board {
        scroll-snap-type: x mandatory;
        -webkit-overflow-scrolling: touch;
        margin-inline: calc(var(--space-4) * -1);
        padding-block-end: var(--space-3);
    }

    .planner-list {
        scroll-snap-align: start;
        max-height: calc(100dvh - 168px);
        /* ONE LANE PER SCREEN (#1351). The snap above was only ever half the mechanism: it lands you ON a
           lane, but the lane kept its desktop basis (clamp(16rem, 24cqi, 18rem) ≈ 256px), so a 375px phone
           showed one lane plus most of the next and still read as a two-axis scroll. The heading above this
           block has claimed "one list per screen" since it was written; this is the line that makes it true.

           86%, not 100%, ON PURPOSE — the sliver of the next lane is the same honesty rule the filter rail
           follows: you can SEE there is another lane, rather than discovering it by accident. A lane that
           fills the screen edge-to-edge looks like the only lane. */
        flex: 0 0 86%;
    }

    .planner-list:first-child {
        margin-inline-start: var(--space-4);
    }

    .planner-list:last-child {
        margin-inline-end: var(--space-4);
    }
    /* The card-back editor's mobile full-page takeover lives in the unified max-width:768px block below
       (#planner-card-back) — one seam, no 621–768 hybrid. */
}

/* --- read-only repo board (the "Eightythree Planner" tab — PLANNER.md via PlanBoard) --- */

/* Board tabs — a quiet segmented control above the board (personal vs the repo board). */
.planner-tabs {
    display: inline-flex;
    gap: 2px;
    margin-bottom: var(--space-4, 16px);
    padding: 3px;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius-full, 999px);
}

.planner-tabs a {
    padding: 5px 14px;
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--text-muted);
    text-decoration: none;
    border-radius: var(--radius-full, 999px);
}

.planner-tabs a.is-active {
    background: var(--surface-raised);
    color: var(--text);
    box-shadow: var(--shadow-sm);
}

/* A read-only (source) card: a link that JS upgrades into the five-pane MODAL — so it lays its face out
   as a stack and sheds the raw <a> underline/colour. The base .planner-card gives the surface + hover. */
.planner-card-readonly {
    display: flex;
    flex-direction: column;
    gap: 6px;
    text-decoration: none;
    color: inherit;
    cursor: pointer;
}

/* A parse-fallback card (planner.enabled off) has no five-pane — a static, non-interactive tile: no
   pointer, no hover-lift (the lift would falsely imply it opens something). */
.planner-card-static,
.planner-card-static:hover {
    cursor: default;
    transform: none;
    background: transparent;
    border-block-end-color: var(--line);
}

@media (pointer: coarse) {
    .planner-list-add {
        width: 2.75rem;
        height: 2.75rem;
    }
}

.planner-card-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
}

.planner-chip {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    font-size: 11px;
    font-weight: 600;
    line-height: 1.5;
    padding: 0 7px 0 6px;
    border-radius: var(--radius-full, 999px);
    color: var(--text-muted);
    background: color-mix(in srgb, var(--text-muted) 15%, transparent);
}

/* Tag scent icon (Card anatomy v2) — inherits the chip's themed text colour via currentColor. */
.planner-chip svg {
    width: 12px;
    height: 12px;
    flex-shrink: 0;
}

.planner-chip-decision {
    color: var(--primary-600, var(--primary-500));
    background: color-mix(in srgb, var(--primary-500) 16%, transparent);
}

.planner-chip-committed {
    font-family: var(--font-mono, monospace);
    color: var(--success);
    background: var(--success-surface);
}

/* Builder BADGE (Card anatomy v3) — top-left, ABOVE the title: an accent-tinted pill with a Heroicon lock
   (the HOLDER, in-progress/review) or user (BUILT-BY, Done) + the builder's name. A block-level flex on its
   own line (width: fit-content) so it sits above the title; the card-open banner (.cardo-holder) mirrors it. */
.planner-card-badge {
    display: flex;
    align-items: center;
    gap: 4px;
    width: fit-content;
    max-width: 100%;
    margin-bottom: 5px;
    padding: 1px 8px 1px 6px;
    border-radius: var(--radius-full, 999px);
    font-size: 11px;
    font-weight: 600;
    color: color-mix(in srgb, var(--primary-500) 52%, var(--text));
    background: color-mix(in srgb, var(--primary-500) 15%, transparent);
    border: 1px solid color-mix(in srgb, var(--primary-500) 28%, transparent);
}

.planner-card-badge svg {
    width: 12px;
    height: 12px;
    flex-shrink: 0;
}

.planner-card-badge > span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* CATEGORICAL colours (Tag model v2). Every card's two chips — the master TIER and the PROJECT — plus
   the filter pills colour-code from the VETTED --planner-fam-* categorical tokens (R13; emitted by
   PaletteService::plannerFamVars, engine-contrast-vetted), deliberately NOT the tenant ramp: these are
   identities that must stay stable across tenants. A .planner-fam-<family> class sets --planner-cat; the
   chip/pill rules color-mix it into a tinted background + border with contrast-safe text that flips per
   theme by blending toward --text. No two chips/pills in one render share a family (PlanBoard::familyMap). */
.planner-chip-tier,
.planner-chip-project {
    color: color-mix(in srgb, var(--planner-cat, var(--text-muted)) 46%, var(--text));
    background: color-mix(in srgb, var(--planner-cat, var(--text-muted)) 15%, transparent);
    border: 1px solid color-mix(in srgb, var(--planner-cat, var(--text-muted)) 28%, transparent);
}

/* Family → the vetted --planner-fam-* tokens (R13). Each family sets --planner-cat (the identity tint
   base for inactive pills/chips = --planner-fam-<fam>) AND --planner-active-fill (the SOLID rung a pill
   fills with when SELECTED under white text = --planner-fam-<fam>-active, engine-picked ≥4.5:1 on white).
   PaletteService::plannerFamVars emits both per family, so any family config/planner.php picks resolves —
   no raw --palette-* in this file anymore (R13's whole point: one vetted, governed home). */
.planner-fam-slate   { --planner-cat: var(--planner-fam-slate);   --planner-active-fill: var(--planner-fam-slate-active); }
.planner-fam-gray    { --planner-cat: var(--planner-fam-gray);    --planner-active-fill: var(--planner-fam-gray-active); }
.planner-fam-zinc    { --planner-cat: var(--planner-fam-zinc);    --planner-active-fill: var(--planner-fam-zinc-active); }
.planner-fam-neutral { --planner-cat: var(--planner-fam-neutral); --planner-active-fill: var(--planner-fam-neutral-active); }
.planner-fam-stone   { --planner-cat: var(--planner-fam-stone);   --planner-active-fill: var(--planner-fam-stone-active); }
.planner-fam-red     { --planner-cat: var(--planner-fam-red);     --planner-active-fill: var(--planner-fam-red-active); }
.planner-fam-orange  { --planner-cat: var(--planner-fam-orange);  --planner-active-fill: var(--planner-fam-orange-active); }
.planner-fam-amber   { --planner-cat: var(--planner-fam-amber);   --planner-active-fill: var(--planner-fam-amber-active); }
.planner-fam-yellow  { --planner-cat: var(--planner-fam-yellow);  --planner-active-fill: var(--planner-fam-yellow-active); }
.planner-fam-lime    { --planner-cat: var(--planner-fam-lime);    --planner-active-fill: var(--planner-fam-lime-active); }
.planner-fam-green   { --planner-cat: var(--planner-fam-green);   --planner-active-fill: var(--planner-fam-green-active); }
.planner-fam-emerald { --planner-cat: var(--planner-fam-emerald); --planner-active-fill: var(--planner-fam-emerald-active); }
.planner-fam-teal    { --planner-cat: var(--planner-fam-teal);    --planner-active-fill: var(--planner-fam-teal-active); }
.planner-fam-cyan    { --planner-cat: var(--planner-fam-cyan);    --planner-active-fill: var(--planner-fam-cyan-active); }
.planner-fam-sky     { --planner-cat: var(--planner-fam-sky);     --planner-active-fill: var(--planner-fam-sky-active); }
.planner-fam-blue    { --planner-cat: var(--planner-fam-blue);    --planner-active-fill: var(--planner-fam-blue-active); }
.planner-fam-indigo  { --planner-cat: var(--planner-fam-indigo);  --planner-active-fill: var(--planner-fam-indigo-active); }
.planner-fam-violet  { --planner-cat: var(--planner-fam-violet);  --planner-active-fill: var(--planner-fam-violet-active); }
.planner-fam-purple  { --planner-cat: var(--planner-fam-purple);  --planner-active-fill: var(--planner-fam-purple-active); }
.planner-fam-fuchsia { --planner-cat: var(--planner-fam-fuchsia); --planner-active-fill: var(--planner-fam-fuchsia-active); }
.planner-fam-pink    { --planner-cat: var(--planner-fam-pink);    --planner-active-fill: var(--planner-fam-pink-active); }
.planner-fam-rose    { --planner-cat: var(--planner-fam-rose);    --planner-active-fill: var(--planner-fam-rose-active); }

/* --- filter pills — ONE wrapping row (ticket 1217, supersedes the R12 two-segment layout of 1066): the
   three majors (All · Personal · Business), a 1px --line vertical divider, then every project label. The
   label pills flex-WRAP to the next line as the bar narrows (no horizontal scroll) — reclaiming the full
   second row of height the old layout spent, pulling the board up. Container-first: flex-wrap sizes to the
   bar's own width, so no viewport media query. --- */
/* The filter rail (#1351) — positioning context for the overflow fade, and the owner of the bar's outer
   margin. A small negative top trims the shell main's top padding so the bar (and the board under it) sits up.

   THE BOTTOM HAIRLINE (#1357) gives the row an edge like the rest of the board, and gives the horizontal
   scrollbar something to sit against instead of floating in space.

   #1357 shipped this as two rules holding RAW rungs (neutral-300 / neutral-700), because --line was then
   toned against --surface while the shell actually paints --page-bg — so the shared token was too faint to
   use here. That comment carried its own retirement instruction: when the shared token is fixed, come back
   to var(--line). #1359 fixed it (--line stepped one rung in both themes, to exactly the two values this
   rule had hard-coded), so the local override is now redundant BY VALUE — it is retired here rather than
   left as a second place the same decision lives. One rule, one token, and the light/dark flip goes back to
   being the token's job. */
.planner-pills-rail {
    position: relative;
    margin-block: calc(var(--space-2, 8px) * -1) var(--space-4, 16px);
    border-block-end: 1px solid var(--line);
}

/* THE "THERE IS MORE" SIGNAL. A mask, not a gradient overlay: it fades the CONTENT's own alpha, so it is
   correct on any background and in either theme without naming a colour (the ramp law — nothing is painted).
   planner-board.js adds .is-overflowing only while the row genuinely overflows, and removes it at the end of
   the scroll, so the fade never implies content that is not there. With JS off the row still scrolls; it just
   loses the hint, which is the honest degradation. */
.planner-pills-rail.is-overflowing .planner-pills {
    /* currentColor, not a hex: a mask reads only the ALPHA channel, so any opaque value works — and the
       colour law is "never hand-author a colour" without exception. A literal here (#000) was caught by
       css:check's literal ratchet, which is the gate doing its job: masks are exactly where a stray hex
       looks harmless and creeps in. currentColor is a keyword, carries alpha 1, and adds no literal. */
    mask-image: linear-gradient(to right, currentColor calc(100% - 2.5rem), transparent);
}

.planner-pills {
    display: flex;
    /* ONE LINE (#1351). The wrap was the bug: 27 chips became 4 rows on desktop and ~15 on a phone, pushing
       the board below the fold. Every tag is still here — it scrolls sideways instead of stacking. */
    flex-wrap: nowrap;
    align-items: center;
    gap: var(--space-2, 8px) var(--space-3, 12px);
    overflow-x: auto;
    /* A slim, ALWAYS-DRAWN scrollbar on pointer platforms — a second, redundant "more" cue beside the fade.
       Touch platforms auto-hide it, which is exactly why the fade carries the signal rather than the bar. */
    scrollbar-width: thin;
    /* Keeps a snapped/keyboard-focused chip clear of the rail edge instead of flush against it. */
    scroll-padding-inline: var(--space-3, 12px);
    /* BREATHING ROOM (#1357). The scrollbar is drawn inside this box, so its gutter has to be paid for here
       or the bar sits directly against the chips it scrolls — James: "we want to make sure we have breathing
       room so it's not touching the pills right above it". --space-2 clears a thin bar and still leaves a gap
       between the bar and the rail's hairline below. */
    padding-block-end: var(--space-2, 8px);
}

/* Chips size to their content and never squash — a compressed chip row would hide the overflow the rail
   exists to advertise. */
.planner-pills > * {
    flex: 0 0 auto;
}

/* The small vertical divider between the majors group and the label group: a 1px --line rule, kept short so
   it reads as a group separator, sitting between the majors and the first label on the opening line. */
.planner-pills-divider {
    flex-shrink: 0;
    align-self: center;
    width: 1px;
    height: 1.4rem;
    background: var(--line);
}

/* Inactive pills tint by their family (--planner-cat); 'All' has none, so it falls back to a neutral
   --text-muted tint. Text blends toward --text so it stays readable in both themes. */
.planner-pill {
    display: inline-flex;
    flex-shrink: 0;   /* pills keep their natural size and WRAP to the next line — never shrink */
    align-items: center;
    gap: 5px;
    padding: 5px 13px 5px 11px;
    font-size: var(--text-sm);
    font-weight: 500;
    color: color-mix(in srgb, var(--planner-cat, var(--text-muted)) 45%, var(--text));
    background: color-mix(in srgb, var(--planner-cat, var(--text-muted)) 9%, var(--surface-raised));
    border: 1px solid color-mix(in srgb, var(--planner-cat, var(--text-muted)) 30%, var(--line));
    border-radius: var(--radius-full, 999px);
    text-decoration: none;
    cursor: pointer;
}

/* Pill scent icon (Card anatomy v2). The 'All' pill has none. */
.planner-pill svg {
    width: 14px;
    height: 14px;
    flex-shrink: 0;
}

.planner-pill:hover {
    color: color-mix(in srgb, var(--planner-cat, var(--text-muted)) 62%, var(--text));
    border-color: color-mix(in srgb, var(--planner-cat, var(--text-muted)) 52%, var(--line));
}

/* Active pill = a SOLID inversion of its OWN family (Tag model v2: red-as-active would falsely read as
   a category in a hue-means-category row). --planner-active-fill is the per-family rung verified ≥4.5:1
   with white text; 'All' has no family, so it falls back to the tenant primary. Distinguishable by more
   than colour — the solid fill + aria-current carry the selected state, never the hue alone (a11y). */
.planner-pill.is-active {
    color: var(--on-primary);
    background: var(--planner-active-fill, var(--primary-600));
    border-color: transparent;
}

.planner-pill-tag {
    font-size: 12px;
}

/* The live card count trailing each pill's label. Dimmed so the name leads; inverts with the pill when
   active (inherits the white text, sits on a translucent wash of it). */
.planner-pill-count {
    margin-left: 5px;
    font-variant-numeric: tabular-nums;
    opacity: 0.66;
}

.planner-pill.is-active .planner-pill-count {
    opacity: 0.82;
}

/* The vertical divider is back as .planner-pills-divider (ticket 1217, above) — the one-row layout splits
   the majors from the labels with a 1px rule instead of the retired two-segment hairlines (ticket 1066). */

/* ===== Card-detail MODAL — the ONE detail view, opened as a fast overlay over the board. The five-pane
   body is AJAX-injected (planner-board.js) into an x-dialog; wide enough for the desktop five-column
   layout, and on mobile it's the x-tabs one-pane-at-a-time view (the same body the deep-link page uses).
   Sized by ID — x-dialog keeps its literal class="dialog", so a passed class attribute is dropped (the
   other dialogs set their width by #id too). */
#planner-card-modal {
    /* Two tabs replaced the five-panes-in-one-wide-view, so the modal no longer needs near-fullscreen —
       comfortably wide is enough (the .dialog min() still caps it to the viewport). */
    --dialog-width: 1040px;
}

#planner-card-modal .dialog-body {
    display: flex;
    min-height: 0;
    padding-top: 4px;
}

.planner-card-modal-body {
    flex: 1;
    min-width: 0;
    min-height: 45vh;   /* holds the loading state + keeps the modal from collapsing before the fetch lands */
}

/* @toc Pricing table */
/*
 * x-pricing-table — the pricing skin over the x-table structure it composes (#1212, spec §6). x-table owns the
 * <table>, the <caption>, the <th scope="col"> tier headers, and the horizontal-scroll container (.table-wrap
 * overflow-x) that lets the matrix fit any container without a second DOM — so this file only skins the matrix:
 * centre the tier columns, draw the highlighted column, and style the price / ✓ / — cells.
 *
 * SECTION-AUTHORING LAW: one immutable DOM; the table fits its container by scrolling (container-first, no
 * --bp-*, no viewport assumption). (§6's stacked-cards-below-md is a display PREFERENCE that would need x-table
 * itself to gain a responsive-cards mode — a shared-primitive change, tracked separately; the scroll container
 * meets the law here.) COLOUR: role tokens only; the highlight tint is color-mix on the ramp, never a literal.
 */

/* Tier columns (every column but the lead feature-label column) centre their header and their cells. `center`
   is not a physical value, so it needs no logical form. */
.pricing-table th:not(:first-child) {
    text-align: center;
}

.pricing-table-cell {
    text-align: center;
}

/* ── the price row ────────────────────────────────────────────────────────────────────────────────────── */
.pricing-table-row-label {
    font-weight: 500;
    color: var(--text-muted);
}

.pricing-table-badge {
    display: block;
    width: fit-content;
    margin-inline: auto;
    margin-block-end: var(--space-2);
}

.pricing-table-price {
    display: block;
    font-size: var(--text-lg);
    font-weight: 600;
    color: var(--text);
}

.pricing-table-period {
    font-size: var(--text-sm);
    font-weight: 400;
    color: var(--text-muted);
}

/* ── the ✓ / — / value cells ─────────────────────────────────────────────────────────────────────────── */
/* ✓ — the shared check heroicon, --primary, decorative (the sr-only "Included" carries the meaning). */
.pricing-table-yes {
    display: inline-flex;
    width: 1.25rem;
    height: 1.25rem;
    color: var(--primary);
}

/* — the absence glyph, muted (the sr-only "Not included" carries the meaning). */
.pricing-table-no {
    color: var(--text-muted);
}

.pricing-table-value {
    font-size: var(--text-sm);
    color: var(--text);
}

/* ── the highlighted tier column (ring + badge law: the tint + rule is the ring, the badge is the label) ──
   The tint rides x-table's per-cell class passthrough (is-highlight on every cell of the featured column); the
   top/bottom cells add the block borders + the radius-8 corners so the column reads as one ruled panel. */
.pricing-table-cell.is-highlight {
    background: color-mix(in srgb, var(--primary) 6%, var(--surface));
    border-inline: 1px solid var(--line);
}

.pricing-table-cell.is-highlight-top {
    border-block-start: 1px solid var(--line);
    border-start-start-radius: var(--global-border-radius);
    border-start-end-radius: var(--global-border-radius);
}

.pricing-table-cell.is-highlight-bottom {
    border-block-end: 1px solid var(--line);
    border-end-start-radius: var(--global-border-radius);
    border-end-end-radius: var(--global-border-radius);
}

/* @toc Production checklist */
/*
 * x-production-checklist — mobile-first and container-first. The base is the truthful 320px stack; one
 * min-width container enhancement aligns label, note, and status without changing the DOM. Semantic tokens and
 * existing Progress/Badge primitives own every colour/status treatment. No raster, viewport query, or motion.
 */

.production-checklist {
    position: relative;
    container-type: inline-size;
    padding: var(--space-5);
    color: var(--text);
    background: var(--surface-raised);
    border: 1px solid var(--line);
    border-radius: var(--radius-container);
}

.production-checklist-decoration-rule::before {
    content: '';
    position: absolute;
    inset-block-start: 0;
    inset-inline: var(--space-4);
    block-size: 2px;
    background: var(--primary-600);
    pointer-events: none;
}

.production-checklist-header {
    display: grid;
    gap: var(--space-3);
    margin-block-end: var(--space-4);
}

.production-checklist-heading {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2) var(--space-4);
}

.production-checklist-title {
    margin: 0;
    font-size: var(--text-lg);
    font-weight: 700;
    line-height: 1.3;
}

.production-checklist-summary {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--text-muted);
}

.production-checklist-progress {
    inline-size: 100%;
}

.production-checklist-stages {
    display: grid;
    gap: 0;
    margin: 0;
    padding: 0;
    list-style: none;
}

.production-checklist-stage {
    display: grid;
    gap: var(--space-2);
    min-inline-size: 0;
    padding-block: var(--space-4);
    border-block-start: 1px solid var(--line);
}

.production-checklist-stage-label {
    min-inline-size: 0;
    font-weight: 600;
    overflow-wrap: anywhere;
}

.production-checklist-stage-state {
    justify-self: start;
}

.production-checklist-stage-note {
    min-inline-size: 0;
    margin: 0;
    font-size: var(--text-sm);
    line-height: 1.5;
    color: var(--text-muted);
    overflow-wrap: anywhere;
}

.production-checklist-empty {
    margin: 0;
    padding-block: var(--space-4);
    border-block-start: 1px solid var(--line);
    color: var(--text-muted);
    overflow-wrap: anywhere;
}

.production-checklist-compact {
    padding: var(--space-3);
}

.production-checklist-compact .production-checklist-stage {
    padding-block: var(--space-2);
}

@container (min-width: 40rem) {
    .production-checklist-stage {
        grid-template-columns: minmax(10rem, 1fr) minmax(0, 2fr) auto;
        align-items: start;
        column-gap: var(--space-4);
    }

    .production-checklist-stage-label {
        grid-column: 1;
        grid-row: 1;
    }

    .production-checklist-stage-state {
        grid-column: 3;
        grid-row: 1;
    }

    .production-checklist-stage-note {
        grid-column: 2;
        grid-row: 1;
    }
}

/* @toc Progress */
/*
 * Progress. A themed native <progress>. Every token is a --progress-* var from
 * config/components/progress.php['css']; this file only references those plus the
 * structural rules the element needs. Styling <progress> means resetting the UA
 * appearance, then painting the track (-webkit-progress-bar / -moz the host) and
 * the fill (-webkit-progress-value / -moz-progress-bar) separately. An element
 * with no value attribute is indeterminate — the browser animates it for free.
 */

.progress {
    /* Reset the UA chrome so the track/value pseudo-elements paint cleanly. */
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    display: block;
    width: var(--progress-width, 100%);
    height: var(--progress-height, 8px);
    border: none;
    border-radius: var(--progress-border-radius, var(--radius-full, 9999px));
    /* Firefox paints the track on the host element, not a -bar pseudo. */
    background: var(--progress-track, var(--surface-raised));
    overflow: hidden;
    color: var(--progress-value, var(--primary));
}

/* WebKit / Blink: the track. */
.progress::-webkit-progress-bar {
    background: var(--progress-track, var(--surface-raised));
    border-radius: var(--progress-border-radius, var(--radius-full, 9999px));
}

/* WebKit / Blink: the filled portion. */
.progress::-webkit-progress-value {
    background: var(--progress-value, var(--primary));
    border-radius: var(--progress-border-radius, var(--radius-full, 9999px));
    transition: width 0.25s ease;
}

/* Firefox: the filled portion. */
.progress::-moz-progress-bar {
    background: var(--progress-value, var(--primary));
    border-radius: var(--progress-border-radius, var(--radius-full, 9999px));
}

/* Indeterminate: no value attribute. WebKit ignores ::-webkit-progress-value
 * here, so we drive a looping sweep on the track itself. */
.progress:indeterminate {
    background-image:
        linear-gradient(
            90deg,
            transparent 0%,
            var(--progress-value, var(--primary)) 50%,
            transparent 100%
        );
    background-size: 40% 100%;
    background-repeat: no-repeat;
    background-position: -40% 0;
    animation: progress-indeterminate 1.2s infinite ease-in-out;
}

/* Firefox draws a solid -moz-progress-bar while indeterminate; blank it so only
 * the host's looping sweep shows. */
.progress:indeterminate::-moz-progress-bar {
    background: transparent;
}

@keyframes progress-indeterminate {
    0% {
        background-position: -40% 0;
    }
    100% {
        background-position: 140% 0;
    }
}

/* Respect reduced-motion: hold the sweep mid-track instead of looping. */
@media (prefers-reduced-motion: reduce) {
    .progress:indeterminate {
        animation: none;
        background-position: 50% 0;
    }

    .progress::-webkit-progress-value {
        transition: none;
    }
}

/* Keyboard focus ring, matching the other primitives. The transparent outline is
 * the forced-colors fallback (box-shadow is dropped in high-contrast mode). */
.progress:focus-visible {
    outline: 2px solid transparent;
    outline-offset: 2px;
    box-shadow: 0 0 0 3px var(--progress-focus-ring, var(--primary));
}

/* @toc Pt answer field */
/* x-pt-answer-field (#1349) — the writing surface. Structure + the field's own reading treatment; the panel
 * around it owns the outer chrome. Colour comes only from semantic tokens + the ramp. */

.pt-answer-field {
    display: flex;
    flex-direction: column;
    gap: var(--pt-answer-field-gap, 8px);
}

/* The class lands on the <textarea> ITSELF — x-textarea ends its markup with $attributes->merge on the field,
 * not on its wrapper. (A descendant selector here matches nothing, which is how the first build shipped a box
 * still at its four-row default.)
 *
 * IT FILLS ITS PANEL ON PURPOSE. This is the exercise — a 90-minute exam answer — and a short box says the
 * opposite. `resize: vertical` leaves the final say with the person writing.
 *
 * SCOPED THROUGH THE ROOT ON PURPOSE. x-textarea styles this same element with `.textarea-field` — one class,
 * exactly the specificity a bare `.pt-answer-field-input` would have — and CssService bundles component sheets
 * alphabetically, so textarea.css lands AFTER this file and would win the tie. The root makes it (0,2,0).
 *
 * THIS RULE WAS SILENTLY DROPPED TWICE, both times by comment punctuation, and it is worth knowing how because
 * the symptom is indistinguishable from losing a specificity fight — the token emits 1rem and the element
 * computes 14px either way. First a stray continuation line sat outside a closed comment; then, writing THIS
 * note, a comment-close sequence inside the prose ended the block early and the remaining text became garbage
 * that swallowed the selector after it. Never put a comment-close sequence in comment text. And to tell the two
 * causes apart, read the APPLIED RULE LIST off document.styleSheets — an absent rule is a parse failure, a
 * present-but-overridden one is a cascade problem, and the computed value cannot distinguish them. */
.pt-answer-field .pt-answer-field-input {
    min-block-size: var(--pt-answer-field-min-height, 28rem);
    font-size: var(--pt-answer-field-font-size, var(--text-base));
    line-height: var(--pt-answer-field-line-height, 1.75);
    resize: vertical;
}

/* The status line RESERVES ITS HEIGHT even when empty, so the first "Saving…" does not shove the textarea up
   the page while she is typing into it. */
.pt-answer-field-status {
    margin: 0;
    min-block-size: 1.2em;
    font-size: var(--pt-answer-field-status-font-size, var(--text-xs));
    color: var(--pt-answer-field-status-color, var(--text-muted));
}

/* A failed save is the one state worth colouring — it is the only one that asks her to do something. Status
   meaning comes from the categorical token, never a hand-authored colour. */
.pt-answer-field-status[data-state="error"] {
    color: var(--pt-answer-field-error-color, var(--danger));
    font-weight: 600;
}

/* @toc Reveal phase */
/* Reveal phase — the study reader's phase control: an APG radio group rendered as a segmented control (native
 * radio inputs; the C direction reskins it as a numbered stepper — same semantics). STRUCTURE is locked; the
 * decorative skin waits on the direction pick. The real <input> is .sr-only; the active segment is driven by
 * :has(:checked) and the focus ring by :has(:focus-visible) — no JS for the visual state. Config:
 * config/components/reveal-phase.php (--reveal-phase-border-radius). View: reveal-phase.blade.php; dispatch +
 * persistence: reveal-phase.js. */

.reveal-phase {
    display: inline-flex;
    gap: 2px;
    padding: 3px;
    background: var(--surface-muted);
    border: 1px solid var(--line);
    border-radius: var(--reveal-phase-border-radius, var(--radius-control));
}

.reveal-phase-segment {
    position: relative;
    display: inline-flex;
    align-items: center;
    padding: 5px var(--space-3);
    border-radius: calc(var(--reveal-phase-border-radius, var(--radius-control)) - 2px);
    font-size: var(--text-sm);
    font-weight: 500;
    color: var(--text-muted);
    white-space: nowrap;
    cursor: pointer;
    user-select: none;
}

.reveal-phase-segment:hover {
    color: var(--text);
}

/* The active segment (:has the checked input) — a raised fill that pops from the recessed track. */
.reveal-phase-segment:has(.reveal-phase-input:checked) {
    color: var(--text);
    background: var(--surface);
    box-shadow: 0 1px 2px color-mix(in srgb, var(--text) 12%, transparent);
}

/* The focus ring rides the sr-only input's :focus-visible onto its segment (the visible control). */
.reveal-phase-segment:has(.reveal-phase-input:focus-visible) {
    outline: 2px solid var(--primary);
    outline-offset: 1px;
}

/* @toc Room canvas */
/* @toc Room canvas */

/* room-canvas (#1465) — the room authoring surface.
 *
 * MOBILE-FIRST, AND THAT IS THE CARD RATHER THAN A POLISH PASS. The base rules ARE the 390px layout: the
 * toolbar sits above the canvas, the canvas takes the width it is given, and every control is finger-sized.
 * Desktop is the min-width enhancement.
 *
 * ★ IT RESPONDS TO ITS CONTAINER, NEVER THE VIEWPORT. A landscape phone is 844px wide, so a width-only media
 * query classifies it as a desktop and hands a thumb a layout drawn for a mouse. `container-type: inline-size`
 * plus @container means the surface answers to the box it is actually in — which is also what makes it
 * truthful inside the editor's device frame and beside a docked 240px sidebar. */
/* ★ THE CONTAINER AND THE THING THAT RESPONDS MUST BE DIFFERENT ELEMENTS. `.room-canvas` establishes the
 * container; `@container` rules style its DESCENDANTS, never itself — so a `display: grid` written on this
 * element inside the query is silently ignored while its children's rules apply normally. That is exactly how
 * the first 844px shot came out: the toolbar stacked (a descendant rule, applied) but the two-column layout
 * never happened (a self rule, dropped), giving a landscape phone six full-width buttons above the canvas.
 *
 * ★ HALF THE RULE APPLYING IS WHAT MADE IT LOOK DELIBERATE. Nothing errored, every gate stayed green, and the
 * result was a plausible-looking layout — so the only thing that could catch it was opening the screenshot.
 * If you are tempted to "simplify" this wrapper away, that is the bug you will reintroduce.
 * `.room-canvas-layout` exists solely to be a descendant that can answer. */
.room-canvas {
    container-type: inline-size;
    min-block-size: var(--room-canvas-min-block-size, 320px);
}

.room-canvas-layout {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

/* The toolbar WRAPS rather than scrolls. A horizontally scrolling toolbar hides its own controls at exactly
 * the width where there is least room to discover them. */
.room-canvas-toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: var(--room-canvas-toolbar-gap, var(--space-2));
    padding: var(--room-canvas-toolbar-padding, var(--space-2));
    background-color: var(--room-canvas-toolbar-background-color, var(--surface-raised));
    border-radius: var(--room-canvas-border-radius, var(--radius-container));
}

.room-canvas-surface {
    inline-size: 100%;
    /* The room's own 16:9 space, so the canvas never letterboxes differently from the geometry inside it. */
    aspect-ratio: 16 / 9;
    /* ★ A HEIGHT CAP, because 16:9 of a WIDE viewport is TALLER THAN A SHORT ONE. At 844px wide the canvas
     * wants 475px, which does not fit a 390px-tall landscape phone — the surface would run off the bottom
     * with the toolbar pushed off with it. This is the one place a VIEWPORT unit is right: the constraint
     * genuinely is the screen's height, and no container query can report it. `svh` rather than `vh` so a
     * mobile browser's retracting address bar cannot make the canvas taller than the space it actually has. */
    max-block-size: 68svh;
    background-color: var(--room-canvas-background-color, var(--surface-muted));
    border: 1px solid var(--room-canvas-border-color, var(--line));
    border-radius: var(--room-canvas-border-radius, var(--radius-container));
    /* The gesture handler owns the pointer. Without this the browser pans the page instead of the room, and a
     * one-finger drag scrolls away from the surface being drawn on. */
    touch-action: none;
}

.room-canvas-surface:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

.room-canvas-bounds {
    fill: none;
    stroke: var(--room-canvas-border-color, var(--line));
    stroke-width: 1;
}

/* Geometry, coloured by what it MEANS rather than by family: a solid is mass, an anchor is a destination, a
 * hazard is a warning. The status tokens already name those and are AA by construction. */
.room-canvas-item {
    cursor: pointer;
}

.room-canvas-item-solids {
    fill: var(--room-canvas-solid-fill, var(--neutral-500));
    stroke: var(--room-canvas-solid-border-color, var(--neutral-300));
    stroke-width: 1;
}

.room-canvas-item-anchors {
    fill: var(--room-canvas-anchor-fill, var(--info));
    fill-opacity: 0.55;
    stroke: var(--room-canvas-anchor-fill, var(--info));
    stroke-width: 1;
}

.room-canvas-item-hazards {
    fill: var(--room-canvas-hazard-fill, var(--warning));
    fill-opacity: 0.45;
    stroke: var(--room-canvas-hazard-fill, var(--warning));
    stroke-width: 1;
}

/* Selection is a STROKE, never a fill change — a filled selection hides the thing being selected, and on a
 * phone the finger is already covering half of it. */
.room-canvas-item[data-room-selected] {
    stroke: var(--room-canvas-selected-border-color, var(--primary));
    stroke-width: var(--room-canvas-selected-border-width, 2px);
}

.room-canvas-item:focus-visible {
    outline: none;
    stroke: var(--primary);
    stroke-width: var(--room-canvas-selected-border-width, 2px);
}

/* A handle is drawn small and hit large. The visible square is 12px so it does not bury the geometry; the
 * transparent companion is 44px because that is what a finger needs. */
.room-canvas-handle {
    fill: var(--room-canvas-handle-fill, var(--primary));
}

.room-canvas-handle-hit {
    fill: transparent;
    cursor: grab;
}

.room-canvas-empty {
    margin: 0;
    padding: var(--space-3);
    color: var(--text-muted);
    font-size: var(--text-sm);
    text-align: center;
}

.room-canvas-create {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

/* DESKTOP IS THE ENHANCEMENT — min-width polarity, keyed on the CONTAINER. At 40rem the toolbar can sit
 * beside the canvas because there is room for it to, not because the window is wide. */
@container (min-width: 40rem) {
    .room-canvas-layout {
        display: grid;
        /* `max-content` rather than `auto`: an auto track lets the toolbar's buttons stretch and eat the row,
         * which is how the first attempt handed a landscape phone six full-width buttons. */
        grid-template-columns: max-content 1fr;
        align-items: start;
    }

    .room-canvas-toolbar {
        flex-direction: column;
        position: sticky;
        inset-block-start: var(--space-2);
    }

    .room-canvas-empty {
        grid-column: 2;
    }
}

/* @toc Section cards */
/*
 * x-section-cards — the card-display section's card skin (#1228). The RESPONSIVE GRID is the x-page-section
 * chassis's (.page-section-items, 1 → N on the section's OWN box via @container), so this file skins only the
 * card and needs no @media / @container of its own: the card is intrinsically fluid and the chassis owns the
 * columns. SECTION-AUTHORING LAW: one immutable DOM, container-first, no --bp-*, no viewport assumption.
 * COLOUR: semantic role tokens only (surface / line / text / text-muted / primary); the hover lift is a
 * color-mix on the ramp, never a literal.
 */

/* The card — a surface tile, a full-height flex column so the chips sit at a consistent baseline whatever the
   summary length. A whole-card LINK (<a>) carries the hover + focus affordance; a plain <div> card (a row with
   no safe href) shares the skin without the interaction. */
.section-cards-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    height: 100%;
    padding: var(--space-4);
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--global-border-radius);
    color: var(--text);
    text-decoration: none;
}

/* The whole-card link affordance — a[href] only (a plain-div card is not interactive). */
a.section-cards-card {
    transition: border-color 150ms ease, background-color 150ms ease;
}

a.section-cards-card:hover {
    border-color: var(--primary);
    background: color-mix(in srgb, var(--primary) 4%, var(--surface));
}

a.section-cards-card:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

.section-cards-eyebrow {
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
}

.section-cards-title {
    font-size: var(--text-base);
    font-weight: 600;
    color: var(--text);
}

/* The preview line — muted, clamped to three lines so the cards keep an even height in the grid. */
.section-cards-summary {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    line-clamp: 3;
    overflow: hidden;
    font-size: var(--text-sm);
    color: var(--text-muted);
}

/* Pin the chips to the card foot regardless of summary length, so a grid of cards aligns its badge row. */
.section-cards-chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-block-start: auto;
    padding-block-start: var(--space-1);
}

.section-cards-empty {
    color: var(--text-muted);
}

/* @toc Section cta */
/*
 * x-section-cta — the CTA band (spec §3). Composes the x-page-section chassis with ZERO chassis edits: every
 * rule here is scoped under .section-cta, so page-section.css is untouched. The chassis explicitly sanctions
 * this seam ("a section that centres (e.g. a CTA band) overrides text-align + align-items in its own CSS").
 *
 * Section-Authoring Law: the BASE rules ARE the 375 layout (one column, --space-* rhythm, clamp() type); desktop
 * is a pure @container (min-width) ENHANCEMENT on the section's OWN inline size — no --bp-*, no @media, no
 * matchMedia. ONE immutable DOM: `orientation` and `reverse` rearrange the same markup by CSS alone.
 *
 * THE PANEL is the chassis's .page-section-inner — the centred content column, and the only region that holds
 * BOTH the header (title · description · actions) and the body (the demo pane), which the band's panel must.
 * Its padding IS the panel's padding, re-scaled to the CTA's values (X 24 → 48 → 64, Y 48 → 96); the section's
 * vertical rhythm against its neighbours comes from their own padding. Colour is semantic tokens only, so the
 * band themes per tenant in both schemes.
 */

/* The panel — radius one tier rounder than a card (12 vs 8) because the band is a page-level object, and
   overflow:hidden so a demo island/media pane clips to the rounded corner. */
.section-cta .page-section-inner {
    overflow: hidden;
    border-radius: var(--radius-lg);
    padding-block: var(--space-12);
    padding-inline: var(--space-6);
}

@container (min-width: 640px) {
    .section-cta .page-section-inner {
        padding-block: var(--space-24);
        padding-inline: var(--space-12);
    }
}

@container (min-width: 1024px) {
    .section-cta .page-section-inner {
        padding-inline: var(--space-16);
    }
}

/* Variants — the surface treatment. `solid` (the inverted panel) is DEFERRED (spec §3 / ruling R7): it needs the
   inverse-surface token and is deliberately NOT faked with raw ramp rungs, so the set ships four. Alpha rides
   color-mix on a BACKGROUND only, which the colour law sanctions. */
.section-cta--outline .page-section-inner {
    background: var(--surface);
    border: 1px solid var(--line);
}

.section-cta--soft .page-section-inner {
    background: color-mix(in srgb, var(--surface-raised) 50%, transparent);
}

.section-cta--subtle .page-section-inner {
    background: color-mix(in srgb, var(--surface-raised) 50%, transparent);
    border: 1px solid var(--line);
}

.section-cta--naked .page-section-inner {
    background: transparent;
}

/* The band's title runs one tier shorter than a section header (30 → 36, not the chassis's 30 → 48) — a closing
   prompt, not a page title. Fluid on the section's OWN width (cqi), per the law. */
.section-cta .page-section-title {
    font-size: clamp(1.875rem, 1.6rem + 1.2cqi, 2.25rem);
}

/* The band's internal rhythm: title → description 32 and description → actions 32 (the chassis ships 16 / 24).
   With no description, the title → actions gap is the same 32. The actions row keeps the chassis's 12/24 gaps. */
.section-cta .page-section-description {
    margin-block-start: var(--space-8);
}

.section-cta .page-section-actions {
    margin-block-start: var(--space-8);
}

/* Vertical orientation (default) — the band centres its title, description and actions. */
.section-cta--vertical .page-section-head {
    align-items: center;
    text-align: center;
}

.section-cta--vertical .page-section-description {
    margin-inline: auto;
}

.section-cta--vertical .page-section-actions {
    justify-content: center;
}

/* Horizontal orientation — content beside the demo pane. Stacked at the base (the chassis's flex column, whose
   32 gap IS the content↔media gap); a 2-col grid once the section's own box is wide enough, gap 64. `reverse`
   swaps the columns by order alone — the DOM is identical either way. */
@container (min-width: 1024px) {
    .section-cta--horizontal .page-section-inner {
        display: grid;
        grid-template-columns: 1fr 1fr;
        align-items: center;
        gap: var(--space-16);
    }

    /* Grid children may hold a wide island — min-width:0 keeps it from blowing the column out. */
    .section-cta--horizontal .page-section-head,
    .section-cta--horizontal .page-section-body {
        min-width: 0;
    }

    .section-cta--reverse .page-section-head {
        order: 2;
    }
}

/* @toc Section features */
/*
 * x-section-features — the feature grid's ITEM SHAPES (spec §2). The x-page-section chassis owns the header
 * grammar AND the responsive items grid (1 → 2 → 3 columns on the section's own width, gap 32), so there is
 * deliberately NO grid CSS here — composing the chassis with zero edits is the whole point of the pattern.
 *
 * Section-Authoring Law: the base rules ARE the 375 layout; the only desktop delta (card padding 16 → 24) is a
 * pure @container (min-width) ENHANCEMENT that resolves against the chassis's .page-section container context,
 * so the section reflows on its OWN box and never viewport-assumes (no --bp-*, no @media). ONE immutable DOM at
 * every width: the SAME flat item markup (icon · title · description · footer) is stacked by the card shape and
 * rearranged into a row by the line shape — CSS only, never markup.
 *
 * Colour is semantic tokens only (--surface, --line, --text, --text-muted, --primary for the decorative icon,
 * --link for hovered link text), so the section themes per tenant in both schemes. Token-sourced is NOT the
 * same as contrast-compliant, so the two are picked per ROLE: --primary is judged as non-text (3:1) and only
 * ever tints the aria-hidden icon; text that changes colour uses --link, which is rung-stepped per theme to
 * clear 4.5:1. No left-rail accents (the anti-slop law) — the card is a clean bordered surface.
 *
 * Two spec values sit OFF the --space-/--radius- ladders (icon↔text 10, line radius 4). They are written as
 * literals rather than snapped to a neighbouring rung (8/12, 6) because the spec fixes them and picking a
 * different number would be authoring skin — flagged for the design pass, not silently normalized.
 *
 * CHASSIS SURFACE TOUCHED (the section-tier ruling): NONE — no --page-section-* knob is overridden and no
 * .page-section-* anatomy class is styled here. Every selector below is this section's OWN .section-features-*
 * surface. The chassis's default items grid already IS spec §2's grid, so there is nothing to reach for.
 */

/* The item, both shapes. A linked item must not inherit the UA anchor look; its keyboard ring is the house
   :focus-visible outline from base.css (a real outline, so it survives forced-colors and follows the radius) —
   no ring is authored here. */
.section-features-item {
    color: inherit;
    text-decoration: none;
}

.section-features-icon {
    display: inline-flex;
    width: var(--space-5);
    height: var(--space-5);
    flex-shrink: 0;
    color: var(--primary);
}

.section-features-icon svg {
    display: block;
    width: 100%;
    height: 100%;
}

.section-features-title {
    margin: 0;
    font-size: var(--text-base);
    font-weight: 600;
    line-height: 1.5;
    color: var(--text);
    transition: color 150ms;
}

.section-features-description {
    margin: 0;
    font-size: var(--text-sm);
    line-height: 1.6;
    color: var(--text-muted);
}

/* A linked item gets the house hover delta on its title (their theme ships none; ours adds the standard 150ms
   colour hover). Colour-only, so there is no motion to guard for reduced-motion.
   --link, NOT --primary: this tints REAL 16px/600 text, which is normal-size under WCAG, so it needs 4.5:1.
   Bare --primary (primary-500) is only 3.76:1 on the light --surface with the live ramp — the exact miss the
   purpose-built --link token exists to fix (A11y-D: primary-700 light / primary-400 dark, stepping opposite
   ways per theme). The decorative icon above keeps --primary: it is aria-hidden non-text, judged at 3:1. */
a.section-features-item:hover .section-features-title {
    color: var(--link);
}

/* ---- CARD (rich): a bordered surface — icon → title → description, with the footer pinned to the bottom.
   The item is a flex column and grid stretches it to the row height, so margin-block-start:auto floats the
   footer to the card's base regardless of how much copy the item carries. ---- */
.section-features-card .section-features-item {
    display: flex;
    flex-direction: column;
    align-items: start;
    padding: var(--space-4);
    border: 1px solid var(--line);
    border-radius: var(--radius-container);
    background-color: var(--surface);
}

@container (min-width: 640px) {
    .section-features-card .section-features-item {
        padding: var(--space-6);
    }
}

.section-features-card .section-features-icon + .section-features-title {
    margin-block-start: 10px;
}

.section-features-card .section-features-title + .section-features-description {
    margin-block-start: var(--space-4);
}

.section-features-card .section-features-footer {
    margin-block-start: auto;
    padding-block-start: var(--space-4);
    font-size: var(--text-sm);
    color: var(--text-muted);
}

/* ---- LINE (compact): the same flat DOM, rearranged — the icon takes a leading column and the title sits over
   its one-line description. For dense lists. Radius 4 shapes the focus target's ring. ---- */
.section-features-line .section-features-item {
    display: grid;
    grid-template-columns: auto 1fr;
    column-gap: 10px;
    align-items: start;
    border-radius: 4px;
}

.section-features-line .section-features-icon {
    grid-column: 1;
    grid-row: 1;
}

.section-features-line .section-features-title {
    grid-column: 2;
    grid-row: 1;
}

.section-features-line .section-features-description {
    grid-column: 2;
    grid-row: 2;
}

/* @toc Section hero */
/*
 * x-section-hero — the flagship marketing section (SPEC 2 §1). Composes x-page-section; every rule below is
 * scoped to .section-hero and restyles only the chassis's PUBLISHED anatomy — the section-layer contract in
 * the component docblock (KNOBS FIRST, then documented anatomy only, and the law still binds).
 *
 * Mobile-first + container-first (the Section-Authoring Law): the 375 base IS the chassis's single column
 * carrying the hero's deeper rhythm and larger type; the content | demo 2-col layout is a pure
 * @container (min-width: 1024px) ENHANCEMENT on the section's OWN inline size. ONE immutable DOM — `reverse`
 * swaps the columns by grid placement, never by re-ordering markup. No --bp-*, no @media, no matchMedia.
 *
 * SCALE NOTE: the hero runs one padding tier deeper than the chassis (96 → 128 → 160 against its
 * 64 → 96 → 128). The 160 tier reads --space-40 — the contiguous Tailwind rung added to the scale for
 * exactly this (the chassis's --space-28/32 precedent: the section tier extends the ladder as it needs it).
 *
 * The chassis's actions row already ships the hero's gaps (row 12 / column 24), so it is deliberately not
 * overridden here.
 */

/* Base (375) — single column, hero rhythm: 96 padding, 64 between the content and the demo. */
.section-hero .page-section-inner {
    gap: var(--space-16);
    padding-block: var(--space-24);
}

/* Hero type — the section's own scale (48 → 72), a step above the chassis's 30 → 48. `pretty` over the
   chassis's `balance`: a hero title runs long enough that orphan control beats line-length evening. */
.section-hero .page-section-title {
    font-size: clamp(3rem, 2.25rem + 3.2cqi, 4.5rem);
    text-wrap: pretty;
}

.section-hero .page-section-description {
    margin-block-start: var(--space-10);
    font-size: clamp(1.125rem, 1.05rem + 0.4cqi, 1.25rem);
    line-height: 1.6;
}

/* Hero rhythm inside the content column: eyebrow → title 16, title → body 40, body → actions 40. */
.section-hero .page-section-eyebrow + .page-section-title {
    margin-block-start: var(--space-4);
}

.section-hero .page-section-actions {
    margin-block-start: var(--space-10);
}

/* The island pane. The hero gives the cell and nothing else — no radius, border, shadow, or sizing (SPEC §1:
   the hero never constrains, sizes, or styles the island's internals). min-width: 0 only so a wide island can
   shrink inside its grid track instead of forcing overflow. */
.section-hero .page-section-body {
    min-width: 0;
}

/* Stacked enhancement (≥640): the rhythm opens up before the layout ever splits. */
@container (min-width: 640px) {
    .section-hero .page-section-inner {
        gap: var(--space-24);
        padding-block: var(--space-32);
    }
}

/* The 2-col enhancement (≥1024) — content | demo on the section's own width. */
@container (min-width: 1024px) {
    .section-hero .page-section-inner {
        display: grid;
        grid-template-columns: 1fr 1fr;
        align-items: center;
        gap: var(--space-16);
        padding-block: var(--space-40);
    }

    /* `reverse` — demo leads, content follows. Explicit placement (not `order`) so the auto-placed footer
       cannot be dragged ahead of them. */
    .section-hero--reverse .page-section-head {
        grid-column: 2;
        grid-row: 1;
    }

    .section-hero--reverse .page-section-body {
        grid-column: 1;
        grid-row: 1;
    }

    /* The footer trails the whole section, never a single column. */
    .section-hero .page-section-footer {
        grid-column: 1 / -1;
    }
}

/* @toc Section pricing */
/*
 * x-section-pricing — the plan-card shape that fills the x-page-section chassis's items grid (spec §6).
 *
 * SECTION-AUTHORING LAW: mobile-first + container-first. The base rules ARE the 375 layout (one column — the
 * chassis's items grid is 1-col until its own width earns more); the desktop delta is a pure CONTAINER response,
 * carried by clamp() keyed to `cqi` (the chassis owns container-type, so cqi resolves against the SECTION's
 * inline size — fluid padding/type with no --bp-*, no @media, no viewport assumption). ONE immutable DOM: this
 * styles the plan cards as descendants; nothing here changes structure by width.
 *
 * COLOUR: role tokens only — text --text / --text-muted, border --line, fill --surface, the highlight RING
 * --primary (a non-text 2px outline, judged against the 3:1 non-text threshold). No hand-authored colour.
 */

.section-pricing-plan {
    display: flex;
    flex-direction: column;
    gap: var(--space-6);
    /* radius 8 (the container knob), 1px rule, fluid padding 24 → 40 (the spec's 24 → 32 → 40 as a container
       curve). position:relative anchors the highlight badge. */
    position: relative;
    padding: clamp(1.5rem, 1.25rem + 2cqi, 2.5rem);
    border: 1px solid var(--line);
    border-radius: var(--section-pricing-border-radius);
    background: var(--surface);
}

/* HIGHLIGHT: a 2px inset --primary ring AND a badge (never colour alone). Inset box-shadow, so it adds no box
   and causes no layout shift between a plain and a featured plan. */
.section-pricing-plan.is-highlight {
    box-shadow: inset 0 0 0 2px var(--primary);
}

/* The "Popular" / per-plan badge sits at the top of the card, before the title in the reading order. */
.section-pricing-badge {
    align-self: flex-start;
}

.section-pricing-title {
    margin: 0;
    font-size: clamp(1.5rem, 1.35rem + 1.2cqi, 1.875rem);   /* 24 → 30 */
    font-weight: 600;
    color: var(--text);
}

.section-pricing-description {
    margin: 0;
    font-size: var(--text-base);
    color: var(--text-muted);
}

/* The price line: current amount is the loud figure; a struck former price sits before it; the period trails. */
.section-pricing-price {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-2);
    margin: 0;
}

.section-pricing-amount {
    font-size: clamp(1.875rem, 1.65rem + 1.8cqi, 2.25rem);   /* 30 → 36 */
    font-weight: 600;
    color: var(--text);
}

.section-pricing-was {
    font-size: clamp(1.25rem, 1.15rem + 0.8cqi, 1.5rem);   /* 20 → 24, struck */
    color: var(--text-muted);
}

.section-pricing-period {
    font-size: var(--text-base);
    color: var(--text-muted);
}

.section-pricing-billing {
    margin: 0;
    font-size: var(--text-xs);
    font-weight: 500;
    color: var(--text-muted);
}

.section-pricing-features {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    margin: 0;
    padding: 0;
    list-style: none;
}

.section-pricing-feature {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
    font-size: var(--text-sm);
    color: var(--text-muted);
}

/* The ✓ glyph — 20px, --primary; decorative (aria-hidden on the wrapper), the label carries the meaning. */
.section-pricing-check {
    display: inline-flex;
    flex-shrink: 0;
    width: 1.25rem;
    height: 1.25rem;
    color: var(--primary);
}

.section-pricing-feature-label {
    min-width: 0;
    overflow-wrap: anywhere;
}

/* The CTA pins to the card bottom so buttons line up across plans of uneven copy, and spans the card width. */
.section-pricing-cta {
    margin-block-start: auto;
    justify-content: center;
    width: 100%;
}

.section-pricing-terms {
    margin: 0;
    font-size: var(--text-xs);
    line-height: 1.7;
    color: var(--text-muted);
}

/* @toc Section stats */
/*
 * x-section-stats — the stat band + the receipt variant (spec §4 of the marketing-section library). Authored
 * mobile-first + container-first (the Section-Authoring Law): every rule here IS the 375 base, and the only
 * desktop step is an @container (min-width) enhancement measured on the SECTION's own inline size — never the
 * viewport (no --bp-*, no @media, no matchMedia). The default variant adds NO grid of its own: the chassis's
 * .page-section-items already lays out 1 → 2 → 3 at gap 32, so this styles only what is inside a card. Type is
 * fluid via clamp() + cqi; every colour is a semantic token, so the band themes per tenant; box properties are
 * logical, so an RTL tenant reads correctly for free.
 */

/* --- Default variant: one card per stat, laid out by the chassis's items grid ---------------------------- */

.section-stats-card {
    display: flex;
    flex-direction: column;
    align-items: start;
    gap: var(--space-2);
    padding: var(--space-4);
    border: 1px solid var(--line);
    border-radius: var(--section-stats-border-radius);
    background: var(--surface);
}

.section-stats-icon {
    display: inline-flex;
    flex: none;
    inline-size: var(--space-5);
    block-size: var(--space-5);
    color: var(--primary);
}

.section-stats-icon svg {
    inline-size: 100%;
    block-size: 100%;
}

/* The figure — the pricing-price scale (30 → 36), adopted as our stat scale and made fluid on the section's box. */
.section-stats-value {
    margin: 0;
    font-size: clamp(1.875rem, 1.65rem + 1cqi, 2.25rem);
    font-weight: 600;
    line-height: 1.1;
    color: var(--text);
}

.section-stats-label {
    margin: 0;
    font-size: var(--text-sm);
    line-height: 1.4;
    color: var(--text-muted);
}

/* The citation line — the source law made visible. Muted and small, but never absent. */
.section-stats-source {
    margin: 0;
    font-size: var(--text-xs);
    line-height: 1.5;
    color: var(--text-muted);
}

.section-stats-marker {
    margin-inline-end: var(--space-1);
    font-weight: 600;
}

.section-stats-trend {
    margin-inline-start: var(--space-2);
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--text-muted);
}

/* --- Receipt variant: the band rendered as a receipt (the signature) ------------------------------------- */

.section-stats-receipt {
    padding: var(--space-4);
    border: 1px solid var(--line);
    border-radius: var(--section-stats-border-radius);
    background: var(--surface);
}

.section-stats-rows {
    margin: 0;
}

/* Ruled line-items: a dashed rule between rows; the last row is unruled so the band closes clean. */
.section-stats-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-4);
    padding-block: var(--space-3);
    border-block-end: 1px dashed var(--line);
}

.section-stats-row:first-child {
    padding-block-start: 0;
}

.section-stats-row:last-child {
    padding-block-end: 0;
    border-block-end: 0;
}

.section-stats-row-label {
    font-size: var(--text-sm);
    line-height: 1.4;
    color: var(--text-muted);
}

/* Mono figures, aligned to the end — the receipt's tell. `end`, not `right`, so RTL reads correctly. */
.section-stats-row-value {
    margin: 0;
    font-family: var(--font-mono);
    font-size: clamp(1.875rem, 1.65rem + 1cqi, 2.25rem);
    font-weight: 600;
    line-height: 1.1;
    text-align: end;
    color: var(--text);
}

/* The citations, set as receipt line-items. The ordered-list numbering IS each row's marker. */
.section-stats-sources {
    margin-block-start: var(--space-4);
    padding-block-start: var(--space-4);
    border-block-start: 1px dashed var(--line);
}

.section-stats-sources-title {
    margin: 0 0 var(--space-2);
    font-size: var(--text-xs);
    line-height: 1.4;
    color: var(--text-muted);
}

.section-stats-sources-list {
    margin: 0;
    padding-inline-start: var(--space-5);
    font-size: var(--text-xs);
    line-height: 1.6;
    color: var(--text-muted);
}

/* --- The one desktop enhancement: the section's own box crosses 640 → the roomier padding step (16 → 24). --- */

@container (min-width: 640px) {
    .section-stats-card {
        padding: var(--space-6);
    }

    .section-stats-receipt {
        padding: var(--space-6);
    }
}

/* @toc Select */
/* Select. All --select-* tokens come from config/components/select.php['css'].
 * We restyle only the box (appearance:none) and overlay a chevron — the native
 * option list, keyboard, and typeahead are left to the platform. */

.select {
    margin-bottom: 14px;
}

.select-label {
    display: block;
    margin-bottom: 6px;
    color: var(--select-label-color);
    font-size: var(--select-label-font-size);
}

/* The box wraps the field so the chevron can sit over it. The chevron itself is
 * a masked pseudo-element, so its colour is a real token (var) and themes with
 * the page — a background-image data-URI can't read CSS vars. pointer-events are
 * off so clicks fall through to the native control underneath. */
.select-box {
    position: relative;
    display: block;
}

.select-box::after {
    content: "";
    position: absolute;
    top: 50%;
    right: var(--select-padding-x);
    width: 16px;
    height: 16px;
    transform: translateY(-50%);
    pointer-events: none;
    background-color: var(--select-chevron);
    -webkit-mask: var(--select-chevron-image) center / contain no-repeat;
    mask: var(--select-chevron-image) center / contain no-repeat;
}

.select-field {
    appearance: none;
    -webkit-appearance: none;
    width: 100%;
    /* Extra right padding clears the chevron. */
    padding: var(--select-padding-y) calc(var(--select-padding-x) * 2 + 16px) var(--select-padding-y) var(--select-padding-x);
    border: 1px solid var(--select-border);
    border-radius: var(--select-border-radius);
    background: var(--select-background-color);
    color: var(--select-color);
    font-size: var(--select-font-size);
    font-family: var(--font-sans);
    line-height: 1.2;
    cursor: pointer;
}

/* The placeholder (disabled first option) reads as muted while it's the value. */
.select-field:has(option[value=""]:checked) {
    color: var(--text-muted);
}

/* Native option list: paint it from tokens so it isn't bright-white on dark UAs
 * that honour these (most do not, and that's fine — the box is the surface we own). */
.select-field option {
    background: var(--select-background-color);
    color: var(--text);
}

/* Invalid affordance — the categorical --danger token (red-700 light / red-400 dark),
 * self-flipping, so the border clears 3:1 on each theme without a per-theme rule. */
.select-field[aria-invalid="true"] {
    border-color: var(--danger);
}

.select-error {
    margin-top: 6px;
    font-size: var(--text-xs);
    color: var(--danger);
}

.select-required {
    margin-left: 2px;
    color: var(--danger);
}

/* Keyboard focus recolors the border as a secondary cue; the global :focus-visible
 * ring (base.css) supplies the primary indicator. :focus-visible so a mouse open
 * doesn't trigger it. */
.select-field:focus-visible {
    border-color: var(--select-focus-border);
}

.select-field:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* @toc Sidebar nav */
/* Sidebar nav — brand (top), nav items (middle, scrolls), footer (bottom: theme + logout). */

.sidebar-nav {
    width: var(--sidebar-nav-width, 240px);
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    background: var(--page-bg);
    border-inline-end: 1px solid var(--line);
}

.sidebar-nav-brand {
    display: block;
    padding: 20px 16px 12px;
    line-height: 0;
    text-decoration: none;
}

.sidebar-nav-brand .brand-logo svg {
    width: 40px;
}

.sidebar-nav-list {
    flex: 1;
    overflow-y: auto;
    margin: 0;
    padding: 4px 10px;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.sidebar-nav-link {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 12px;
    border-radius: 8px;
    color: var(--text-muted);
    font-size: var(--text-sm);
    text-decoration: none;
}

.sidebar-nav-link:hover {
    color: var(--text);
    background: var(--line);
}

.sidebar-nav-link.is-active {
    color: var(--text);
    background: var(--line);
}

.sidebar-nav-icon {
    display: inline-flex;
    width: 20px;
    height: 20px;
    flex-shrink: 0;
}

.sidebar-nav-icon svg {
    display: block;
    width: 100%;
    height: 100%;
}

.sidebar-nav-label {
    flex: 1;
}

/* Panels ↑ / sites ↓ divider — a hairline between the two tiers of the rail. */
.sidebar-nav-divider {
    height: 0;
    margin: 8px 12px;
    border-block-start: 1px solid var(--line);
    list-style: none;
}

/* A SITE row's jump-arrow — pushed to the far right by the flex:1 label (where the theme glyph sits),
 * muted, and it brightens with the row on hover. Decorative (MediaService marks it aria-hidden). */
.sidebar-nav-jump {
    display: inline-flex;
    width: 15px;
    height: 15px;
    flex-shrink: 0;
    color: var(--text-muted);
}

.sidebar-nav-jump svg {
    width: 100%;
    height: 100%;
}

.sidebar-nav-site:hover .sidebar-nav-jump {
    color: var(--text);
}

/* A section is a toggle button styled like a link. */
.sidebar-nav-section {
    width: 100%;
    appearance: none;
    border: none;
    background: transparent;
    font-family: inherit;
    text-align: start;
    cursor: pointer;
}

.sidebar-nav-chevron {
    display: inline-flex;
    width: 15px;
    height: 15px;
    flex-shrink: 0;
    color: var(--text-muted);
    transition: transform 0.15s ease;
}

.sidebar-nav-chevron svg {
    width: 100%;
    height: 100%;
}

.sidebar-nav-item.is-expanded .sidebar-nav-chevron {
    transform: rotate(90deg);
}

.sidebar-nav-sublist {
    margin: 2px 0 6px;
    padding-inline-start: 30px;
    list-style: none;
    display: none;
    flex-direction: column;
    gap: 1px;
}

.sidebar-nav-item.is-expanded .sidebar-nav-sublist {
    display: flex;
}

.sidebar-nav-sublink {
    display: block;
    padding: 6px 10px;
    border-radius: 6px;
    color: var(--text-muted);
    font-size: var(--text-sm);
    text-decoration: none;
}

.sidebar-nav-sublink:hover {
    color: var(--text);
    background: var(--line);
}

.sidebar-nav-sublink.is-active {
    color: var(--text);
    background: var(--line);
}

/* Section header within the submenu (Foundations / Components / Internals). */
.sidebar-nav-subhead {
    margin: 10px 0 2px;
    padding: 0 10px;
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    opacity: 0.75;
}

.sidebar-nav-subhead:first-child {
    margin-top: 0;
}

/* Footer — theme controls, user, and logout. Each is a uniform [icon] [label]
 * row that matches the nav items above: the same 20px .sidebar-nav-icon, the same
 * text size, the same left alignment, so every icon and label lines up. Theme
 * adds its toggle + dropper on the right; logout is its own clickable row.
 * Stays position: relative so the theme-colors popup anchors to the footer. */
.sidebar-nav-footer {
    position: relative;
    margin-top: auto;
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 8px 8px 10px;
    border-top: 1px solid var(--line);
}

.sidebar-nav-footer-row {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    min-height: 40px;
    padding: 0 12px;
    border: none;
    border-radius: 8px;
    background: transparent;
    color: var(--text-muted);
    font-family: inherit;
    font-size: var(--text-sm);
    text-align: start;
    /* The Admin row is an <a> - kill the default underline in every state so it
     * reads like the button rows beside it (no link/visited decoration). */
    text-decoration: none;
}

.sidebar-nav-footer-row .sidebar-nav-label {
    flex: 1;
    min-width: 0;
}

.sidebar-nav-username {
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.sidebar-nav-footer-controls {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex-shrink: 0;
}

.sidebar-nav-footer-form {
    margin: 0;
}

.sidebar-nav-footer-action {
    cursor: pointer;
    transition: color 0.15s ease, background 0.15s ease;
}

.sidebar-nav-footer-action:hover {
    color: var(--text);
    background: var(--line);
}

/* @toc Skeleton */
/*
 * Skeleton — a loading placeholder. A neutral filled shape (text line / block / avatar circle) that pulses
 * softly while content loads. SIZELESS by default — the consumer sets width/height (inline) to mock the
 * incoming layout. The fill is a neutral rung (--line, no ramp), and the pulse is GATED behind
 * prefers-reduced-motion: a visitor who prefers reduced motion sees a STATIC placeholder, never the
 * animation (the house-law win the source pattern lacked).
 */

.skeleton {
    display: block;
    background: var(--skeleton-fill, var(--line));
    border-radius: var(--skeleton-border-radius, var(--radius-control));
    animation: skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}

@keyframes skeleton-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.5; }
}

/* House-law win: NO pulse under reduced motion — a static placeholder, never the animation. */
@media (prefers-reduced-motion: reduce) {
    .skeleton {
        animation: none;
    }
}

/* ---- Block (the default): a rectangle, house 8px container corner. width/height come from the props. ---- */
.skeleton-block {
    width: 100%;
    height: 1rem;
    border-radius: var(--radius-container);
}

/* ---- Avatar: a circle. ---- */
.skeleton-avatar {
    width: 2.5rem;
    height: 2.5rem;
    border-radius: var(--radius-full);
}

/* ---- Text: a stack of pill lines (a paragraph placeholder); the width prop sizes the stack. ---- */
.skeleton-text {
    display: flex;
    flex-direction: column;
    gap: 0.55em;
}

.skeleton-line {
    height: 0.7em;
    width: 100%;
    border-radius: var(--radius-full);
}

/* The last line of a multi-line block is ragged (shorter), like real text. */
.skeleton-line.is-last {
    width: 60%;
}

/* @toc Slider */
/*
 * Slider — themed native <input type="range">. All --slider-* tokens come from
 * config/components/slider.php['css']. We only paint the track + thumb; the
 * browser keeps the drag, keyboard stepping, and value semantics. Track/thumb
 * pseudo-elements must each get their own rule — a combined -webkit-/-moz-
 * selector list is dropped wholesale if the engine doesn't know one half.
 */

.slider {
    margin-bottom: 14px;
}

.slider-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 8px;
}

.slider-label {
    color: var(--slider-label-color);
    font-size: var(--slider-label-font-size);
}

.slider-value {
    color: var(--slider-value-color);
    font-size: var(--slider-value-font-size);
    font-variant-numeric: tabular-nums;
}

/* Reset the UA chrome so the track + thumb rules below are the whole surface. */
.slider-range {
    -webkit-appearance: none;
    appearance: none;
    width: 100%;
    height: var(--slider-thumb-size);
    margin: 0;
    background: transparent;
    cursor: pointer;
}

/* Track — WebKit/Blink. */
.slider-range::-webkit-slider-runnable-track {
    height: var(--slider-track-height);
    border-radius: var(--slider-track-border-radius);
    background: var(--slider-track-background-color);
}

/* Track — Firefox. */
.slider-range::-moz-range-track {
    height: var(--slider-track-height);
    border-radius: var(--slider-track-border-radius);
    background: var(--slider-track-background-color);
}

/* Filled portion before the thumb — Firefox only (WebKit has no native fill). */
.slider-range::-moz-range-progress {
    height: var(--slider-track-height);
    border-radius: var(--slider-track-border-radius);
    background: var(--slider-fill);
}

/* Thumb — WebKit/Blink. margin-top recentres it on the thinner track. */
.slider-range::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: var(--slider-thumb-size);
    height: var(--slider-thumb-size);
    margin-top: calc((var(--slider-track-height) - var(--slider-thumb-size)) / 2);
    border: var(--slider-thumb-border-width) solid var(--slider-thumb-border);
    border-radius: 50%;
    background: var(--slider-thumb-background-color);
    transition: box-shadow 0.15s ease, background 0.15s ease;
}

/* Thumb — Firefox. */
.slider-range::-moz-range-thumb {
    width: var(--slider-thumb-size);
    height: var(--slider-thumb-size);
    border: var(--slider-thumb-border-width) solid var(--slider-thumb-border);
    border-radius: 50%;
    background: var(--slider-thumb-background-color);
    transition: box-shadow 0.15s ease, background 0.15s ease;
}

/* Keyboard focus ring on the thumb — mirrors the button :focus-visible ring.
   The transparent outline is the forced-colors fallback (box-shadow is dropped
   in high-contrast mode, so the outline gets promoted to a system colour). */
.slider-range:focus-visible {
    outline: 2px solid transparent;
    outline-offset: 2px;
}

.slider-range:focus-visible::-webkit-slider-thumb {
    box-shadow: 0 0 0 var(--slider-focus-ring-width) var(--slider-focus-ring);
}

.slider-range:focus-visible::-moz-range-thumb {
    box-shadow: 0 0 0 var(--slider-focus-ring-width) var(--slider-focus-ring);
}

.slider-range:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* @toc Sprite player */
/*
 * Sprite player — the living-preview canvas island. A poster→canvas stage plus a play/pause control. The
 * pixel law holds on both the poster and the canvas (crisp integer scaling; the JS also disables canvas
 * smoothing). Every colour is a token; the play/pause glyph is CSS-drawn (no icon dependency).
 */

.sprite-player {
    display: inline-flex;
    flex-direction: column;
    gap: var(--space-2);
    align-items: flex-start;
}

.sprite-player-stage {
    display: inline-flex;
    padding: var(--space-3);
    background: var(--sprite-player-stage-background-color, var(--surface-muted));
    border: 1px solid var(--sprite-player-stage-border, var(--line));
    border-radius: var(--sprite-player-border-radius, var(--radius-container));
}

/* Poster (pre-upgrade / no-JS) + canvas share the stage; the JS hides the poster and shows the canvas. Both
 * render at natural size × the integer scale, so the stage stays the same size across the swap. */
.sprite-player-poster {
    display: block;
    image-rendering: pixelated;
    zoom: var(--sprite-scale, 4);
}

.sprite-player-canvas {
    display: block;
    image-rendering: pixelated;
}

/* The frame sources are a data/preload source only — never laid out (the [hidden] attr covers it too). */
.sprite-player-frames {
    display: none;
}

.sprite-player-controls {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
}

.sprite-player-label {
    font-size: var(--text-sm);
    color: var(--text-muted);
}

.sprite-player-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: var(--radius-control);
    background: var(--surface);
    color: var(--text);
    cursor: pointer;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.sprite-player-toggle:hover {
    border-color: var(--text-muted);
}

.sprite-player-toggle:focus-visible {
    outline: 2px solid transparent;
    outline-offset: 2px;
    box-shadow: 0 0 0 3px var(--primary);
}

/* CSS-drawn play / pause glyphs. Play = a triangle (clip-path); pause = two bars (a gradient). The control's
 * aria-pressed selects which shows — kept in sync with playback by sprite-player.js. */
.sprite-player-glyph {
    width: 11px;
    height: 11px;
    background: currentColor;
}

.sprite-player-glyph-play {
    clip-path: polygon(0 0, 100% 50%, 0 100%);
}

.sprite-player-glyph-pause {
    display: none;
    background: linear-gradient(to right, currentColor 0 35%, transparent 35% 65%, currentColor 65% 100%);
}

.sprite-player-toggle[aria-pressed="true"] .sprite-player-glyph-play {
    display: none;
}

.sprite-player-toggle[aria-pressed="true"] .sprite-player-glyph-pause {
    display: block;
}

/* @toc Sprite strip */
/*
 * Sprite strip — the animation QA row. A frame-per-cell track (scrolls horizontally on overflow), the pixel
 * law on the frame imgs (crisp nearest-neighbour, integer scale), status chips + anchor marks, and an
 * ONION-SKIN mode that stacks the frames translucently to check their alignment. Every colour is a token.
 */

.sprite-strip {
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.sprite-strip-caption {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    font-size: var(--text-sm);
    color: var(--text);
}

.sprite-strip-label {
    font-weight: 600;
}

.sprite-strip-count {
    color: var(--text-muted);
    font-size: var(--text-xs);
}

/* The frame track — a row that scrolls horizontally when the frames overflow. */
.sprite-strip-track {
    display: flex;
    gap: var(--sprite-strip-gap, var(--space-3));
    overflow-x: auto;
    padding-block-end: var(--space-1);
    align-items: flex-start;
}

.sprite-strip-frame {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    flex: 0 0 auto;
}

.sprite-strip-cell {
    display: inline-flex;
    padding: var(--space-2);
    background: var(--sprite-strip-cell-background-color, var(--surface-muted));
    border: 1px solid var(--sprite-strip-cell-border, var(--line));
    border-radius: var(--sprite-strip-border-radius, var(--radius-control));
}

/* The pixel law: crisp nearest-neighbour scaling. `zoom` (not transform) scales the LAYOUT box too, so an
 * integer-scaled frame reserves its own space — the honest way to enlarge pixel art without knowing its
 * natural size in CSS. Integer factor only (the prop is an int). */
.sprite-strip-img {
    display: block;
    image-rendering: pixelated;
    zoom: var(--sprite-scale, 2);
}

.sprite-strip-meta {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.sprite-strip-ord {
    font-variant-numeric: tabular-nums;
}

.sprite-strip-anchor {
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: 0.7rem;
    font-weight: 600;
    color: var(--text);
}

.sprite-strip-controls {
    display: flex;
}

.sprite-strip-onion {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: 5px 12px;
    border: 1px solid var(--line);
    border-radius: var(--radius-control);
    background: var(--surface);
    color: var(--text);
    font-size: var(--text-sm);
    cursor: pointer;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.sprite-strip-onion:hover {
    border-color: var(--text-muted);
}

.sprite-strip-onion[aria-pressed="true"] {
    border-color: var(--primary);
    box-shadow: inset 0 0 0 1px var(--primary);
}

.sprite-strip-onion:focus-visible {
    outline: 2px solid transparent;
    outline-offset: 2px;
    box-shadow: 0 0 0 3px var(--primary);
}

/* ---- Onion-skin mode: stack every frame in one grid cell, translucent, anchor kept opaque ---- */
.sprite-strip.is-onion .sprite-strip-track {
    display: grid;
    justify-items: center;
    overflow: visible;
}

.sprite-strip.is-onion .sprite-strip-frame {
    grid-area: 1 / 1;   /* all frames occupy the same cell → they stack */
}

.sprite-strip.is-onion .sprite-strip-img {
    opacity: 0.35;
}

.sprite-strip.is-onion .sprite-strip-frame.is-anchor .sprite-strip-img {
    opacity: 1;
}

.sprite-strip.is-onion .sprite-strip-meta {
    display: none;
}

/* @toc Study issue */
/* Study issue — the bilingual checklist card (BarBruiser answer key). A left-accent by
 * type (must = success, nice = neutral, trap = danger), then a two-column EN | RU grid
 * (stacks on mobile). Surface + text are theme tokens; the type/lang accents are the
 * app's status tokens. View: study-issue.blade.php. */

.study-issue {
    container-type: inline-size;
    margin: 9px 0;
    padding: 11px 14px;
    border-inline-start: 4px solid var(--study-issue-accent, var(--success));
    border-radius: var(--radius);
    background: var(--surface-raised);
}

.study-issue-must { --study-issue-accent: var(--success); }
.study-issue-nice { --study-issue-accent: var(--text-muted); }
.study-issue-trap { --study-issue-accent: var(--danger); }

.study-issue-head {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 8px;
}

.study-issue-mark {
    display: inline-flex;
    align-items: center;
    width: 18px;
    height: 18px;
    flex-shrink: 0;
    color: var(--study-issue-accent);
}

.study-issue-mark svg {
    width: 100%;
    height: 100%;
}

.study-issue-title {
    font-weight: 700;
    font-size: var(--text-sm);
    color: var(--text);
}

.study-issue-bilingual {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
}

.study-issue-lang {
    background: var(--surface);
    border-inline-start: 3px solid var(--study-lang-accent, var(--line));
    border-radius: var(--radius-sm);
    padding: 9px 11px;
    font-size: var(--text-sm);
}

.study-issue-en { --study-lang-accent: var(--success); }
.study-issue-ru { --study-lang-accent: var(--warning); }

.study-issue-lang p {
    margin: 6px 0;
    line-height: 1.5;
}

.study-issue-lang p:first-of-type { margin-top: 0; }
.study-issue-lang p:last-of-type { margin-bottom: 0; }

.study-issue-lang strong {
    color: var(--text);
}

.study-issue-langlabel {
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    font-weight: 700;
    color: var(--text-muted);
    margin-bottom: 5px;
}

/* 720px: the card's OWN inline size, via a container query — NOT the app --bp-* viewport seam (with which it
   coexists). The EN | RU split collapses to one column whenever the card itself is narrow (e.g. dropped into a
   disclosure or a narrow slot), independent of the viewport width. .study-issue sets container-type above. */
@container (max-width: 720px) {
    .study-issue-bilingual {
        grid-template-columns: 1fr;
    }
}

/* @toc Study reader */
/*
 * Study reader — the §4b two-pane teaching shell. Stage (the stimulus) and rail (the study aside) are
 * SIMULTANEOUS ≥ the drawer breakpoint — the simultaneity is the teaching mechanic. Below it, the rail
 * collapses to a non-modal off-canvas drawer toggled by the peek button, gated by a @CONTAINER query (the
 * component-internal responsive seam, per the modernity contract — not a viewport media query). Layout dims
 * are --study-reader-* config (TBD-direction; only the 860px breakpoint is LOCKED). The reader root is the
 * query container; data-phase (set by the phase control) drives per-phase visibility in CSS elsewhere.
 */

.study-reader {
    container-type: inline-size;
    container-name: study-reader;
    display: flex;
    flex-direction: column;
    gap: var(--study-reader-gutter, 24px);
    color: var(--text);
}

/* ---- Header ---- */
.study-reader-head {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.study-reader-title {
    margin: 0;
    font-size: var(--text-xl);
}

.study-reader-meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-block-start: var(--space-2);
}

/* Honesty band (minimal stopgap for x-provenance-band) — a quiet labelled strip, never hidden. */
.study-reader-provenance {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-2);
    margin: 0;
    padding: var(--space-2) var(--space-3);
    border-inline-start: 3px solid var(--line-strong);
    background: var(--surface-muted);
    color: var(--text-muted);
    font-size: var(--text-sm);
}

.study-reader-provenance-level {
    font-weight: 600;
    color: var(--text);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: 0.75em;
}

/* ---- Layout: stage | rail, simultaneous by default ---- */
.study-reader-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) var(--study-reader-rail-width, 360px);
    gap: var(--study-reader-gutter, 24px);
    align-items: start;
}

.study-reader-stage {
    max-width: var(--study-reader-stage-measure, 68ch);
}

.study-reader-rail {
    min-width: 0;
}

/* The peek button is drawer chrome — hidden while stage + rail are simultaneous. */
.study-reader-peek {
    display: none;
    align-items: center;
    gap: var(--space-2);
    align-self: flex-start;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--line);
    border-radius: var(--space-2);
    background: var(--surface);
    color: var(--text);
    font: inherit;
    cursor: pointer;
}

.study-reader-peek-icon {
    display: inline-flex;
    width: 1.25em;
    height: 1.25em;
}

/* ---- Drawer: below the LOCKED breakpoint, the rail becomes a non-modal off-canvas panel ---- */
@container study-reader (max-width: 860px) {
    .study-reader-layout {
        grid-template-columns: minmax(0, 1fr);
    }

    .study-reader-peek {
        display: inline-flex;
    }

    .study-reader-rail {
        position: fixed;
        inset-block: 0;
        inset-inline-end: 0;
        z-index: var(--z-popover);
        width: min(360px, 85vw);
        padding: var(--space-4);
        overflow-y: auto;
        background: var(--surface);
        border-inline-start: 1px solid var(--line);
        box-shadow: -8px 0 24px color-mix(in srgb, var(--neutral-950) 20%, transparent);
        transform: translateX(100%);
        /* Closed = off the tab order + AT tree (visibility), not just off-screen — so a keyboard/SR user can't
           Tab into the invisible rail. visibility is delayed until the slide-out finishes so it still animates. */
        visibility: hidden;
        transition: transform 0.2s ease, visibility 0s linear 0.2s;
    }

    .study-reader.is-drawer-open .study-reader-rail {
        transform: translateX(0);
        visibility: visible;
        transition: transform 0.2s ease, visibility 0s linear 0s;
    }
}

/* ---- Flash: the shell's legend:activate feedback on the matching spans (transient, in the mark's hue) ---- */
.study-reader .annotated-text-mark.is-flash {
    border-radius: var(--annotated-text-span-border-radius, 3px);
    animation: study-reader-flash 1.2s ease-out;
}

@keyframes study-reader-flash {
    0% {
        box-shadow: 0 0 0 3px var(--_hue, currentColor);
    }

    100% {
        box-shadow: 0 0 0 3px transparent;
    }
}

/* Reduced motion: no drawer slide, no flash (the shell also skips the flash class — belt and braces). */
@media (prefers-reduced-motion: reduce) {
    .study-reader-rail {
        transition: none;
    }

    .study-reader .annotated-text-mark.is-flash {
        animation: none;
    }
}

/* @toc Study sheet */
/* Study sheet — the BarBruiser bar-essay renderer. Head banner, trigger legend, the
 * fact pattern with colour-coded trigger spans, and the collapsible checklist. The
 * trigger hues (t1-t6) map to the app's status tokens (danger/success/info/warning,
 * each self-flipping via its own -surface tint) plus the primary/neutral ramp for the
 * two hues with no status equivalent (t4/t6 — an explicit [data-theme="dark"] override
 * owns their flip). Boxes (source/instructions/call/hook) are x-callout; cards are
 * x-study-issue. View: study-sheet.blade.php. */

.study-sheet {
    max-width: var(--max-w-3xl);
    margin: 0 auto;
}

/* HEAD */
.study-sheet-head {
    background: var(--surface-raised);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    padding: 16px 20px;
    margin-bottom: 16px;
}

.study-sheet-sub {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--text-sm);
}

/* LEGEND */
.study-legend {
    background: var(--surface-raised);
    border-radius: var(--radius);
    padding: 12px 16px;
    margin: 14px 0;
}

.study-legend-title {
    margin: 0 0 8px;
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.4px;
    color: var(--text-muted);
}

.study-legend-list {
    margin: 0;
    padding-left: 18px;
    font-size: var(--text-sm);
}

.study-legend-list li {
    margin-bottom: 5px;
}

/* A solid colour chip (its own classes, not the trigger-* tint, so the theme
 * override doesn't fade it). The hue matches the matching fact-pattern highlight. */
.study-swatch {
    display: inline-block;
    width: 26px;
    height: 12px;
    border-radius: 3px;
    margin-right: 8px;
    vertical-align: middle;
}

.study-swatch-t1 { background: var(--danger); }
.study-swatch-t2 { background: var(--success); }
.study-swatch-t3 { background: var(--info); }
.study-swatch-t4 { background: var(--primary-500); }
.study-swatch-t5 { background: var(--warning); }
.study-swatch-t6 { background: var(--neutral-500); }

/* FACT PATTERN */
.study-fact {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 16px 20px;
    margin: 14px 0;
    font-size: var(--text-base);
    line-height: 1.65;
}

.study-fact-label {
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    font-weight: 700;
    color: var(--text-muted);
    margin-bottom: 6px;
}

.study-fact p {
    margin: 8px 0;
}

/* The inline trigger highlight + the legend swatch share the .trigger-* colours. */
.trigger {
    padding: 1px 4px;
    border-radius: 3px;
    font-weight: 500;
}

.trigger-t1 { background: var(--danger-surface); border-bottom: 2px solid var(--danger); }
.trigger-t2 { background: var(--success-surface); border-bottom: 2px solid var(--success); }
.trigger-t3 { background: var(--info-surface); border-bottom: 2px solid var(--info); }
.trigger-t4 { background: var(--primary-100); border-bottom: 2px solid var(--primary-500); }
.trigger-t5 { background: var(--warning-surface); border-bottom: 2px solid var(--warning); }
.trigger-t6 { background: var(--neutral-100); border-bottom: 2px solid var(--neutral-500); }

/* t1/t2/t3/t5 already self-flip via their -surface tokens above; only t4/t6 (the two
 * hues with no status equivalent, on the raw primary/neutral ramp) need an owned flip. */
:root[data-theme="dark"] .trigger-t4 { background: var(--primary-950); }
:root[data-theme="dark"] .trigger-t6 { background: var(--neutral-700); }

/* CALL */
.study-call-list {
    margin: 8px 0 0 20px;
    padding: 0;
}

.study-call-list > li {
    margin-bottom: 6px;
}

.study-call-ru {
    margin-top: 8px;
    color: var(--text-muted);
    font-size: var(--text-sm);
}

/* CHECKLIST — now the x-details primitive (themed disclosure); no bespoke styles. */

/* @toc Swatch */
/*
 * x-swatch — a colour chip that copies its token (#761, promoted from the docs Palette page's raw <button>s).
 *
 * ZERO COLOUR LITERALS, deliberately. The FILL is the one arbitrary colour and it arrives through the
 * --swatch-color custom property set by the consumer (the swatch-picker exception, exactly as theme-panel takes
 * its fill). Everything else is a semantic token — and that is only possible because the rung label sits BESIDE
 * the fill rather than on it. The raw markup this replaces put its step number over the colour, which forced a
 * hard-coded white plus a text-shadow to stay legible on an arbitrary background; theme-panel.css had already
 * recorded that a mark legible over an arbitrary palette colour is not achievable from tokens. Moving the label
 * onto the page surface keeps the number readable (a palette is for SCANNING) with ordinary --text-muted.
 */

.swatch {
    position: relative;   /* the copied confirmation is positioned against the control */
    display: flex;
    flex: 1;
    flex-direction: column;
    gap: 2px;
    min-inline-size: 0;
    padding: 0;
    border: none;
    background: none;
    cursor: pointer;
}

/* The colour itself. The hover lift is on the FILL, not the whole control, so the rung label stays put and
   legible while the chip grows. */
.swatch-fill {
    display: block;
    block-size: var(--swatch-height, 44px);
    border-radius: var(--swatch-border-radius, 8px);
    background: var(--swatch-color);
    transition: transform 120ms ease, box-shadow 120ms ease;
}

.swatch:hover .swatch-fill {
    transform: scale(var(--swatch-hover-scale, 1.12));
    box-shadow: var(--shadow-lg);
}

.swatch:hover {
    z-index: 2;
}

/* The rung, on the page surface — the whole reason the fill carries no text. */
.swatch-step {
    font-size: var(--text-2xs);
    color: var(--text-muted);
    text-align: center;
    font-variant-numeric: tabular-nums;
}

/* State is a RING, never a colour change — the fill is the content, so it can never also be the state cue. */
.swatch:focus-visible {
    outline: var(--swatch-ring-width, 2px) solid var(--primary);
    outline-offset: 2px;
    border-radius: var(--swatch-border-radius, 8px);
    z-index: 2;
}

/*
 * The copied confirmation, also on the page surface. Hidden until swatch.js writes into it; :empty keeps it out
 * of the layout so a row of chips never shifts when one is used.
 */
.swatch-feedback {
    position: absolute;
    inset-block-start: calc(100% + 4px);
    inset-inline: 50% auto;
    translate: -50% 0;
    padding: 2px 6px;
    border-radius: var(--swatch-border-radius, 8px);
    background: var(--surface);
    border: 1px solid var(--line);
    color: var(--text-muted);
    font-size: var(--text-2xs);
    white-space: nowrap;
    pointer-events: none;
    z-index: 3;
}

.swatch-feedback:empty {
    display: none;
}

@media (max-width: 768px) {
    .swatch-fill {
        block-size: 34px;
    }
}

/* @toc Switch */
/*
 * Switch. A native <input type="checkbox" role="switch"> drives a decorative
 * track + knob: the real input is visually hidden (it keeps behaviour, focus and
 * keyboard), and its :checked / :focus-visible state styles the sibling pill. All
 * --switch-* tokens come from config/components/switch.php['css']. Dark-first.
 */

.switch {
    display: inline-flex;
    align-items: center;
    gap: var(--switch-gap, 10px);
    cursor: pointer;
    font-size: var(--switch-label-font-size, var(--text-sm));
    color: var(--switch-label-color, var(--text));
    line-height: 1.3;
    -webkit-user-select: none;
    user-select: none;
}

/* The real control: present for behaviour + a11y, visually replaced by the pill.
 * Not display:none — that would drop it from the tab order and kill keyboard. */
.switch-input {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: 0;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
}

.switch-track {
    position: relative;
    display: inline-block;
    flex: none;
    width: var(--switch-width, 40px);
    height: var(--switch-height, 22px);
    border: 1px solid var(--switch-track-border, var(--line));
    border-radius: var(--radius-full);
    background: var(--switch-track-background-color, var(--surface-raised));
    transition: background 0.18s ease, border-color 0.18s ease;
}

.switch-knob {
    position: absolute;
    top: 50%;
    left: var(--switch-knob-gap, 2px);
    width: var(--switch-knob-size, 16px);
    height: var(--switch-knob-size, 16px);
    border-radius: var(--radius-full);
    background: var(--switch-knob-background-color, var(--text-muted));
    transform: translateY(-50%);
    transition: transform 0.18s ease, background 0.18s ease;
}

/* Checked: tint the track and slide the knob to the right edge. The travel is the
 * track width minus the knob and both gaps, so it never clips the rounded end. */
.switch-input:checked + .switch-track {
    background: var(--switch-track-background-color-on, var(--primary));
    border-color: var(--switch-track-background-color-on, var(--primary));
}

.switch-input:checked + .switch-track .switch-knob {
    background: var(--switch-knob-background-color-on, var(--on-primary));
    transform: translateY(-50%) translateX(calc(var(--switch-width, 40px) - var(--switch-knob-size, 16px) - (var(--switch-knob-gap, 2px) * 2) - 2px));
}

/* Keyboard focus ring on the pill, mirroring the other primitives' :focus-visible.
 * The transparent outline is the forced-colors fallback (box-shadow is dropped in
 * high-contrast mode, so the outline gets promoted and focus stays visible). */
.switch-input:focus-visible + .switch-track {
    outline: 2px solid transparent;
    outline-offset: 2px;
    box-shadow: 0 0 0 3px var(--switch-focus-ring, var(--primary));
}

.switch.is-disabled {
    cursor: not-allowed;
    opacity: 0.5;
}

/* Submit-on-change fallback button (rendered only with `submit`; switch.js hides it and auto-submits on
 * change). It's a composed <x-button> — all styling is the button primitive's; this only positions it. */
.switch-apply {
    margin-left: 10px;
}

/* @toc Table */
/* Table — generic data table. Themeable via --table-* (config), falling back to
 * the global theme tokens. Signed/numeric cells use .is-positive / .is-negative. */

.table-wrap {
    overflow-x: auto;
}

.table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--text-sm);
}

/* The label TREATMENT (size/weight/case/tracking/colour) now comes from the shared .ui-label recipe
   (R5-B) applied in table.blade.php — revert-to-eyebrow is that one rule, base.css. .table th keeps only
   its STRUCTURAL styles here (never re-declares a label property, or it would defeat the one-edit revert). */
.table th {
    padding: 10px 12px;
    border-bottom: 1px solid var(--table-border, var(--line));
    text-align: start;
    white-space: nowrap;
}

.table td {
    padding: 10px 12px;
    border-bottom: 1px solid var(--table-border, var(--line));
    color: var(--text);
}

.table tbody tr:hover {
    background: var(--table-row-hover, var(--surface-raised));
}

.table-right {
    text-align: end;
}

.table-sort {
    color: inherit;
    text-decoration: none;
}

.table-sort:hover {
    color: var(--text);
}

.table th.is-sorted-asc .table-sort::after {
    content: " \2191";
}

.table th.is-sorted-desc .table-sort::after {
    content: " \2193";
}

.table .is-positive {
    color: var(--table-positive, var(--success));
}

.table .is-negative {
    color: var(--table-negative, var(--danger));
}

.table-empty {
    padding: 40px 16px;
    text-align: center;
    color: var(--text-muted);
}

/* --- polish (adapted from leadthink): zebra, scrollbar, and the Cell formatters --- */

/* Horizontal scroll on mobile; the scrollbar itself is the global themed one (base.css). */
.table-wrap {
    -webkit-overflow-scrolling: touch;
}

/* Zebra + hover, theme-agnostic (a tint of the current text colour works on light or dark). The
   hover rule is last so it wins over the stripe. */
.table tbody tr:nth-child(2n) {
    background: color-mix(in srgb, currentColor 3%, transparent);
}

.table tbody tr:hover {
    background: color-mix(in srgb, currentColor 7%, transparent);
}

/* Cell formatters (App\Support\Cell): numeric alignment, negatives, badges, stacked lines. */
.cell-num {
    font-variant-numeric: tabular-nums;
}

.cell-negative {
    color: var(--table-negative, var(--danger));
    font-weight: 600;
}

.cell-stacked {
    display: flex;
    flex-direction: column;
    line-height: 1.25;
}

.cell-stacked > div + div {
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.badge {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: var(--text-xs);
    font-weight: 600;
}

.badge-success {
    background: var(--success-surface);
    color: var(--success);
}

.badge-secondary {
    background: var(--neutral-100);
    color: var(--text-muted);
}

/* @toc Tabs */
/* Tabs — a horizontal tab list with a sliding indicator. tabs.js measures the
 * active trigger and sets the indicator's transform + width; CSS transitions them
 * and styles them per variant (set on the root):
 *   .tabs-underline (default) - a thin bar slides along a bottom rule; triggers
 *                               get a faint rounded hover highlight.
 *   .tabs-pill                - a segmented control; a filled pill slides behind
 *                               the active trigger.
 * Triggers take an optional icon (recolours with the trigger; accent when active).
 * States: active / inactive (hover) / .is-empty (dimmed). Colours: --tabs-*
 * (config). View: tabs.blade.php; switcher + indicator: tabs.js. */

.tabs-nav {
    position: relative;
    display: flex;
    gap: 2px;
    /* Scroll the tab row when it overflows (e.g. 6 facet tabs on mobile) rather
     * than wrapping or squashing; the scrollbar is hidden. */
    overflow-x: auto;
    scrollbar-width: none;
}

.tabs-nav::-webkit-scrollbar {
    display: none;
}

.tabs-tab {
    position: relative;
    z-index: 1;
    display: inline-flex;
    align-items: center;
    gap: 7px;
    flex-shrink: 0;
    appearance: none;
    border: none;
    border-radius: var(--radius-control);   /* R1: a tab trigger is a CONTROL → 6px (was the off-scale 7px) */
    background: transparent;
    color: var(--tabs-tab-text);
    font-family: inherit;
    font-size: var(--text-sm);
    font-weight: 500;
    padding: 8px 12px;
    white-space: nowrap;
    cursor: pointer;
    transition: color 0.15s ease, background 0.15s ease;
}

.tabs-tab:hover:not(.is-active):not(.is-empty) {
    color: var(--tabs-tab-text-hover);
}

.tabs-tab.is-active {
    color: var(--tabs-tab-text-active);
}

.tabs-tab.is-empty {
    opacity: 0.4;
    cursor: default;
}

.tabs-tab.is-empty.is-active {
    opacity: 1;
}

/* Icon — inherits the trigger colour (currentColor), turns accent when active. */
.tabs-tab-icon {
    display: inline-flex;
    width: 16px;
    height: 16px;
    flex-shrink: 0;
    transition: color 0.15s ease;
}

.tabs-tab-icon svg {
    display: block;
    width: 100%;
    height: 100%;
}

.tabs-tab.is-active .tabs-tab-icon {
    color: var(--tabs-accent);
}

/* The sliding indicator (one per nav). tabs.js sets width + translateX; the
 * variant rules below give it its position + look. */
.tabs-indicator {
    position: absolute;
    left: 0;
    width: 0;
    transform: translateX(0);
    transition: transform 0.2s ease, width 0.2s ease;
    pointer-events: none;
}

/* underline variant */
.tabs-underline > .tabs-nav {
    border-bottom: 1px solid var(--tabs-line);
}

.tabs-underline > .tabs-nav > .tabs-tab:hover:not(.is-active):not(.is-empty) {
    background: var(--tabs-tab-hover-background-color);
}

.tabs-underline > .tabs-nav > .tabs-indicator {
    /* bottom: 0 (not -1px) so the horizontal-scroll overflow doesn't clip it. */
    bottom: 0;
    height: 2px;
    background: var(--tabs-accent);
    border-radius: 2px;
}

/* pill variant */
.tabs-pill > .tabs-nav {
    gap: 0;
    padding: 4px;
    border-radius: var(--radius-lg);
    background: var(--tabs-pill-track);
}

.tabs-pill > .tabs-nav > .tabs-tab {
    padding: 7px 14px;
}

.tabs-pill > .tabs-nav > .tabs-indicator {
    top: 4px;
    bottom: 4px;
    background: var(--tabs-pill-active);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
}

.tabs-body {
    padding-top: 18px;
}

/* ── VERTICAL (#1349): the strip runs down the side, the panel sits BESIDE it ───────────────────
 * Added because a tab count can come from DATA. A horizontal strip is right for the three or four tabs this
 * component was built for; the PT reader's source list is 4–13 documents from the record in a half-width panel,
 * where horizontal either scrolls (hiding tabs behind the deliberately-invisible scrollbar) or wraps to three
 * rows — and a 135px-tall strip pushes its panel content that far below the sibling panel it is supposed to sit
 * level with. Vertical holds its shape at any count: the strip grows DOWN, which is the axis with room.
 *
 * `aria-orientation` + Up/Down keys come with it (tabs.blade.php + tabs.js key off the same value), so this is
 * an APG-shaped option rather than a skin.
 */
.tabs-vertical {
    display: grid;
    /* The strip takes what its labels need up to a cap, then wraps text rather than starving the panel. */
    grid-template-columns: minmax(0, var(--tabs-vertical-strip, 13rem)) minmax(0, 1fr);
    gap: var(--tabs-vertical-gap, 18px);
    align-items: start;
}

.tabs-vertical > .tabs-nav {
    flex-direction: column;
    /* The horizontal default scrolls sideways and hides its scrollbar; down this axis the strip simply grows,
       so the scroller has to be undone or it clips the column. */
    overflow-x: visible;
}

/* Left-aligned, full-width triggers: a vertical strip reads as a list, and centred labels in a column look
   like a mistake. `inline-flex` from the base rule would shrink-wrap each one. */
.tabs-vertical > .tabs-nav > .tabs-tab {
    display: flex;
    justify-content: flex-start;
    text-align: start;
    /* A long document title wraps instead of being clipped — the whole point of the cap above. */
    white-space: normal;
}

/* The panel is a grid sibling now, so the top padding that separated it from a strip ABOVE would only push it
   out of alignment with the strip BESIDE it — which is the bug this variant exists to fix. */
.tabs-vertical > .tabs-body {
    padding-top: 0;
}

/* The strip's rule moves to its inner edge — a divider between the list and the panel, where the horizontal
   variant puts it under the tabs. */
.tabs-vertical.tabs-underline > .tabs-nav {
    border-bottom: 0;
    border-inline-end: 1px solid var(--tabs-line);
}

/* The indicator becomes a marker down the LEADING edge of the active tab, not the divider edge: it marks the
   ROW you are on, which is how a vertical list reads, and it stays clear of the border it would otherwise sit
   on top of. `inset-inline-start` rather than the base rule's physical `left`, so it flips under RTL — and it
   has to be stated, because leaving both a start and an end inset set is over-constrained and the physical
   `left` silently wins (which is how this first rendered on the wrong edge from what the CSS claimed).
   tabs.js drives height + translateY, so the transition needs `height` too — the base only animates width. */
.tabs-vertical.tabs-underline > .tabs-nav > .tabs-indicator {
    bottom: auto;
    inset-block-start: 0;
    inset-inline-start: 0;
    inline-size: 2px;
    height: auto;
    border-radius: 2px;
    transition: transform 0.2s ease, height 0.2s ease;
}

.tabs-panel {
    display: none;
}

.tabs-panel.is-active {
    display: block;
}

/* @toc Textarea */
/* Textarea. All --textarea-* tokens come from config/components/textarea.php['css']. */

.textarea {
    margin-bottom: 14px;
}

.textarea-label {
    display: block;
    margin-bottom: 6px;
    color: var(--textarea-label-color);
    font-size: var(--textarea-label-font-size);
}

.textarea-field {
    width: 100%;
    padding: var(--textarea-padding-y) var(--textarea-padding-x);
    border: 1px solid var(--textarea-border);
    border-radius: var(--textarea-border-radius);
    background: var(--textarea-background-color);
    color: var(--textarea-color);
    font-size: var(--textarea-font-size);
    font-family: var(--font-sans);
    line-height: var(--textarea-line-height);
    /* Vertical-only drag handle: width stays on the layout grid, height grows with content. */
    resize: var(--textarea-resize);
    min-height: var(--textarea-min-height);
}

/* opacity:1 stops the UA re-fading the placeholder below the token's contrast. */
.textarea-field::placeholder {
    color: var(--text-muted);
    opacity: 1;
}

/* Code variant — a monospace face + tighter type for pasting JSON, config, or code. */
.textarea-field-code {
    font-family: var(--font-mono);
    font-size: var(--text-xs);
}

/* Invalid / hint / required affordances. Error colour is the categorical --danger
 * token (red-700 light / red-400 dark) — self-flipping and menu-hover-certified, so
 * the text clears 4.5:1 and the border 3:1 on every surface without a per-theme rule. */
.textarea-field[aria-invalid="true"] {
    border-color: var(--danger);
}

.textarea-error {
    margin-top: 6px;
    font-size: var(--text-xs);
    color: var(--danger);
}

.textarea-hint {
    margin-top: 6px;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.textarea-required {
    margin-left: 2px;
    color: var(--danger);
}

/* Keyboard focus recolors the border as a secondary cue; the global :focus-visible
 * ring (base.css) supplies the primary indicator. :focus-visible (not :focus) so a
 * mouse click doesn't trigger either. */
.textarea-field:focus-visible {
    border-color: var(--textarea-focus-border);
}

/* @toc Theme colors */
/* Accent/surface colour picker — a dropper button with a popover of swatch
 * grids, sits in the sidebar footer beside the light/dark theme-picker.
 *
 * The swatch fill (background: var(--swatch-color)) is the load-bearing line:
 * theme-colors.js sets --swatch-color to var(--palette-<name>-500) per swatch,
 * pulling from the catalog ramps CssService emits.
 *
 * The popup is absolutely positioned and needs a wider positioned ancestor than
 * the 34px dropper, so it anchors to .sidebar-nav-footer (which is position:
 * relative). Used by /resources/views/components/theme-colors.blade.php.
 */

.theme-colors {
    display: inline-flex;
}

.theme-colors-dropper {
    appearance: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
    transition: color 0.15s ease, background 0.15s ease;
}

.theme-colors-dropper:hover {
    color: var(--text);
    background: var(--line);
}

.theme-colors-dropper svg {
    width: 17px;
    height: 17px;
}

.theme-colors-popup {
    position: absolute;
    left: 0;
    right: 0;
    bottom: calc(100% + 8px);
    z-index: 20;
    display: flex;
    flex-direction: column;
    gap: 12px;
    padding: 12px;
    border: 1px solid var(--line);
    border-radius: 10px;
    background: var(--page-bg);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
    transform: scale(0.96);
    transform-origin: bottom center;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: transform 160ms ease, opacity 160ms ease, visibility 0s linear 160ms;
}

.theme-colors-popup.is-open {
    transform: scale(1);
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition: transform 160ms ease, opacity 160ms ease, visibility 0s;
}

.theme-colors-group {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.theme-colors-label {
    color: var(--text-muted);
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.theme-colors-grid {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: 6px;
}

.theme-colors-swatch {
    appearance: none;
    width: 100%;
    aspect-ratio: 1 / 1;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: 5px;
    background: var(--swatch-color);
    cursor: pointer;
    outline: 2px solid transparent;
    outline-offset: 1px;
    transition: transform 120ms ease, outline-color 120ms ease;
}

.theme-colors-swatch:hover {
    transform: scale(1.08);
}

.theme-colors-swatch.is-active {
    outline-color: var(--text);
}

/* @toc Theme panel */
/*
 * Theme panel — the blueprint schedule inside the x-drawer. Hairline-ruled sections with .ui-label heads,
 * compact rhythm, ring-not-border selected states, every colour from a token (the one exception: the colour
 * SWATCH fills are raw --palette-* values — the sanctioned swatch-picker exception to the colour law). No
 * shadow-heavy cards, no left-stroke accents (the anti-slop law — this panel is a taste showcase).
 *
 * This file ALSO carries the data-radius axis (the :root[data-radius] blocks at the end) — a global static
 * axis, bundled with the component that drives it, so utilities.css stays untouched.
 */

/* ---- The schedule: hairline-ruled sections ---- */
.theme-panel-section {
    padding: var(--space-4) 20px;
    border-block-end: 1px solid var(--line);
}

.theme-panel-section:last-child {
    border-block-end: none;
}

.theme-panel-head {
    margin: 0 0 var(--space-3);
}

/* ---- 1 · Theme list ---- */
.theme-panel-themes {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}

.theme-panel-theme {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: 7px 12px;
    border: 1px solid var(--line);
    border-radius: var(--radius-control);
    background: var(--surface);
    color: var(--text);
    font-size: var(--text-sm);
    cursor: pointer;
    transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
}

.theme-panel-theme:hover {
    border-color: var(--text-muted);
}

.theme-panel-theme[aria-pressed="true"] {
    border-color: var(--primary);
    box-shadow: inset 0 0 0 1px var(--primary);
}

.theme-panel-theme-icon {
    display: inline-flex;
    line-height: 0;
    color: var(--text-muted);
}

.theme-panel-theme-icon svg {
    width: 16px;
    height: 16px;
}

.theme-panel-theme[aria-pressed="true"] .theme-panel-theme-icon {
    color: var(--primary);
}

.theme-panel-theme:focus-visible {
    outline: 2px solid transparent;
    outline-offset: 2px;
    box-shadow: 0 0 0 3px var(--primary);
}

/* ---- 2 + 3 · Swatch grids (built by theme-panel.js) ---- */
.theme-panel-swatches {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}

.theme-panel-swatch {
    position: relative;
    width: var(--theme-panel-swatch-size, 26px);
    height: var(--theme-panel-swatch-size, 26px);
    padding: 0;
    border: 1px solid var(--line);
    border-radius: var(--radius-control);
    /* The raw palette fill — the swatch-picker exception (a --palette-<family>-500 catalog value). */
    background: var(--swatch-color);
    cursor: pointer;
    transition: box-shadow 0.15s ease, transform 0.1s ease;
}

.theme-panel-swatch:hover {
    transform: scale(1.08);
}

/* Selected = a RING offset by the surface then the primary token (a shape cue, not the fill colour alone) +
 * aria-pressed. A check GLYPH over the swatch was considered, but a check legible on an arbitrary --palette
 * fill needs a fixed light colour, which the colour law forbids without a Planner-baselined literal — the
 * ring is the honest, on-token non-colour cue (a corner check badge is a demand-driven refinement). */
.theme-panel-swatch[aria-pressed="true"] {
    box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--primary);
}

.theme-panel-swatch:focus-visible {
    outline: 2px solid transparent;
    outline-offset: 2px;
    box-shadow: 0 0 0 3px var(--primary);
}

/* ---- 4 · Radius segmented row ---- */
.theme-panel-radius {
    display: flex;
    gap: var(--space-2);
    flex-wrap: wrap;
}

.theme-panel-radius-opt {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    padding: 8px 10px;
    border: 1px solid var(--line);
    border-radius: var(--radius-control);
    background: var(--surface);
    color: var(--text-muted);
    font-size: var(--text-xs);
    cursor: pointer;
    transition: border-color 0.15s ease, box-shadow 0.15s ease, color 0.15s ease;
}

.theme-panel-radius-opt:hover {
    border-color: var(--text-muted);
}

.theme-panel-radius-opt[aria-pressed="true"] {
    border-color: var(--primary);
    color: var(--text);
    box-shadow: inset 0 0 0 1px var(--primary);
}

/* The corner-preview glyph — a small square whose top-left corner shows the preset's rounding. */
.theme-panel-radius-glyph {
    width: 20px;
    height: 20px;
    border: 2px solid currentColor;
    border-radius: 0;
    /* Only the top-left corner is rounded, so the preset reads at a glance. */
    border-block-end-color: transparent;
    border-inline-end-color: transparent;
}

.theme-panel-radius-glyph-none { border-start-start-radius: 0; }
.theme-panel-radius-glyph-small { border-start-start-radius: 3px; }
.theme-panel-radius-glyph-default { border-start-start-radius: 6px; }
.theme-panel-radius-glyph-large { border-start-start-radius: 10px; }

.theme-panel-radius-opt[aria-pressed="true"] .theme-panel-radius-glyph {
    color: var(--primary);
}

.theme-panel-radius-opt:focus-visible {
    outline: 2px solid transparent;
    outline-offset: 2px;
    box-shadow: 0 0 0 3px var(--primary);
}

/* ---- 5 · Font (read-only current pairing) ---- */
.theme-panel-font {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    padding: 8px 14px;
    border: 1px solid var(--line);
    border-radius: var(--radius-control);
    background: var(--surface);
}

.theme-panel-font-specimen {
    font-family: var(--font-sans);
    font-size: var(--text-2xl);
    font-weight: 600;
    line-height: 1;
    color: var(--text);
}

.theme-panel-font-name {
    font-size: var(--text-sm);
    color: var(--text-muted);
}

/* ---- 6 · Icons fact row ---- */
.theme-panel-fact {
    font-size: var(--text-sm);
    color: var(--text-muted);
}

.theme-panel-fact-value {
    color: var(--text);
    font-weight: 500;
}

/* =================================================================================================
 * The data-radius axis — a global static axis (parallels data-primary/data-neutral, but its presets are
 * fixed, not ramp-derived, so it needs no service). The base :root values (utilities.css) ARE the default
 * R1 6/8 scale, so ABSENCE of data-radius = Default (the "default" preset carries no attribute). We remap
 * the RAW rungs too (not just --radius-control/-container): most components still consume the raw rungs, so
 * a control/container-only remap would move almost nothing. --radius-full stays 9999 (pills never square).
 * ================================================================================================= */
:root[data-radius="none"] {
    --radius-control: 0;
    --radius-container: 0;
    --radius-sm: 0;
    --radius: 0;
    --radius-md: 0;
    --radius-lg: 0;
    --radius-xl: 0;
    --radius-2xl: 0;
}

:root[data-radius="small"] {
    --radius-control: 3px;
    --radius-container: 4px;
    --radius-sm: 3px;
    --radius: 4px;
    --radius-md: 6px;
    --radius-lg: 8px;
    --radius-xl: 10px;
    --radius-2xl: 14px;
}

:root[data-radius="large"] {
    --radius-control: 10px;
    --radius-container: 14px;
    --radius-sm: 10px;
    --radius: 14px;
    --radius-md: 18px;
    --radius-lg: 22px;
    --radius-xl: 28px;
    --radius-2xl: 36px;
}

/* @toc Theme picker */
/* Theme picker — a small segmented control. --theme-picker-* from config. */

.theme-picker {
    display: inline-flex;
    gap: 2px;
    padding: 3px;
    border: 1px solid var(--line);
    border-radius: var(--theme-picker-border-radius, 8px);
    background: transparent;
}

.theme-picker-opt {
    appearance: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: none;
    background: transparent;
    color: var(--text-muted);
    padding: 5px 8px;
    border-radius: 6px;
    cursor: pointer;
}

.theme-picker-opt svg {
    width: 16px;
    height: 16px;
}

.theme-picker-opt:hover:not(.is-active) {
    color: var(--text);
}

.theme-picker-opt.is-active {
    background: var(--surface-raised);
    color: var(--text);
}

/* @toc Theory sheet */
/* Theory study sheet — bilingual rule-sheet (Theory mode) + interactive MBE drill
 * (Drills mode). App design tokens throughout; no hardcoded artifact colours.
 * View: theory-sheet.blade.php; behaviour: theory-sheet.js.
 *
 * Bilingual primitive: .theory-bilingual is EN | RU side-by-side on desktop and
 * stacked (EN over RU, divider between) on mobile. EN is authoritative; RU is the
 * condensed gloss (slightly muted). */

/* A recessed panel surface, one step behind --surface (like --page-bg, which already
 * has exactly that relationship in both themes) — for chips/badges/letters that need
 * to read as sunken rather than flush with the card they sit on. */
:root {
    --surface-sunken: var(--page-bg);
}

.theory-sheet {
    max-width: var(--max-w-4xl);
}

/* ---- meta head ---- */
.theory-head {
    margin-bottom: 18px;
}

.theory-sub {
    margin: 0;
    color: var(--text);
    font-weight: 600;
}

.theory-sub-ru {
    display: block;
    color: var(--text-muted);
    font-weight: 400;
    font-size: var(--text-sm);
}

.theory-arc {
    margin: 6px 0 0;
    color: var(--text-muted);
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.6px;
}

.theory-prov-ru {
    display: block;
    opacity: 0.8;
    font-size: var(--text-xs);
}

.theory-next {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 10px 0 0;
    color: var(--text-muted);
    font-size: var(--text-sm);
}

.theory-next svg {
    width: 15px;
    height: 15px;
}

/* The Theory/Drills mode switch is the shared <x-tabs> (pill variant) — its own
 * component owns the toggle look + behaviour. This sheet only reinstates the
 * mobile-sticky tab bar (see the responsive block). */

/* ---- bilingual primitive ---- */
.theory-bilingual {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
}

.theory-lang {
    padding: 12px 14px;
    border-radius: var(--radius);
    background: var(--surface-raised);
    border-left: 3px solid var(--info);
    font-size: var(--text-sm);
    line-height: 1.6;
    color: var(--text);
}

.theory-lang-ru {
    border-left-color: var(--success);
    color: var(--text-muted);
}

.theory-lang-label {
    font-size: var(--text-2xs, 10px);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.8px;
    color: var(--text-muted);
    margin-bottom: 5px;
}

.theory-lang strong {
    color: var(--text);
}

.theory-lang em {
    color: var(--primary);
    font-style: italic;
}

/* ---- section heads ---- */
.theory-section-head {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 26px 0 12px;
    padding-bottom: 8px;
    border-bottom: 2px solid var(--accent, var(--line));
}

.theory-section-icon {
    display: inline-flex;
    width: 20px;
    height: 20px;
    color: var(--accent, var(--primary));
}

.theory-section-icon svg {
    width: 100%;
    height: 100%;
}

.theory-section-title {
    margin: 0;
    font-size: var(--text-lg);
    font-weight: 700;
    color: var(--text);
}

.theory-section-ru {
    display: block;
    font-size: var(--text-xs);
    font-weight: 400;
    color: var(--text-muted);
}

.theory-tier {
    margin-left: auto;
    flex-shrink: 0;
    padding: 3px 10px;
    border-radius: var(--radius-full, 999px);
    font-size: var(--text-2xs, 10px);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.6px;
}

.theory-tier-1 { background: color-mix(in srgb, var(--danger) 16%, var(--surface)); color: var(--danger); border: 1px solid var(--danger); }
.theory-tier-2 { background: color-mix(in srgb, var(--warning) 16%, var(--surface)); color: var(--warning); border: 1px solid var(--warning); }
.theory-tier-3 { background: var(--surface-sunken); color: var(--text-muted); border: 1px solid var(--line); }

/* ---- tier grid (the master comparison) ---- */
.theory-tiergrid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 12px;
    margin: 12px 0;
}

.theory-tiercard {
    padding: 14px;
    border-radius: var(--radius-lg);
    border: 2px solid var(--accent, var(--line));
    background: color-mix(in srgb, var(--accent, var(--primary)) 8%, var(--surface));
}

.theory-tiercard-name {
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.6px;
    text-align: center;
    color: var(--accent, var(--text));
    margin-bottom: 8px;
}

.theory-tiercard-formula {
    font-size: var(--text-xs);
    line-height: 1.5;
    padding: 8px;
    border-radius: var(--radius);
    background: var(--surface-sunken);
    color: var(--text);
}

.theory-tiercard-formula strong { color: var(--accent, var(--text)); }

.theory-tiercard-burden {
    font-size: var(--text-2xs, 11px);
    font-style: italic;
    color: var(--text-muted);
    margin: 8px 0 6px;
}

.theory-tiercard-triggers {
    list-style: none;
    padding: 0;
    margin: 0;
}

.theory-tiercard-triggers li {
    font-size: var(--text-xs);
    padding: 2px 0;
    color: var(--text);
}

.theory-tiercard-triggers li::before {
    content: "▸ ";
    color: var(--accent, var(--primary));
}

/* ---- checklist ---- */
.theory-checklist {
    margin: 14px 0;
    padding: 16px;
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--surface-raised);
}

.theory-checklist-title {
    margin: 0 0 12px;
    font-size: var(--text-base);
    font-weight: 700;
    color: var(--text);
}

.theory-checklist-steps {
    margin: 0;
    padding-left: 22px;
}

.theory-checklist-steps li {
    margin-bottom: 10px;
}

.theory-checklist-example {
    margin-top: 12px;
}

/* ---- mnemonic ---- */
.theory-mnemonic {
    margin: 14px 0;
    padding: 14px 16px;
    text-align: center;
    border: 2px solid var(--warning);
    border-radius: var(--radius-lg);
    background: color-mix(in srgb, var(--warning) 10%, var(--surface));
}

.theory-mnemonic-text {
    font-size: var(--text-lg);
    font-weight: 700;
    letter-spacing: 1px;
    color: var(--warning);
}

.theory-mnemonic-gloss {
    margin-top: 6px;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.theory-trap-tested {
    margin-top: 8px;
    font-size: var(--text-2xs, 11px);
    text-transform: uppercase;
    letter-spacing: 0.6px;
    color: var(--text-muted);
}

/* ---- cases ---- */
.theory-case {
    margin: 10px 0;
    padding: 10px 14px;
    border-left: 3px solid var(--line);
    border-radius: var(--radius);
    background: var(--surface-raised);
}

.theory-case-key {
    border-left-color: var(--primary);
}

.theory-case-cite {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-bottom: 6px;
    color: var(--text);
}

.theory-case-cite svg { width: 14px; height: 14px; color: var(--primary); }

/* ---- examples ---- */
.theory-examples { margin: 12px 0; }

.theory-example {
    display: flex;
    gap: 8px;
    margin-bottom: 8px;
}

.theory-example-mark { flex-shrink: 0; }
.theory-example-mark svg { width: 18px; height: 18px; }
.theory-example-win .theory-example-mark { color: var(--success); }
.theory-example-fail .theory-example-mark { color: var(--danger); }
.theory-example .theory-bilingual { flex: 1; }

/* ===================== QUIZ ===================== */
.theory-quiz-bar {
    position: sticky;
    top: 0;
    z-index: 5;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 10px 14px;
    margin-bottom: 14px;
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--surface-raised);
    font-size: var(--text-sm);
    color: var(--text-muted);
}

.theory-quiz-pct {
    font-weight: 800;
    color: var(--primary);
}

.theory-q {
    display: none;
    padding: 20px;
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--surface-raised);
}

.theory-q.is-active { display: block; }

.theory-q-meta {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 14px;
}

.theory-q-topic, .theory-q-source, .theory-q-diff {
    font-size: var(--text-2xs, 10px);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.6px;
    padding: 3px 9px;
    border-radius: var(--radius-full, 999px);
}

.theory-q-topic { background: color-mix(in srgb, var(--primary) 14%, var(--surface)); color: var(--primary); border: 1px solid var(--primary); }
.theory-q-source { background: var(--surface-sunken); color: var(--text-muted); border: 1px solid var(--line); }
.theory-q-diff-easy { background: color-mix(in srgb, var(--success) 14%, var(--surface)); color: var(--success); }
.theory-q-diff-medium { background: color-mix(in srgb, var(--warning) 14%, var(--surface)); color: var(--warning); }
.theory-q-diff-hard { background: color-mix(in srgb, var(--danger) 14%, var(--surface)); color: var(--danger); }
.theory-q-num { margin-left: auto; font-size: var(--text-xs); color: var(--text-muted); }

.theory-q-question { margin-bottom: 14px; font-size: var(--text-base); }

.theory-q-options {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.theory-q-option {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    width: 100%;
    padding: 12px 14px;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--text);
    font: inherit;
    text-align: left;
    cursor: pointer;
    transition: border-color 0.12s ease, background 0.12s ease;
}

.theory-q-option:hover:not(:disabled) { border-color: var(--primary); }
.theory-q-option:disabled { cursor: default; }

.theory-q-option .theory-bilingual { flex: 1; gap: 8px; }
.theory-q-option .theory-lang { padding: 0; background: none; border: none; }

.theory-q-letter {
    flex-shrink: 0;
    width: 22px;
    height: 22px;
    line-height: 22px;
    text-align: center;
    border-radius: 50%;
    background: var(--surface-sunken);
    font-size: var(--text-xs);
    font-weight: 700;
}

.theory-q-option.is-correct { border-color: var(--success); background: color-mix(in srgb, var(--success) 12%, var(--surface)); }
/* SOLID-fill status = the INVERTED vetted pair: fill on the main token, text on its -surface companion.
   Contrast is symmetric, so each ratio is the already-vetted tint number read the other way — success
   4.57 light / 8.55 dark, danger 5.30 / 5.84 — and the pair flips with the theme, so no on-fill token is
   needed. Replaces emerald-600 + --on-primary (3.77, a real AA fail): white on a -600 fill only ever
   passed for the deep red/blue hues, which is why the drift went unnoticed. Zero new tokens. */
.theory-q-option.is-correct .theory-q-letter { background: var(--success); color: var(--success-surface); }
.theory-q-option.is-incorrect { border-color: var(--danger); background: color-mix(in srgb, var(--danger) 12%, var(--surface)); }
.theory-q-option.is-incorrect .theory-q-letter { background: var(--danger); color: var(--danger-surface); }

.theory-q-feedback { margin-top: 14px; }

.theory-q-verdict { font-weight: 700; margin-bottom: 10px; }
.theory-q-verdict.is-correct { color: var(--success); }
.theory-q-verdict.is-incorrect { color: var(--danger); }

.next-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin-top: 12px;
    padding: 9px 18px;
    border: none;
    border-radius: var(--radius);
    background: var(--primary);
    color: var(--on-primary);
    font: inherit;
    font-weight: 600;
    cursor: pointer;
}

/* ---- score screen ---- */
.theory-quiz-screen {
    text-align: center;
    padding: 32px 20px;
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--surface-raised);
}

.theory-quiz-final {
    font-size: 56px;
    font-weight: 800;
    color: var(--primary);
    line-height: 1;
}

.theory-quiz-detail { margin-top: 6px; color: var(--text-muted); }
.theory-quiz-msg { margin: 18px auto; max-width: 480px; color: var(--text); line-height: 1.6; }

/* ===================== RESPONSIVE ===================== */
/* 760px: local component breakpoint, intentionally a touch before --bp-md (768); not the app seam. */
@media (max-width: 760px) {
    /* Bilingual stacks: EN over RU with a divider, not side-by-side. */
    .theory-bilingual { grid-template-columns: 1fr; gap: 8px; }
    .theory-lang-ru { border-left: none; border-top: 2px solid var(--success); border-radius: 0; padding-top: 10px; }
    .theory-q-option .theory-lang-ru { border-top: 1px solid var(--line); padding-top: 6px; }

    /* Tier cards stack instead of cramming 3-up. */
    .theory-tiergrid { grid-template-columns: 1fr; }

    /* Mode switch (the <x-tabs> nav) pins to the top so she can flip without
     * scrolling back — the old .theory-switch sticky behaviour, scoped to the
     * tablist inside this sheet only. */
    .theory-sheet .tabs-nav {
        position: sticky;
        top: 0;
        z-index: 10;
        background: var(--surface);
    }
    .theory-sheet .tabs-pill > .tabs-nav > .tabs-tab { flex: 1; justify-content: center; }
}

/* @toc Tooltip */
/* Tooltip — a single floating label (one host per page) shown on hover of any
 * [data-tooltip] element, positioned by tooltip.js. An inverted chip (dark in
 * light mode, light in dark) so it reads over any surface. Colours: --tooltip-*. */

.tooltip {
    position: fixed;
    top: 0;
    left: 0;
    z-index: var(--z-tooltip);
    padding: 4px 8px;
    border-radius: 6px;
    background: var(--tooltip-background-color);
    color: var(--tooltip-text);
    font-family: var(--font-sans);
    font-size: var(--text-xs);
    font-weight: 500;
    white-space: nowrap;
    pointer-events: none;
    opacity: 0;
    transform: translateY(3px);
    transition: opacity 110ms ease, transform 110ms ease;
}

.tooltip.is-visible {
    opacity: 1;
    transform: translateY(0);
}

/* @toc Tree */
/* Tree — the APG Tree View primitive. Indentation via --tree-depth (set per node); collapse hides the
   child group (out of the a11y tree); the folder icon swaps closed↔open on aria-expanded and the chevron
   twists; leaf status (new/updated/deleted) reads at a glance via a glyph + row treatment. Ramp-lawful. */
.tree,
.tree-group {
    list-style: none;
    margin: 0;
    padding: 0;
}

/* Collapsed folder — its children leave the tree entirely (not just visually). */
.tree-node[aria-expanded="false"] > .tree-group {
    display: none;
}

.tree-row {
    display: flex;
    align-items: center;
    gap: 6px;
    height: var(--tree-row-height);
    padding-inline-end: 8px;
    /* Depth costs from the level BELOW the root, so the top row sits flush with its container (#1330). It used
       to be `depth * indent`, which charged the root a full step it had not earned: the container already pads
       (the editor's panel pads by 16px — exactly one step), so level one paid for the same space twice and the
       whole tree looked shoved right. The `- 1` removes only the LEADING space; every gap between levels is
       still one full --tree-indent, so nothing below the root shifts relative to anything else. */
    padding-inline-start: calc(6px + (var(--tree-depth, 1) - 1) * var(--tree-indent));
    border-radius: var(--tree-border-radius);
    cursor: pointer;
    color: var(--text);
    user-select: none;
}

.tree-row:hover {
    background: var(--tree-row-hover-background-color);
}

/* Roving focus lands on the <li> (role=treeitem); ring the row. */
.tree-node:focus {
    outline: none;
}

.tree-node:focus-visible > .tree-row,
.tree-node:focus > .tree-row {
    outline: 2px solid var(--primary);
    outline-offset: -2px;
}

.tree-node.is-selected > .tree-row {
    background: var(--tree-row-selected-background-color);
    font-weight: 600;
}

/* Chevron twist — points right when collapsed, down when expanded. Leaves get an inert spacer so their
   labels align with folder labels. */
.tree-twist {
    display: inline-flex;
    flex: 0 0 auto;
    width: 14px;
    height: 14px;
    color: var(--tree-twist);
    transition: transform 0.12s ease;
}

.tree-twist svg {
    width: 14px;
    height: 14px;
}

.tree-node[aria-expanded="true"] > .tree-row > .tree-twist {
    transform: rotate(90deg);
}

.tree-twist-leaf {
    visibility: hidden;
}

/* Folder icon swaps on state; leaf shows the document glyph. */
.tree-icon {
    display: inline-flex;
    flex: 0 0 auto;
    width: 16px;
    height: 16px;
    color: var(--tree-icon);
}

.tree-icon svg {
    width: 16px;
    height: 16px;
}

.tree-node[aria-expanded="true"] > .tree-row > .tree-icon-closed,
.tree-node[aria-expanded="false"] > .tree-row > .tree-icon-open {
    display: none;
}

.tree-label {
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--text-sm);
}

/* Leaf status — the change each file carries, readable at a glance. */
.tree-status {
    flex: 0 0 auto;
    width: 1.1em;
    text-align: center;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.tree-status-new {
    color: var(--tree-status-new);
}

.tree-status-updated {
    color: var(--tree-status-updated);
}

.tree-status-deleted {
    color: var(--tree-status-deleted);
}

/* new = additive accent on the icon; deleted = struck + dimmed name + danger accent. */
.tree-row-new .tree-icon {
    color: var(--tree-status-new);
}

.tree-row-deleted .tree-label {
    text-decoration: line-through;
    color: var(--text-muted);
}

.tree-row-deleted .tree-icon {
    color: var(--tree-status-deleted);
}

/* @toc Typography panel */
/*
 * Typography panel — the Editor's type authoring surface, the sibling of theme-panel's colour surface. Same
 * hairline-ruled schedule grammar (sections with .ui-label heads, compact rhythm) so the two panels read as
 * one system in the rail. Every colour comes from a semantic token; there is no swatch exception to make here,
 * because the specimen previews a FONT, not a fill.
 *
 * Container-first per the CSS-modernity contract: the panel lives in a rail whose width the user can change by
 * docking, so the element axes reflow on the PANEL's own inline size (@container), never the viewport. Base
 * rules are the narrow layout; the wider arrangement is the enhancement, min-width polarity.
 */

.typography-panel {
    container-type: inline-size;
}

/* ---- The schedule: hairline-ruled sections ---- */
.typography-panel-section {
    padding: var(--space-4) 20px;
    border-block-end: 1px solid var(--line);
}

.typography-panel-section:last-child {
    border-block-end: none;
}

.typography-panel-head {
    margin: 0 0 var(--space-3);
}

/* ---- 1 · Font families ---- */
.typography-panel-family {
    display: flex;
    align-items: center;
    gap: var(--space-3);
}

.typography-panel-family + .typography-panel-family {
    margin-block-start: var(--typography-panel-row-gap, var(--space-2));
}

/*
 * The specimen previews the family by rendering in it. Fixed box so a wide face (or a fallback that resolves
 * to something unexpected) cannot shove the select around as the value changes.
 */
.typography-panel-specimen {
    flex: none;
    display: grid;
    place-items: center;
    inline-size: 40px;
    block-size: 40px;
    font-size: var(--typography-panel-specimen-font-size, 20px);
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
}

.typography-panel-family .typography-panel-control {
    flex: 1 1 auto;
    min-inline-size: 0;
}

/* ---- 2 · Per-element type ---- */
.typography-panel-element + .typography-panel-element {
    margin-block-start: var(--space-4);
}

.typography-panel-element-name {
    margin: 0 0 var(--space-2);
    color: var(--text-muted);
    font-size: var(--text-sm);
}

/*
 * Base (narrow rail): the three axes stack. min-inline-size: 0 lets a select shrink inside the grid instead of
 * forcing an overflow — without it the native control's intrinsic width wins and the panel scrolls sideways.
 */
.typography-panel-axes {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-2);
}

.typography-panel-axes > * {
    min-inline-size: 0;
}

/* The enhancement: once the PANEL itself is wide enough, the three axes sit on one line. */
@container (min-width: 22rem) {
    .typography-panel-axes {
        grid-template-columns: repeat(3, 1fr);
    }
}

/* ---- Honest empty state ---- */
.typography-panel-empty {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--text-sm);
}

/* @toc Uploads gallery */
/* Uploads gallery — the bulk file dropper, the upload grid, and the modal viewer.
 * Self-contained (no cross-component class deps); theme tokens throughout.
 * View: uploads-gallery.blade.php; behaviour: uploads-gallery.js. */

.uploads-gallery {
    max-width: var(--max-w-4xl);
}

.uploads-gallery-upload {
    margin-bottom: 28px;
}

/* The big bulk drop zone — the whole uploader is this one click/drop target. */
.uploads-drop {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    min-height: 190px;
    padding: 48px 24px;
    text-align: center;
    border: 2px dashed var(--line);
    border-radius: var(--radius-lg);
    background: var(--surface-raised);
    color: var(--text-muted);
    font-size: var(--text-base);
    cursor: pointer;
    transition: border-color 0.15s ease, color 0.15s ease;
}

.uploads-drop:hover,
.uploads-drop.is-over {
    border-color: var(--primary);
    color: var(--text);
}

/* Keyboard focus — the file <input> is .sr-only (focusable but off-screen), so its focus ring must ride
 * the visible drop-zone label (WCAG 2.4.7). :focus-visible = keyboard only; the transparent outline is the
 * forced-colors fallback (box-shadow is dropped in high-contrast, so the outline gets promoted). */
.uploads-drop:has(input:focus-visible) {
    border-color: var(--primary);
    color: var(--text);
    outline: 2px solid transparent;
    outline-offset: 2px;
    box-shadow: 0 0 0 3px var(--primary);
}

/* While the batch posts, dim the zone and stop further drops. */
.uploads-drop.is-busy {
    opacity: 0.6;
    pointer-events: none;
}

.uploads-drop-icon {
    display: inline-flex;
    width: 30px;
    height: 30px;
    color: var(--text-muted);
}

.uploads-drop-icon svg {
    width: 100%;
    height: 100%;
}

.uploads-drop-hint {
    font-weight: 600;
    color: var(--text);
}

.uploads-drop-sub {
    font-size: var(--text-sm);
    color: var(--text-muted);
}

.uploads-empty {
    margin-top: 8px;
}

/* The grid of uploaded files. */
.uploads-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: 14px;
    margin-top: 8px;
}

.uploads-tile {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
    padding: 16px;
    text-align: left;
    background: var(--surface-raised);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    color: var(--text);
    cursor: pointer;
    font: inherit;
    transition: border-color 0.15s ease;
}

.uploads-tile:hover,
.uploads-tile:focus-visible {
    border-color: var(--primary);
}

.uploads-tile-icon {
    display: inline-flex;
    width: 22px;
    height: 22px;
    color: var(--text-muted);
}

.uploads-tile-icon svg {
    width: 100%;
    height: 100%;
}

.uploads-tile-title {
    font-weight: 600;
    line-height: 1.3;
    word-break: break-word;
}

.uploads-tile-meta {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.4px;
    color: var(--text-muted);
}

/* Modal viewer — a reusable <x-dialog>. The default .dialog is content-width with a
 * padded scrolling body; these id/class-scoped overrides make the viewer large and let
 * the iframe fill it. .uploads-viewer lands on the native <dialog> (class merge). */
.uploads-viewer {
    width: min(1000px, calc(100vw - 32px));
}

.uploads-viewer .dialog-panel {
    height: min(90vh, calc(100vh - 64px));
}

.uploads-viewer .dialog-body {
    padding: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.uploads-viewer-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 12px 16px;
    border-bottom: 1px solid var(--line);
}

.uploads-modal-title {
    font-weight: 600;
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.uploads-viewer .uploads-modal-frame {
    flex: 1 1 auto;
    width: 100%;
    border: 0;
}

/* @toc Wishlist */
/* Wishlist — a core, standalone intake box: a short compose form plus the author's (or, for
 * admins, everyone's) list of wishes with a New/Doing/Done status. Built on primitives
 * (x-textarea / x-select / x-input / x-button), so this sheet themes only the wishlist's own
 * composition + the status chips. View: wishlist.blade.php. No JS — plain server forms. */

.wishlist {
    max-width: var(--max-w-3xl);
    margin-top: 32px;
}

.wishlist-head {
    margin-bottom: 12px;
}

.wishlist-h {
    margin: 0;
    font-size: var(--text-lg);
    font-weight: 600;
    color: var(--text);
}

.wishlist-h-ru {
    margin-inline-start: 8px;
    font-size: var(--text-sm);
    font-weight: 500;
    color: var(--text-muted);
}

.wishlist-lead {
    margin: 4px 0 0;
    font-size: var(--text-sm);
    color: var(--text-muted);
}

.wishlist-form {
    margin: 14px 0 24px;
}

.wishlist-form-actions {
    margin-top: 10px;
}

/* The list of wishes — newest first, a coloured leading (inline-start) edge per status. */
.wishlist-items {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.wishlist-item {
    padding: 14px 16px;
    border: 1px solid var(--line);
    border-inline-start: 3px solid var(--text-muted);
    border-radius: var(--radius-lg);
    background: var(--surface-raised);
}

.wishlist-item-doing {
    border-inline-start-color: var(--warning);
}

.wishlist-item-done {
    border-inline-start-color: var(--success);
}

.wishlist-item-body {
    color: var(--text);
    white-space: pre-wrap;
    word-break: break-word;
}

.wishlist-item-meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    margin-top: 8px;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.wishlist-status {
    text-transform: uppercase;
    letter-spacing: 0.4px;
    font-weight: 600;
    padding: 2px 8px;
    border-radius: var(--radius-full, 999px);
    color: var(--text-muted);
    background: color-mix(in srgb, var(--text-muted) 16%, transparent);
}

/* Active-status chips ride the vetted self-contrasting token pair (token on its own
 * -surface tint is AA by construction), replacing the ad-hoc palette tints. The New
 * chip above stays a neutral --text-muted wash — it carries no status hue. */
.wishlist-status-doing {
    color: var(--warning);
    background: var(--warning-surface);
}

.wishlist-status-done {
    color: var(--success);
    background: var(--success-surface);
}

/* Admin's reply back to the author. */
.wishlist-item-note {
    display: flex;
    gap: 8px;
    margin-top: 8px;
    padding-top: 8px;
    border-top: 1px dashed var(--line);
    font-size: var(--text-sm);
    color: var(--text);
}

.wishlist-item-note-label {
    flex-shrink: 0;
    font-weight: 600;
    color: var(--text-muted);
}

/* Admin status control — a compact row of primitives (status select + reply + save). The
 * primitives theme themselves; this only lays them out, growing the note input to fill. */
.wishlist-admin {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 8px;
    margin-top: 12px;
    padding-top: 12px;
    border-top: 1px solid var(--line);
}

.wishlist-admin .input {
    flex: 1;
    min-width: 160px;
}

.wishlist-empty {
    margin-top: 4px;
}

/* @toc Accessibility page */
/*
 * /accessibility page — landing-grade presentation (hero, icon cards, stat band, status card, chip
 * nav). All class-scoped .a11y-*; no JS. Same sections/copy/derivation as the restructure.
 *
 * COLOR LAW (this page's original axe violations were opacity-muted text): NEVER de-emphasize text
 * with opacity on glyphs, and NEVER hand-author a colour. Every colour is a ramp/semantic token
 * (--text, --text-muted, --line, --surface, --surface-raised, --primary-*); for a tint, color-mix on
 * a BACKGROUND only. No hex, no --palette-*. Decorative icons are aria-hidden (MediaService), so their
 * colour is free of contrast rules; real text stays on tokens that hold AA.
 *
 * RHYTHM: a wide container (~1080px) lets the bands (hero, cards, stat band) breathe; prose + the
 * contract ledger stay at a readable measure. De-emphasis is size/weight/spacing, never glyph opacity.
 */

.a11y {
    max-width: 1080px;
    margin-inline: auto;
}

.a11y section {
    margin: 3rem 0;
}

.a11y section > h2 {
    font-size: var(--text-2xl);
    margin: 0 0 1.1rem;
    scroll-margin-top: 5rem;
}

/* Prose + ledger keep a readable measure; the bands above break wider. */
.a11y section > p,
.a11y-list,
.a11y-audit-caveat,
.a11y-rules,
.a11y-fineprint p {
    max-width: 66ch;
}

/* ── 1. Hero band ─────────────────────────────────────────────────────────── */
.a11y-hero {
    background: var(--surface-raised);
    border: 1px solid var(--line);
    border-radius: 16px;
    padding: 2.75rem 2.25rem;
    margin-bottom: 2.5rem;
}

.a11y-kicker {
    font-size: var(--text-sm);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.09em;
    /* --text-muted (not --primary-500, which is only 4.02:1 on the dark surface) so the eyebrow holds
       AA on both tenants; the brand red still carries the card icons, which are decorative. */
    color: var(--text-muted);
    margin: 0 0 0.85rem;
}

.a11y-lead {
    font-size: var(--text-3xl);
    line-height: 1.2;
    font-weight: 650;
    color: var(--text);
    margin: 0 0 1rem;
    max-width: 24ch;
}

.a11y-aim {
    font-size: var(--text-lg);
    color: var(--text-muted);
    margin: 0 0 1.5rem;
    max-width: 56ch;
}

.a11y-cta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.65rem 1rem;
    margin: 0 0 1.5rem;
}

.a11y-cta-email {
    font-size: var(--text-sm);
    color: var(--text-muted);
}

/* The hero shows the address as readable TEXT (the button is the mailto action) — full-ink so it holds
   AA, and it avoids a second brand-red link that wouldn't. */
.a11y-email {
    color: var(--text);
    font-weight: 600;
}

/* ── Chips (status row, audit date, mirror source, group counts) ──────────── */
.a11y-chiprow {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

.a11y-chip {
    display: inline-flex;
    align-items: center;
    padding: 0.2rem 0.7rem;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--surface);
    font-size: var(--text-sm);
    color: var(--text-muted);
    white-space: nowrap;
}

/* ── 2. On this page — pill-chip nav, CSS-only sticky on desktop ───────────── */
.a11y-toc {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin: 0 0 2.5rem;
}

.a11y-toc a {
    display: inline-flex;
    padding: 0.35rem 0.85rem;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--surface-raised);
    font-size: var(--text-sm);
    color: var(--text-muted);
    text-decoration: none;
}

.a11y-toc a:hover,
.a11y-toc a:focus-visible {
    color: var(--text);
    border-color: var(--primary-500);
}

@media (min-width: 768px) {
    .a11y-toc {
        position: sticky;
        top: 0;
        z-index: 5;
        margin-inline: -0.75rem;
        padding: 0.75rem;
        background: color-mix(in srgb, var(--surface) 88%, transparent);
        backdrop-filter: blur(6px);
        border-radius: 12px;
    }
}

/* ── 3. What you can expect — icon cards ──────────────────────────────────── */
.a11y-cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
    gap: 1rem;
    margin: 0 0 1.25rem;
}

.a11y-card {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    padding: 1.35rem;
    background: var(--surface-raised);
    border: 1px solid var(--line);
    border-radius: 12px;
}

.a11y-card-icon {
    display: inline-flex;
    color: var(--primary-500);
}

.a11y-card-icon svg {
    width: 26px;
    height: 26px;
}

.a11y-card p {
    margin: 0;
    color: var(--text);
    line-height: 1.45;
}

.a11y-honest {
    color: var(--text-muted);
    font-style: italic;
    margin: 0;
}

/* ── 4/5. Callouts (report + audit status card) ───────────────────────────── */
.a11y section > .callout {
    max-width: 66ch;
}

.a11y .callout p:first-child {
    margin: 0;
}

.a11y-hint {
    font-size: var(--text-sm);
    color: var(--text-muted);
    margin: 0.5rem 0 0;
}

/* Audit as a status card: big count beside the meta; the callout supplies the variant edge. */
.a11y-audit-card .callout-body {
    display: flex;
    align-items: center;
    gap: 1.15rem;
}

.a11y-audit-num {
    font-size: var(--text-4xl);
    font-weight: 700;
    line-height: 1;
    color: var(--text);
}

.a11y-audit-meta {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
}

.a11y-audit-headline {
    margin: 0;
    font-weight: 600;
    color: var(--text);
}

.a11y-audit-sub {
    margin: 0;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: var(--text-sm);
    color: var(--text-muted);
}

.a11y-audit-caveat {
    font-size: var(--text-sm);
    color: var(--text-muted);
    margin: 0.9rem 0 0;
}

.a11y-rules {
    margin: 1rem 0 0;
    padding-left: 1.15rem;
}

.a11y-rules li {
    margin: 0.35rem 0;
    font-size: var(--text-sm);
    color: var(--text);
}

.a11y-audit-details {
    margin-top: 1rem;
    max-width: 66ch;
}

/* ── How-it's-built list ──────────────────────────────────────────────────── */
.a11y-list {
    margin: 0;
    padding-left: 1.2rem;
}

.a11y-list li {
    margin: 0.6rem 0;
    color: var(--text);
    line-height: 1.55;
}

/* ── 8. Stat band (contractStats) ─────────────────────────────────────────── */
.a11y-lead-sub {
    font-size: var(--text-lg);
    color: var(--text-muted);
    margin: 0 0 1.5rem;
    max-width: 56ch;
}

.a11y-statband {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 1rem;
    margin: 0 0 2.5rem;
}

.a11y-stat-card {
    padding: 1.5rem;
    background: var(--surface-raised);
    border: 1px solid var(--line);
    border-radius: 12px;
}

.a11y-stat-num {
    display: block;
    font-size: var(--text-4xl);
    font-weight: 700;
    line-height: 1;
    color: var(--text);
    margin-bottom: 0.4rem;
}

.a11y-stat-label {
    font-size: var(--text-sm);
    color: var(--text-muted);
}

/* ── 5/7. Contract ledger — rows, not cards; group headers with count badges ── */
.a11y-group {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    font-size: var(--text-sm);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
    margin: 2rem 0 0.4rem;
}

.a11y-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.5rem;
    padding: 0.05rem 0.45rem;
    border-radius: 999px;
    background: color-mix(in srgb, var(--text) 8%, transparent);
    font-size: var(--text-sm);
    font-weight: 600;
    letter-spacing: 0;
    color: var(--text-muted);
}

.a11y-ledger {
    max-width: 72ch;
    border-top: 1px solid var(--line);
}

/* Strip the <x-details> card chrome → ledger rows (1px rule between), with a subtle hover tint. */
.a11y-row.details {
    border: 0;
    border-bottom: 1px solid var(--line);
    border-radius: 0;
    background: transparent;
}

.a11y-row.details:hover {
    background: color-mix(in srgb, var(--text) 4%, transparent);
}

.a11y-kind {
    display: inline-block;
    margin-left: 0.55rem;
    padding: 0.05rem 0.5rem;
    border: 1px solid var(--line);
    border-radius: 999px;
    font-size: 0.85rem;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
    vertical-align: middle;
}

.a11y-adherence {
    margin: 0 0 0.5rem;
    font-size: 0.92rem;
    color: var(--text);
}

.a11y-desc {
    margin: 0 0 0.5rem;
    font-size: 0.9rem;
    color: var(--text-muted);
}

.a11y-points {
    margin: 0.4rem 0 0;
    padding-left: 1.15rem;
}

.a11y-points li {
    margin: 0.3rem 0;
    font-size: 0.9rem;
    color: var(--text);
}

/* "Keyboard" label above the shared keyboard-table in a contract row. */
.a11y-kbd-label {
    margin: 0.85rem 0 0.35rem;
    font-size: 0.8rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
}

.a11y-anchor {
    display: inline-block;
    margin-top: 0.65rem;
    font-size: 0.9rem;
}

/* The per-page mirror's intro carries a chip; keep it on a readable measure. */
.a11y-stat {
    color: var(--text-muted);
    margin: 0 0 1rem;
    max-width: 66ch;
}

/* ── 9. Fine print ────────────────────────────────────────────────────────── */
.a11y-fineprint {
    margin-top: 3rem;
    padding-top: 1.5rem;
    border-top: 1px solid var(--line);
}

.a11y-fineprint p {
    font-size: var(--text-sm);
    color: var(--text-muted);
    margin: 0 0 0.5rem;
}

/* @toc Admin page */
/* Admin access panel — the user x section grid. Page CSS, bundled by CssService. */

.admin-panel {
    max-width: var(--max-w-2xl);
}

.admin-table {
    width: 100%;
    border-collapse: collapse;
    margin: 14px 0;
}

.admin-table th,
.admin-table td {
    padding: 10px 12px;
    text-align: left;
    border-bottom: 1px solid var(--line);
}

.admin-table th {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.4px;
    font-weight: 700;
    color: var(--text-muted);
}

.admin-table td {
    font-size: var(--text-sm);
}

.admin-user-name {
    display: block;
    font-weight: 600;
    color: var(--text);
}

.admin-user-email {
    display: block;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.admin-admin-cell {
    color: var(--text-muted);
    font-style: italic;
}

.admin-table input[type="checkbox"] {
    width: 16px;
    height: 16px;
    accent-color: var(--primary);
    cursor: pointer;
}

.admin-actions {
    margin-top: 12px;
}

/* @toc Barbruiser essay page */
/* BarBruiser Essay / SA reader — TWO PANELS, EACH WITH ITS OWN TABS (#1343): the Question on the left, one tab
 * per model answer on the right, standing as two columns at --bp-lg and stacking below it. The tab strips are
 * real at every width; nothing here hides one. The direct sibling of the PT reader (barbruiser-pt.css), which is
 * the same layout with three tabs on its left. No card skin inside a panel — a tab group already draws it. The
 * issue rail composes x-issue-card, so no bespoke issue markup lives here. Page CSS, auto-bundled by CssService.
 * Colour comes only from semantic tokens + the ramp; spacing in px matches barbruiser.css. */

.essay-reader {
    display: flex;
    flex-direction: column;
    gap: 18px;
}

/* Topbar — the meta line and the read/compare control, sharing a row (wrapping when tight). */
.essay-topbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
}

.essay-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin: 0;
    font-size: var(--text-sm);
    color: var(--text-muted);
}

.essay-meta-item {
    white-space: nowrap;
}

/* A trailing separator after the last item is harmless; hide it so the line never ends on a middot. */
.essay-meta-sep {
    color: var(--line);
}

.essay-meta-item:last-child + .essay-meta-sep,
.essay-meta-sep:last-child {
    display: none;
}

/* The two-artifact "study this" affordance. */
.essay-studysheet {
    margin: 0;
    font-size: var(--text-sm);
}

.essay-studysheet a {
    color: var(--link);
    font-weight: 600;
    text-decoration: none;
}

.essay-studysheet a:hover {
    text-decoration: underline;
}

/* ── THE TWO PANELS ───────────────────────────────────────────────────────────────────────────
 * (#1343) Two panels side by side, EACH CARRYING ITS OWN TABS: the Question on the left, one tab per model
 * answer on the right. The tab strips are real at every width — this replaces the `unwrap="1024"` layout,
 * where tabs.js deleted the tab strip above the seam and this file grid-laid the panels out instead.
 *
 * WHAT THAT MEANS FOR THIS FILE: the old block hid `.tabs-nav`, gridded `.tabs-body`, and force-showed every
 * `.tabs-panel` (to defeat the one-at-a-time gating). ALL THREE ARE GONE — with two independent tab groups
 * the component's own gating is exactly what we want, and hiding a tab strip is the bug being fixed. What
 * remains is only the two-column arrangement of the panels themselves.
 *
 * VIEWPORT @media, not @container, and still deliberate: this is PAGE CHROME (does the reader stand in one
 * column or two?), which is where the --bp-* seam belongs. It no longer has to agree with any JS value —
 * tabs.js reads no breakpoint here now — so the seam is free to be a pure layout choice.
 */
.essay-panels {
    display: grid;
    gap: 16px;
    margin-block: 4px;
}

/* A grid child that scrolls must be allowed to be narrower than its content, or a long unbroken line in the
   fact pattern widens the column and the two panels stop being equal. */
.essay-panel {
    min-inline-size: 0;
}

.essay-panel-scroll {
    display: flex;
    flex-direction: column;
    gap: 16px;
}

/* Layout only — the text treatment (14/600 normal-case) comes from the composed .ui-label (R5's shared label
   recipe, base.css), so a future label restyle lands in ONE rule, never here. */
.essay-panel-heading {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0;
}

@media (min-width: 1024px) {
    /* Two columns — LEFT the Question, RIGHT the answers (the essay's classic reading). `align-items: start`
       so a short answer panel does not stretch to the fact pattern's height. */
    .essay-panels {
        grid-template-columns: 1fr 1fr;
        align-items: start;
    }

    /* Independent scroll, now that they are side by side: read the question and the answers in parallel.
       The cap is on the SCROLL REGION rather than the panel, so the tab strip stays pinned above it and a long
       fact pattern scrolls under a strip that never leaves — which is the point of tabs you can still reach. */
    .essay-panel-scroll {
        overflow-y: auto;
        max-block-size: 62vh;
    }
}

/* ── QUESTION PANEL ──────────────────────────────────────────────────────────────────────────── */

.essay-lead {
    margin: 0;
    font-weight: 600;
    color: var(--text);
}

.essay-calls-section {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.essay-calls {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin: 0;
    padding: 0;
    list-style: none;
}

/* Weighted text carries the calls — the "Calls" heading is the emphasis. No accent rule on a self-numbering
   list (the anti-slop tell). */
.essay-calls li {
    font-weight: 600;
    color: var(--text);
}

.essay-instruction {
    margin: 0;
    font-style: italic;
    color: var(--text-muted);
}

/* ── MODEL ANSWERS PANEL ─────────────────────────────────────────────────────────────────────── */

.essay-answer {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.essay-answer-meta {
    margin: 0;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.essay-answer-meta-sep {
    margin-inline: 2px;
}

/* Honest-empty (C3): five essays in the corpus publish no selected answers, so the right panel says so
   rather than rendering a tab strip with nothing behind it. */
.essay-answers-empty {
    margin: 0;
    padding: 16px 0;
    font-size: var(--text-sm);
    color: var(--text-muted);
}

.essay-answer-note {
    margin: 0;
    font-size: var(--text-sm);
    font-style: italic;
    color: var(--text-muted);
}

/* Verbatim answer text — preserve the source line breaks, keep a readable measure; break long unbroken
   tokens rather than force a pane scrollbar (the PT reader's .pt-prose rule). */
.essay-answer-text {
    white-space: pre-line;
    overflow-wrap: break-word;
    max-inline-size: 75ch;   /* a readable measure for the long answer prose (core study content) */
    font-size: var(--text-sm);
    line-height: 1.65;
    color: var(--text);
}

/* ── STUDY REVEAL — the derived issue checklist ──────────────────────────────────────────────── */

.essay-reveal {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.essay-checklist-note {
    margin: 0 0 12px;
    font-size: var(--text-sm);
    font-style: italic;
    color: var(--text-muted);
}

.essay-issues {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin: 0;
    padding: 0;
}

/* @toc Barbruiser mbe page */
/* BarBruiser MBE index (/barbruiser/mbes) — the section's front door: 137 questions as pickable cards,
 * grouped by subject. Sibling of the PT index (barbruiser-pt.css's .pts-index), deliberately: same rhythm,
 * same group-then-grid shape, so the two browse screens read as one system.
 *
 * The CARD SKIN is x-card's, not this file's — these rules own only the link wrapper, the card's internal
 * type, and the grid. Page CSS, auto-bundled by CssService; colour from semantic tokens only.
 */

.mbes-index {
    display: flex;
    flex-direction: column;
    gap: 22px;
}

.mbes-intro {
    margin: 0;
    max-inline-size: 68ch;
    color: var(--text-muted);
    line-height: 1.6;
}

.mbes-group-title {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0 0 12px;
    font-size: var(--text-lg);
    font-weight: 700;
    color: var(--text);
}

/* How many questions this subject holds — the reader is choosing where to spend an hour. */
.mbes-group-count {
    font-size: var(--text-xs);
    font-weight: 500;
    color: var(--text-muted);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 1px 6px;
}

.mbes-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    gap: 12px;
}

/* The whole card is the link. It carries no chrome of its own — x-card inside is the surface — so this is
   just the hit area + the hover/focus affordance. */
.mbes-card {
    display: block;
    text-decoration: none;
    border-radius: var(--card-border-radius);
}

.mbes-card .card {
    block-size: 100%;
    transition: border-color 0.15s ease;
}

.mbes-card:hover .card {
    border-color: var(--link);
}

/* Focus lands on the anchor, but the CARD is what the eye sees — so put the ring on the card. */
.mbes-card:focus-visible {
    outline: none;
}

.mbes-card:focus-visible .card {
    outline: 2px solid var(--focus, var(--link));
    outline-offset: 2px;
}

.mbes-card .card-body {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.mbes-card-topic {
    font-weight: 700;
    color: var(--text);
    line-height: 1.4;
}

.mbes-card-stem {
    color: var(--text-muted);
    font-size: var(--text-sm);
    line-height: 1.5;
}

.mbes-card-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-block-start: auto;
    padding-block-start: 4px;
}

.mbes-empty {
    margin: 0;
    color: var(--text-muted);
}

/* @toc Barbruiser pt page */
/* BarBruiser PT reader — TWO VIEWS OF TWO PANELS (#1349, James's settled design). A [Test | Answer] switch over
 * two 50/50 panels: TEST is every source as a tab beside the place she writes; ANSWER is what she wrote beside
 * the model answers, the issue checklist and the metadata.
 *
 * NOTHING HERE HIDES A TAB STRIP. The `unwrap` block that used to (and the per-pane details folds, the pinned
 * Task strip, the three header bars) is gone with the layout that needed it. Page CSS, auto-bundled by
 * CssService. Colour comes only from semantic tokens + the ramp; spacing in px matches barbruiser.css. */

.pt-reader {
    display: flex;
    flex-direction: column;
    gap: 18px;
}

/* ── THE TOP BAR: TITLE · SWITCH · TIMER, ONE ROW ─────────────────────────────────────────────
 * James asked for this twice: "the top bar is for title and actions". The h1 was 381px wide with 585px of dead
 * space beside it while the switch sat on its own 33px row below. Now all three share row 1.
 *
 * THE MECHANISM, and it is the interesting part. The switch must stay a real x-tabs — the alternative is
 * re-authoring tab behaviour and losing the roles and keyboard contract — but x-tabs renders its tablist and its
 * panels as ONE component, and tabs.js reads both with `:scope >`. So the tablist cannot be lifted into the
 * shell's header bar (a different DOM subtree, outside main) without severing it from the panels it controls.
 *
 * Instead this page owns its own h1 (x-barbruiser-page's documented pattern for content pages) and the x-tabs
 * root becomes `display: contents`, so the component's nav and body are promoted into THIS grid. The nav lands
 * in the title row; the body spans the row beneath. Nothing about x-tabs changes, and its box is safe to
 * dissolve because `.tabs` carries no background, border or padding of its own — only its children do.
 *
 * Grid areas are EXPLICIT rather than auto-flowed: the timer sits before the tabs in the DOM (so the reading
 * order is title → time-remaining → controls) while belonging in the last column.
 */
.pt-topline {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: baseline;
    column-gap: 18px;
}

.pt-title {
    grid-area: 1 / 1;
    margin: 0;
    font-size: var(--text-2xl);
    font-weight: 700;
    color: var(--text);
}

.pt-topline > .tabs {
    display: contents;
}

.pt-topline > .tabs > .tabs-nav {
    grid-area: 1 / 2;
    /* The strip is two tabs; it should sit with the title, not stretch across the gap to the timer. */
    justify-self: start;
    align-self: end;
    /* No bottom rule here: this row already reads as a header, and the underline's rule would draw a line
       through the title row rather than under a strip. */
    border-bottom: 0;
}

.pt-timer {
    grid-area: 1 / 3;
    display: flex;
    align-items: baseline;
    justify-content: flex-end;
    gap: 8px;
    margin: 0;
    white-space: nowrap;
}

.pt-topline > .tabs > .tabs-body {
    grid-area: 2 / 1 / 3 / 4;
}

/* MOBILE: three things do not fit on a 375px row. Collapse to one column and let them stack in DOM order —
   title, time, switch — which is also the order of what she needs to know. */
@media (max-width: 768px) {
    .pt-topline {
        grid-template-columns: minmax(0, 1fr);
    }

    .pt-title,
    .pt-timer,
    .pt-topline > .tabs > .tabs-nav {
        grid-area: auto;
        justify-self: stretch;
    }

    .pt-timer {
        justify-content: flex-start;
    }

    .pt-topline > .tabs > .tabs-body {
        grid-area: auto;
    }
}

.pt-timer-label {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.4px;
    color: var(--text-muted);
}

/* Tabular figures so the digits do not jitter once this counts down. */
.pt-timer-value {
    font-size: var(--text-lg);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--text);
}

/* ── THE TWO PANELS ───────────────────────────────────────────────────────────────────────────
 * 50/50, as drawn. `minmax(0, 1fr)` on each track rather than a bare `1fr` is what actually lets a scrolling
 * child be narrower than its content — without it one long unbroken line in a File document widens its track
 * and the split stops being even.
 *
 * ONE COLUMN below --bp-lg: a phone cannot hold a document beside a writing surface, and stacking them keeps
 * both usable. The panels stay in DOM order, so the sources come first and what she writes follows.
 */
.pt-views {
    display: grid;
    gap: 16px;
    margin-block: 4px;
}

.pt-view-panel {
    min-inline-size: 0;
}

@media (min-width: 1024px) {
    .pt-views {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
        align-items: start;
    }

    /* THE TEST VIEW IS NOT 50/50, and the numbers are why. Its source side holds TWO columns — the vertical
       strip plus the document — so an even split leaves the document 298px, which is 36 characters at reading
       type: below the 45–75 band and below the 42 that had Daria printing the Library (#1346). Weighting it
       1.4:1 gives the document ~392px (~49 characters) and still leaves the writing surface ~400px (~50).
       Both sides clear the band; an even split cleared it on neither. Measured, not chosen.
       SCOPED TO THE TEST VIEW: the ANSWER view's two panels are both plain prose, so the extra width would be
       borrowed from the model answers for no reason — there, 50/50 is the honest split and the card's own. */
    [data-panel="test"] .pt-views {
        grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
    }

    /* Independent scroll, now that they are side by side — read a source and write against it in parallel, the
       way the exam software does. The cap sits on the SCROLL REGION, not the panel, so each tab strip stays
       pinned above its own scrolling content and never leaves the screen. Below the seam the page scrolls
       instead: a nested scroller on a phone is the classic trap. */
    .pt-panel-scroll {
        overflow-y: auto;
        max-block-size: 62vh;
    }
}

.pt-panel-scroll {
    display: flex;
    flex-direction: column;
    gap: 16px;
}

/* ── THE SOURCE STRIP RUNS DOWN THE SIDE ──────────────────────────────────────────────────────
 * Every source is visible at once AND the document sits level with the writing panel beside it. Both of those
 * failed before: x-tabs' horizontal nav scrolls with a hidden scrollbar (so 4 of 7 tabs were unreachable
 * without a gesture), and making it wrap instead produced a 135px three-row strip that pushed the document to
 * y=278 while the writing box started at y=178 — the memorandum read as sitting UNDER the layout rather than
 * beside the answer, which is exactly what James said.
 *
 * The fix is the primitive's own `orientation="vertical"` (#1349, added on x-tabs with aria-orientation and
 * Up/Down keys). A vertical strip grows down — the axis with room — so it holds its shape from 4 sources to 13,
 * and the panel content starts at the strip's own top.
 *
 * THE STRIP IS NARROWER HERE THAN THE COMPONENT DEFAULT (13rem) because it lives inside a half-width panel:
 * every pixel it takes comes off the document's reading measure, which is the thing #1346 was raised to protect.
 */
.pt-view-sources .tabs {
    --tabs-vertical-strip: 9rem;
    --tabs-vertical-gap: 14px;
}

/* BELOW THE SEAM the whole reader is one column, and a 144px strip beside a document inside a 343px column
   would leave ~185px of prose — 23 characters. So the strip goes FULL WIDTH above the document instead: still a
   vertical list, still Up/Down keys, so `aria-orientation="vertical"` stays truthful; only the two boxes stop
   sharing a row. Page chrome, so it rides the --bp-* seam. */
@media (max-width: 768px) {
    .pt-view-sources .tabs-vertical {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* The tab labels are truncated document titles, so they need to wrap inside a narrow strip and sit tight. */
.pt-view-sources .tabs-tab {
    font-size: var(--text-xs);
    line-height: 1.35;
}

/* ── A SOURCE DOCUMENT ───────────────────────────────────────────────────────────────────────── */

.pt-doc {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.pt-doc-head {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

/* The FULL title lives here — the tab label is truncated to keep the strip readable, so the untruncated text
   has to be somewhere, and the panel heading is where you look once the tab is open. */
.pt-doc-title {
    margin: 0;
    font-size: var(--text-base);
    font-weight: 700;
    color: var(--text);
}

.pt-doc-meta {
    margin: 0;
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.4px;
    color: var(--text-muted);
}

.pt-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.pt-tag {
    padding: 2px 8px;
    font-size: var(--text-xs);
    color: var(--text-muted);
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius-full);
}

.pt-auth-statutes {
    margin: 0;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.pt-auth-statutes-label {
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    margin-inline-end: 4px;
    color: var(--text);
}

.pt-instructions-note {
    margin: 0;
    font-size: var(--text-sm);
    font-style: italic;
    color: var(--text-muted);
}

/* Verbatim document/answer text — preserve the source line breaks. The reader is content-agnostic, so break
 * long unbroken tokens (URLs, long cites) rather than force a pane scrollbar. */
.pt-prose {
    white-space: pre-line;
    overflow-wrap: break-word;
    font-size: var(--text-sm);
    line-height: 1.65;
    color: var(--text);
}

/* ── READING A DOCUMENT (#1346, kept through the #1349 rebuild) ────────────────────────────────
 * Daria was PRINTING the library. Measured at 1280x720: the reading column was 299px of 14px text — 42
 * characters a line, against a comfortable band of 45-75 and a sheet of paper at ~90. Six words a line for
 * thousands of words returns your eye to the left margin twice as often as it should. Paper won.
 *
 * THE MEASURE IS DERIVED, AND IT IS IN `em` FOR A REASON. A measure is a character count, so it has to be
 * relative to the text's OWN size: 1 average character is 0.505em in this font (8.08px at 16px, measured), so
 * 36.4em is ~72 characters AT ANY FONT SIZE. That is the whole point of em here.
 *
 * I had it in `rem` first and measured the consequence: 36.5rem is 584px, which is 72 characters at the prose's
 * 16px but **96** at the tags' 12px — one absolute width cannot serve two type sizes. em fixes it by
 * construction rather than by a second hand-tuned number.
 *
 * WHY NOT `ch`, which looks like the obvious unit: `ch` is the width of the "0" GLYPH (10.61px here at 16px),
 * not of an average character (8.08px) — a ratio of 0.762, so `Nch` holds about N x 1.31 characters. A
 * natural-looking `70ch` computes to 742px, ~92 characters, well past the band, and at that width it never
 * binds at all so it looks like it works. Measured, not assumed.
 *
 * IT APPLIES TO THE TAGS TOO, caught by measuring rather than looking: capping only `.pt-prose` left the
 * holding tags spanning the full pane at 12px — past the ceiling in the other direction. A reading column is
 * one column: everything you read in it shares the measure.
 *
 * THE TYPE BUMP IS NOT UNCONDITIONAL. At 375px the pane is ~343px, where 16px yields 42 characters and 14px
 * yields 48 — and 42 is the number that had the Library being printed. Bigger type in a column that cannot grow
 * buys legibility by spending measure, and on a phone there is no measure to spend. So the bump starts where
 * there is width to carry it and below that the prose keeps `.pt-prose`'s own --text-sm.
 */
.pt-view-sources .tabs-body > [data-panel] .pt-prose,
.pt-view-sources .tabs-body > [data-panel] .pt-tag,
.pt-view-sources .tabs-body > [data-panel] .pt-auth-statutes {
    max-inline-size: 36.4em;
}

@media (min-width: 1024px) {
    .pt-view-sources .tabs-body > [data-panel] .pt-prose {
        font-size: var(--text-base);
        line-height: 1.75;
    }
}

/* WHERE SHE WRITES — the surface itself is x-pt-answer-field, which owns its own CSS (it is a component
 * now, not page markup). All this page contributes is letting the panel body stack its parts. */
.pt-write-panel .panel-body-inner {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

/* ── THE ANSWER VIEW ─────────────────────────────────────────────────────────────────────────── */

.pt-answer {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.pt-answer-meta {
    margin: 0;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.pt-answer-meta-sep {
    margin-inline: 2px;
    color: var(--line);
}

/* Honest-empty (C3): one PT in the corpus publishes no selected answers, and she has not written an answer to
   most of them — both say so plainly rather than rendering an empty region. */
.pt-answers-empty {
    margin: 0;
    padding: 16px 0;
    font-size: var(--text-sm);
    color: var(--text-muted);
}

.pt-issues {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin: 0;
    padding: 0;
    list-style: none;
}

/* ── INFO — the record's metadata, stated ─────────────────────────────────────────────────────── */

.pt-about {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin: 0;
}

.pt-about-row {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.pt-about-row dt {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.4px;
    color: var(--text-muted);
}

.pt-about-row dd {
    margin: 0;
    font-size: var(--text-sm);
    overflow-wrap: break-word;
    color: var(--text);
}

/* The source URL is long and unbreakable; let it wrap anywhere rather than widen the panel. */
.pt-about-link {
    color: var(--link);
    overflow-wrap: anywhere;
}

.pt-about + .bb-reader-taxonomy {
    margin-block-start: 14px;
}

/* ── PT index (/barbruiser/pts) — browse by task type ──────────────────────────────────────── */

.pts-index {
    display: flex;
    flex-direction: column;
    gap: 22px;
}

.pts-intro {
    margin: 0;
    max-inline-size: 68ch;
    color: var(--text-muted);
    line-height: 1.6;
}

.pts-group-title {
    margin: 0 0 12px;
    font-size: var(--text-lg);
    font-weight: 700;
    color: var(--text);
}

.pts-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    gap: 14px;
}

.pts-card {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 16px;
    background: var(--surface-raised);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    text-decoration: none;
    transition: border-color 0.15s ease;
}

.pts-card:hover {
    border-color: var(--primary);
}

.pts-card:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

.pts-card-type {
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    color: var(--primary);
}

.pts-card-title {
    font-size: var(--text-lg);
    font-weight: 700;
    font-style: italic;
    color: var(--text);
}

.pts-card-exam {
    font-size: var(--text-sm);
    color: var(--text-muted);
}

.pts-card-skills {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 6px 0 0;
    padding: 0;
    list-style: none;
}

.pts-card-skill {
    padding: 2px 8px;
    font-size: var(--text-xs);
    color: var(--text-muted);
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius-full);
}

.pts-empty {
    margin: 0;
    color: var(--text-muted);
}

/* @toc Barbruiser page */
/* BarBruiser index — study sheets in two segments (Essays / Theory), each a responsive
 * grid of cards. Page CSS, bundled by CssService. The sheet renderers are the
 * study-sheet (essay) and theory-sheet components. */

.barbruiser-actions {
    margin-bottom: 8px;
}

.barbruiser-segment {
    margin-top: 22px;
}

.barbruiser-segment-title {
    margin: 0 0 12px;
    font-size: var(--text-lg);
    font-weight: 700;
    color: var(--text);
}

.barbruiser-segment-empty {
    margin: 0;
}

.barbruiser-index {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 14px;
}

.barbruiser-card {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 16px;
    background: var(--surface-raised);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    text-decoration: none;
    transition: border-color 0.15s ease;
}

.barbruiser-card:hover {
    border-color: var(--primary);
}

.barbruiser-card-subject {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.4px;
    color: var(--text-muted);
}

.barbruiser-card-title {
    font-weight: 600;
    color: var(--text);
}

/* ── Subject folders (Theory) — Daria's wish #1 ──────────────────────────────────────────────────
 * The disclosure itself is x-details: border, radius, chevron rotation, hover and the focus ring all
 * come from the primitive, so nothing here re-implements a control. These rules do only the two
 * things the primitive cannot know about this usage — how folders stack, and that the thing inside
 * is a card grid rather than prose.
 *
 * Both are set through the primitive's OWN --details-* theming surface rather than by overriding
 * .details-summary / .details-body from a page sheet, so the folder is themed the way the component
 * asks to be themed and keeps working if its internals are refactored. */
.barbruiser-folder {
    /* The subject IS this folder's name, so the summary should read as a heading, not as a control. */
    --details-summary-font-size: var(--text-base);
    /* The body holds CARDS, not prose: the primitive's smaller muted body type would shrink the
     * titles inside a folder and leave them looking different from the same cards in Essays. */
    --details-body-font-size: var(--text-base);
    --details-body-color: var(--text);
}

.barbruiser-folder + .barbruiser-folder {
    margin-block-start: var(--space-3);
}

/* The primitive pads its body for prose; a card grid brings its own rhythm, so the top gap would
 * otherwise read as a double space under the summary row. */
.barbruiser-folder > .details-body {
    padding-block-start: var(--space-1);
}

/* The create card — dashed to read as an action, not a sheet. */
.barbruiser-card-new {
    border-style: dashed;
    justify-content: center;
}

.barbruiser-card-new .barbruiser-card-title {
    color: var(--text-muted);
}

.barbruiser-card-new:hover .barbruiser-card-title {
    color: var(--text);
}

/* @toc Demo page */
/* Demo page — the living style guide layout. */

.demo {
    width: 100%;
    max-width: 760px;
}

.demo-sec {
    padding: 26px 0;
    border-bottom: 0.5px solid var(--line);
}

.demo-sec:last-child {
    border-bottom: none;
}

.demo-h {
    margin: 0 0 16px;
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-weight: 500;
    letter-spacing: 0.04em;
}

.demo-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 12px;
}

.demo-narrow {
    max-width: 360px;
}

.demo-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 16px;
}

.demo-mono {
    font-family: var(--font-mono);
    color: var(--text-muted);
}

/* @toc Docs page */
/* Documentation pages (/docs/<page>) — Palette ramps, the Typography scale, and
 * the tabbed component docs. Each page's body is resources/views/docs/<key>.blade.php.
 * Code blocks are the x-code-block component (see code-block.css). */

/* Palette showcase — the three reference forms, the live active ramps, and the
 * full catalog as rows of click-to-copy chips. */
.palette-ref {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: 12px;
    margin: 0 0 32px;
}

.palette-ref-item {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px 10px;
    padding: 14px 16px;
    border: 1px solid var(--line);
    border-radius: 12px;
    background: var(--surface);
}

.palette-ref-swatch {
    width: 28px;
    height: 28px;
    flex: none;
    border-radius: 7px;
    border: 1px solid var(--line);
}

.palette-ref-item code {
    font-size: var(--text-sm);
}

.palette-ref-note {
    flex-basis: 100%;
    color: var(--text-muted);
    font-size: var(--text-xs);
    line-height: 1.5;
}

.palette-active,
.palette-catalog {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.palette-active {
    margin-bottom: 32px;
}

.palette-active-row {
    display: flex;
    align-items: center;
    gap: 12px;
}

.palette-active-label {
    flex: none;
    width: 84px;
    text-align: right;
    font-size: var(--text-sm);
    color: var(--text-muted);
    text-transform: capitalize;
}

.palette-ramp {
    display: flex;
    flex: 1;
    min-width: 0;
    gap: 4px;
}

/* The palette chips are <x-swatch> (#761) — the chip's look, hover, focus ring and copied confirmation all
   live in resources/css/components/swatch.css now. The two rules that used to sit here carried the only colour
   literals on this page (a hard-coded #fff + rgba text-shadow, needed to keep a step number legible over an
   arbitrary fill); the component drops the over-the-fill label entirely, so those literals are simply gone
   rather than relocated. */

@media (max-width: 768px) {
    .palette-active-label { width: 60px; }
}

/* Click-to-copy button on code blocks, and the copy toast (palette chips + code). */
.doc-copy-btn {
    position: absolute;
    top: 8px;
    right: 8px;
    padding: 3px 10px;
    border: 1px solid var(--line);
    border-radius: 6px;
    background: var(--surface);
    color: var(--text-muted);
    font-size: var(--text-xs);
    cursor: pointer;
    opacity: 0;
    transition: opacity 120ms ease, color 120ms ease, border-color 120ms ease;
}

.code-block:hover .doc-copy-btn,
.doc-copy-btn:focus-visible {
    opacity: 1;
}

.doc-copy-btn:hover {
    color: var(--text);
    border-color: var(--text-muted);
}

.doc-copy-toast {
    position: fixed;
    left: 50%;
    bottom: 24px;
    transform: translate(-50%, 10px);
    padding: 8px 16px;
    border-radius: 999px;
    background: var(--text);
    color: var(--surface);
    font-size: var(--text-sm);
    opacity: 0;
    pointer-events: none;
    transition: opacity 150ms ease, transform 150ms ease;
    z-index: 1000;
}

.doc-copy-toast.is-visible {
    opacity: 1;
    transform: translate(-50%, 0);
}

/* Typography: a sample per scale step with a mono spec line under it. */
/* Typography showcase — family cards over a live type-scale specimen. */
.type-families {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 12px;
    margin: 0 0 32px;
}

.type-family {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 16px 18px;
    border: 1px solid var(--line);
    border-radius: 12px;
    background: var(--surface);
}

.type-family-sample {
    flex: none;
    font-size: 40px;
    line-height: 1;
    color: var(--text);
}

.type-family-meta {
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-width: 0;
}

.type-family-meta code {
    cursor: pointer;
}

.type-family-meta span {
    font-size: var(--text-xs);
    color: var(--text-muted);
    font-family: var(--font-mono);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.type-scale-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 24px;
    padding: 14px 0;
    border-bottom: 1px solid var(--line);
}

.type-scale-row:last-child {
    border-bottom: none;
}

.type-scale-sample {
    margin: 0;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.type-scale-meta {
    flex: none;
    font-size: var(--text-xs);
    color: var(--text-muted);
    font-family: var(--font-mono);
    white-space: nowrap;
}

/* Component doc: each component page renders inside an x-tabs (Example / View /
 * CSS / JS); these are the panel-content styles. */
.doc-example {
    padding: 0 0 28px;
}

.doc-example-label {
    margin: 0 0 10px;
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
}

/* The example "canvas" — a raised, padded surface so a live demo reads as a
 * deliberate preview, not markup dropped on the page. */
.doc-example-demo {
    padding: 28px;
    border: 1px solid var(--line);
    border-radius: 12px;
    background: var(--surface-raised);
}

/* A section head. docs._subhead now renders these as a real <h2> (it is the page's section outline, and the
   TOC rail links to it), so the weight is pinned here rather than inherited from the UA's bold h2 default —
   the look is unchanged from when this was a <div>. The one-<h1> law is untouched: the page's h1 is in the
   app header bar. scroll-margin keeps a jumped-to head clear of the sticky app chrome. */
.doc-subhead {
    margin: 0 0 8px;
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
    scroll-margin-top: var(--space-6);
}

.doc-subhead:not(:first-child) {
    margin-top: 20px;
}

/* ── "On this page" (#846) — the docs page splits into body + a TOC rail ───────────────────────────────── */

/* The rail is LAST in the DOM (the stack can only be complete after the content renders) and CSS puts it
   alongside. Order matters for narrow widths too: it stacks UNDER the content rather than gating it. */
.doc-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 12rem;
    gap: var(--space-6);
    align-items: start;
}

/* A page whose sections pushed nothing (a short prose page) gets NO empty rail — one column, honestly. */
.doc-layout:not(:has(.doc-toc-item)) {
    grid-template-columns: minmax(0, 1fr);
}

.doc-layout:not(:has(.doc-toc-item)) .doc-toc {
    display: none;
}

.doc-body {
    min-width: 0;
}

.doc-toc {
    position: sticky;
    top: var(--space-4);
    min-width: 0;
    padding-inline-start: var(--space-4);
    border-inline-start: 1px solid var(--line);
}

.doc-toc-title {
    margin: 0 0 var(--space-2);
}

.doc-toc-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    margin: 0;
    padding: 0;
    list-style: none;
}

.doc-toc-link {
    display: block;
    color: var(--text-muted);
    font-size: var(--text-sm);
    line-height: 1.35;
    text-decoration: none;
}

.doc-toc-link:hover {
    color: var(--text);
}

.doc-toc-link:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

/* Narrow: one column, the rail reads as a plain jump list under the content (no sticky, no rule). */
@media (max-width: 768px) {
    .doc-layout {
        grid-template-columns: minmax(0, 1fr);
    }

    .doc-toc {
        position: static;
        padding-inline-start: 0;
        border-inline-start: 0;
        border-block-start: 1px solid var(--line);
        padding-block-start: var(--space-4);
    }
}

.doc-empty {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--text-sm);
}

/* Intro paragraph above a component doc's tab strip (meta.description). */
.doc-intro {
    margin: 0 0 20px;
    max-width: 68ch;
    color: var(--text-muted);
    font-size: var(--text-sm);
    line-height: 1.6;
}

/* A one-line lead-in inside a tab panel (e.g. the HTML tab's "compiles to"). */
.doc-lead {
    margin: 0 0 12px;
    color: var(--text-muted);
    font-size: var(--text-sm);
}

/* Section accents — one catalog hue per section, used by the home cards, the
 * per-page eyebrow badge, and card hovers. Catalog colours are theme-independent. */
.docs-accent-foundations { --section: var(--palette-emerald-500); }
.docs-accent-primitives  { --section: var(--palette-violet-500); }
.docs-accent-patterns    { --section: var(--palette-amber-500); }
.docs-accent-sections    { --section: var(--palette-rose-500); }
.docs-accent-internals   { --section: var(--palette-sky-500); }

/* /docs home — the section-card front door. */
.docs-home-lead {
    margin: 0 0 24px;
    color: var(--text-muted);
    font-size: var(--text-base);
}

.docs-home-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: 16px;
}

.docs-home-card {
    display: block;
    padding: 20px;
    border: 1px solid var(--line);
    border-radius: 14px;
    background: var(--surface);
    color: var(--text);
    text-decoration: none;
    transition: border-color 0.15s ease;
}

.docs-home-card:hover {
    border-color: var(--section);
}

.docs-home-card-head {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 14px;
}

.docs-home-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border-radius: 10px;
    background: var(--surface-raised);
    color: var(--section);
}

.docs-home-icon svg {
    width: 24px;
    height: 24px;
}

.docs-home-card-title {
    font-size: var(--text-lg);
    font-weight: 600;
    color: var(--text);
}

.docs-home-card-count {
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.docs-home-blurb {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--text-sm);
    line-height: 1.6;
}

/* Per-page section eyebrow badge. */
.doc-eyebrow {
    margin: 0 0 20px;
}

.doc-eyebrow-badge {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 3px 11px 3px 8px;
    border: 1px solid var(--section);
    border-radius: 999px;
    color: var(--section);
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.doc-eyebrow-icon {
    display: inline-flex;
}

.doc-eyebrow-icon svg {
    width: 14px;
    height: 14px;
}

/* Provenance — "based on the <button> element", grounding a primitive in MDN. */
.doc-basedon {
    margin: -8px 0 20px;
    font-size: var(--text-sm);
    color: var(--text-muted);
}

.doc-basedon code {
    color: var(--text);
}

.doc-basedon-link {
    white-space: nowrap;
}

/* "Composed of" — a Pattern's Primitive ingredients, derived from its Blade source. */
.doc-composed {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    margin: 0 0 24px;
}

.doc-composed-label {
    font-size: var(--text-xs);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
}

.doc-composed-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 4px 10px;
    border: 1px solid var(--line);
    border-radius: 999px;
    font-size: var(--text-sm);
    color: var(--text);
    text-decoration: none;
}

.doc-composed-chip:hover {
    border-color: var(--text-muted);
    background: var(--surface-raised);
}

.doc-composed-count {
    color: var(--text-muted);
    font-size: var(--text-xs);
}

/* Props/attributes table at the top of a component doc. */
.doc-props {
    width: 100%;
    margin: 0 0 24px;
    border-collapse: collapse;
    font-size: var(--text-sm);
}

.doc-props th {
    /* label typography now rides the shared .ui-label recipe (R5-B, one-place-reversible) */
    padding: 8px 12px;
    border-bottom: 1px solid var(--line);
    text-align: left;
}

.doc-props td {
    padding: 8px 12px;
    border-bottom: 1px solid var(--line);
    vertical-align: top;
}

.doc-props td:nth-child(4) {
    color: var(--text-muted);
}

.doc-props-req {
    color: var(--primary-500);
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* Per-component accessibility notes, below the tabs. */
.doc-a11y {
    margin-top: 28px;
}

.doc-a11y-list {
    margin: 0 0 10px;
    padding-left: 20px;
}

.doc-a11y-list li {
    margin-bottom: 6px;
    font-size: var(--text-sm);
    line-height: 1.6;
}

.doc-a11y-link {
    font-size: var(--text-sm);
}

/* Prev/next pager at the foot of every doc page. */
.doc-pager {
    display: flex;
    justify-content: space-between;
    gap: 16px;
    margin-top: 48px;
    padding-top: 20px;
    border-top: 1px solid var(--line);
}

.doc-pager-link {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 12px 16px;
    border: 1px solid var(--line);
    border-radius: 10px;
    text-decoration: none;
    max-width: 48%;
}

.doc-pager-next {
    margin-left: auto;
    text-align: right;
}

.doc-pager-link:hover {
    border-color: var(--text-muted);
    background: var(--surface-raised);
}

.doc-pager-dir {
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.doc-pager-label {
    color: var(--text);
    font-weight: 500;
}

/* The sibling's own description (#846) — clamped to two lines so a long one can't unbalance the two cards. */
.doc-pager-blurb {
    display: -webkit-box;
    overflow: hidden;
    margin-block-start: 2px;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    color: var(--text-muted);
    font-size: var(--text-xs);
    line-height: 1.4;
}

/* Internals reference — sectioned services/models/routes map. */
.doc-internals {
    max-width: 80ch;
}

.doc-internals-head {
    margin: 32px 0 4px;
    font-size: var(--text-lg);
}

.doc-internals-head span {
    margin-left: 8px;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: 400;
    color: var(--text-muted);
}

.doc-internals-list {
    margin: 0 0 8px;
    padding-left: 20px;
}

.doc-internals-list li {
    margin-bottom: 10px;
    line-height: 1.6;
}

/* ── Markup Element Ledger (the /docs/markup census page) ─────────────────────────────────
   Reusable so a future JS/CSS census page adopts the same shell. Colours from the ramp + semantic
   tokens only; badge/dot tints via color-mix (decoration alpha, sanctioned). */
.ledger-accounting {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 18px;
    margin: 20px 0 8px;
    padding: 14px 18px;
    border: 1px solid var(--line);
    border-radius: 12px;
    background: var(--surface-raised);
    font-size: var(--text-sm);
    color: var(--text-muted);
}

.ledger-stat strong {
    color: var(--text);
    font-size: var(--text-base);
}

.ledger-stat-clean {
    color: var(--primary);
    font-weight: 600;
}

.ledger-filters {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px 14px;
    position: sticky;
    top: 0;
    z-index: 1;
    padding: 12px 0;
    background: var(--page-bg);
}

.ledger-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

.ledger-chip {
    padding: 4px 12px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--surface);
    color: var(--text-muted);
    font-size: var(--text-sm);
    cursor: pointer;
}

.ledger-chip:hover {
    border-color: var(--line-strong);
    color: var(--text);
}

.ledger-chip.is-active {
    border-color: var(--primary);
    background: color-mix(in oklab, var(--primary) 14%, transparent);
    color: var(--primary);
    font-weight: 600;
}

.ledger-search {
    flex: 1 1 220px;
    max-width: 320px;
    padding: 6px 12px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: var(--surface);
    color: var(--text);
    font-size: var(--text-sm);
}

.ledger-count {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--text-muted);
}

.ledger-cat {
    margin-top: 28px;
}

.ledger-cat-head {
    display: flex;
    align-items: baseline;
    gap: 10px;
    margin-bottom: 10px;
}

.ledger-cat-n {
    font-size: var(--text-sm);
    font-weight: 400;
    color: var(--text-muted);
}

/* Disposition badges — one accent per state, all ramp-lawful. */
.ledger-badge {
    display: inline-block;
    padding: 1px 8px;
    border-radius: 999px;
    border: 1px solid var(--line);
    font-size: var(--text-xs);
    font-weight: 600;
    white-space: nowrap;
    text-transform: lowercase;
    color: var(--text-muted);
}

.ledger-badge-covered {
    color: var(--primary);
    border-color: color-mix(in oklab, var(--primary) 40%, transparent);
    background: color-mix(in oklab, var(--primary) 12%, transparent);
}

.ledger-badge-deferred {
    color: var(--primary);
    border-color: color-mix(in oklab, var(--primary) 40%, transparent);
}

.ledger-badge-watch {
    color: var(--text-danger);
    border-color: color-mix(in oklab, var(--text-danger) 40%, transparent);
}

.ledger-badge-raw-ok {
    color: var(--text);
    border-color: var(--line-strong);
}

.ledger-badge-not-used {
    color: var(--text-muted);
    border-style: dashed;
}

/* Baseline tiers (the CSS census, #900) — the cross-browser-safety layer, on the categorical status
   tokens: widely = success, newly = info, limited = warning. Text on its own -surface tint is the
   AA-vetted-by-construction pairing (a color-mix alpha tint is NOT vetted and drops green/amber below AA
   in light — measured). The border is the token at 35% over the surface, a 3:1 UI edge. Not lowercased —
   these are proper labels ("Widely available"), so the badge opts out of the base text-transform. */
.ledger-baseline-widely,
.ledger-baseline-newly,
.ledger-baseline-limited {
    text-transform: none;
}

.ledger-baseline-widely {
    color: var(--success);
    background: var(--success-surface);
    border-color: color-mix(in oklab, var(--success) 35%, var(--success-surface));
}

.ledger-baseline-newly {
    color: var(--info);
    background: var(--info-surface);
    border-color: color-mix(in oklab, var(--info) 35%, var(--info-surface));
}

.ledger-baseline-limited {
    color: var(--warning);
    background: var(--warning-surface);
    border-color: color-mix(in oklab, var(--warning) 35%, var(--warning-surface));
}

.ledger-type,
.ledger-guidance {
    font-size: var(--text-sm);
    color: var(--text-muted);
}

.ledger-guidance {
    display: block;
    max-width: 46ch;
    line-height: 1.5;
}

.ledger-muted {
    color: var(--text-muted);
}

/* Link-health dot — brand for reachable, danger for broken, muted-hollow for unchecked. */
.ledger-dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    vertical-align: middle;
    background: var(--primary);
}

.ledger-dot-bad {
    background: var(--text-danger);
}

.ledger-dot-unknown {
    background: transparent;
    border: 1px solid var(--line-strong);
}

.ledger-health {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 12px;
    margin-top: 28px;
    padding-top: 16px;
    border-top: 1px solid var(--line);
    font-size: var(--text-sm);
    color: var(--text-muted);
}

.ledger-health-form {
    margin-left: auto;
}

/* ── Typography ROLES + self-hosted families (#1247) ──────────────────────────────────────────────
 * Roles stack rather than sit in a grid: each carries a sentence of rule, and the rule is the content —
 * a card grid would crop exactly the part that stops someone setting body copy in a display face. */
.type-roles {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin: 0 0 32px;
}

.type-role {
    padding: 16px 18px;
    border: 1px solid var(--line);
    border-radius: 12px;
    background: var(--surface);
}

/* The specimen is the point of the row, so it leads at a size where the face is actually legible. */
.type-role-sample {
    margin: 0 0 8px;
    font-size: 28px;
    line-height: 1.2;
    color: var(--text);
}

.type-role-name {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 8px;
    margin: 0 0 4px;
}

.type-role-label {
    font-weight: 600;
    color: var(--text);
}

.type-role-group,
.type-font-role {
    padding: 1px 8px;
    font-size: var(--text-xs);
    color: var(--text-muted);
    background: var(--surface-raised);
    border: 1px solid var(--line);
    border-radius: var(--radius-full);
}

.type-role-desc {
    margin: 0;
    max-width: 68ch;
    font-size: var(--text-sm);
    line-height: 1.55;
    color: var(--text-muted);
}

.type-fonts {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: 12px;
    margin: 0 0 32px;
}

.type-font {
    padding: 16px 18px;
    border: 1px solid var(--line);
    border-radius: 12px;
    background: var(--surface);
}

.type-font-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 8px;
    margin: 0 0 8px;
    color: var(--text);
}

/* A family with no role is a real state (Satoshi ships without one), so it reads as a stated fact rather
   than an error — muted and italic, not a warning colour. */
.type-font-role-none {
    font-style: italic;
}

.type-font-files {
    margin: 0 0 8px;
    padding-inline-start: 18px;
    font-size: var(--text-xs);
    line-height: 1.7;
    color: var(--text-muted);
}

.type-font-var {
    font-weight: 600;
    color: var(--text);
}

.type-font-meta {
    margin: 0;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.type-font-nolicence {
    font-style: italic;
}

/* @toc Drafts page */
/* Drafts — the design render seam (F1). Two surfaces: the admin index/form (inside the app shell) and the
 * standalone preview's slim review bar. Tokens only — the preview links the LIVE sheet, so these read the
 * same ramp every other page does. */

/* ── The preview review bar (drafts/show) ──────────────────────────────────────────────────────────────
 * A slim, in-flow bar above the canvas (never fixed — it must not overlap the rendered draft). It is chrome,
 * so it stays quiet: surface background, a hairline rule, muted text. */
.draft-bar {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 8px 16px;
    background: var(--surface);
    border-bottom: 1px solid var(--line);
    font-size: var(--text-sm);
}

.draft-bar svg {
    width: 15px;
    height: 15px;
}

.draft-bar-back,
.draft-bar-edit {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    color: var(--text-muted);
    text-decoration: none;
    font-weight: 600;
    transition: color 120ms ease;
}

.draft-bar-back:hover,
.draft-bar-edit:hover {
    color: var(--text);
}

.draft-bar-name {
    font-weight: 600;
    color: var(--text);
}

.draft-bar-edit {
    margin-inline-start: auto;
}

.draft-bar-themes {
    display: inline-flex;
    gap: 2px;
    padding: 2px;
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
}

.draft-bar-theme {
    padding: 2px 10px;
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    text-decoration: none;
    font-weight: 600;
    transition: background 120ms ease, color 120ms ease;
}

.draft-bar-theme:hover {
    color: var(--text);
}

.draft-bar-theme.is-active {
    background: color-mix(in srgb, var(--primary) 16%, transparent);
    color: var(--primary);
}

/* The canvas — room around the rendered draft so components aren't jammed to the edge. */
.draft-canvas {
    padding: 24px;
}

/* ── The admin index (drafts/index) ────────────────────────────────────────────────────────────────────*/
.draft-index-lead,
.draft-form-lead {
    max-width: 72ch;
    color: var(--text-muted);
    font-size: var(--text-sm);
    line-height: 1.6;
    margin-bottom: 16px;
}

.draft-index-lead code,
.draft-form-lead code {
    font-family: var(--font-mono);
    font-size: 0.9em;
    color: var(--text);
}

.draft-index-actions {
    margin-bottom: 20px;
}

.draft-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    overflow: hidden;
}

.draft-list-item {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 12px 16px;
    background: var(--surface);
    transition: background 100ms ease;
}

.draft-list-item:hover {
    background: color-mix(in srgb, var(--text) 4%, var(--surface));
}

.draft-list-name {
    font-weight: 600;
    color: var(--text);
    text-decoration: none;
}

.draft-list-name:hover {
    color: var(--primary);
}

.draft-list-meta {
    font-size: var(--text-xs);
    color: var(--text-muted);
    margin-inline-start: auto;
}

.draft-list-edit {
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--text-muted);
    text-decoration: none;
}

.draft-list-edit:hover {
    color: var(--text);
}

/* ── The minimal form (drafts/form) ────────────────────────────────────────────────────────────────────*/
.draft-form-body {
    display: flex;
    flex-direction: column;
    gap: 18px;
    max-width: 720px;
}

.draft-form-actions {
    display: flex;
    align-items: center;
    gap: 16px;
}

.draft-form-cancel {
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--text-muted);
    text-decoration: none;
}

.draft-form-cancel:hover {
    color: var(--text);
}

/* @toc Editor page */
/*
 * The Editor workspace — a FULL-BLEED, FLAT, Figma-grade 3-column workspace (structure | preview | tools), NOT a
 * scrolling page and NOT card-per-region panels (1209, James's direction via the designer). The app-shell runs
 * `:flush` so this fills the main edge-to-edge; the regions meet at 1px `--line` HAIRLINES — no gaps, no radius,
 * no shadows, no per-panel backgrounds. The ONE element that reads as a raised object is the device frame in the
 * preview; flat chrome everywhere else is what makes it pop. It fills the app-shell main's viewport height
 * (height:100%) and each column scrolls on its OWN (overflow:auto; min-height:0). At/below --bp-md the grid
 * collapses to one column and the page scrolls again (full phone-width EDITING is a later pass). Semantic tokens.
 */

.editor-frame {
    display: flex;     /* left rail | preview (flexes) | right rail — flex, so a COLLAPSED rail yields its room (1210) */
    align-items: stretch;
    height: 100%;      /* fill the app-shell main — the workspace, never a scrolling page */
    min-height: 0;     /* let the columns bound their own scroll */
}

/* A RAIL (1210) — a dock of panels. Fixed width, scrolls its own overflow, and a 1px hairline where it meets the
   preview (the flat-workspace division). An EMPTY rail COLLAPSES to nothing so the preview takes the room. */
.editor-rail {
    display: flex;
    flex-direction: column;
    flex: 0 0 320px;
    min-width: 0;
    min-height: 0;
    overflow: auto;
}

.editor-rail[data-editor-rail="left"]:not(.is-empty) {
    border-inline-end: 1px solid var(--line);
}

.editor-rail[data-editor-rail="right"]:not(.is-empty) {
    border-inline-start: 1px solid var(--line);
}

.editor-rail.is-empty {
    flex-basis: 0;
    overflow: hidden;
}

/* The PREVIEW region — FIXED center, flexes to fill between the rails. A FLAT column (no border/radius/shadow/bg);
   the device frame is the sole raised object (1209). */
.editor-region {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-width: 0;
    min-height: 0;
    overflow: auto;
    padding: var(--space-4);
}

/* ── a dockable PANEL (1210) — a flat headed unit; panels within a rail meet at a hairline ─────────────────── */
.editor-panel {
    display: flex;
    flex-direction: column;
    min-height: 0;
    padding: var(--space-4);
}

/* 1298: a rail's non-collapsible panels (Navigator, Layers) SHARE its bounded height and each
   scrolls its OWN body — so the Navigator's whole inventory (35 primitives, all sections open)
   scrolls inside the Navigator instead of overflowing visibly and painting over the Layers panel
   below it (which made Layers unclickable). Equal flex shares the rail; min-height:0 lets a panel
   shrink below its content; the panel body is the scroll box. Collapsible panels (the right rail)
   keep their content-height fold and are excluded. */
.editor-panel:not([data-panel-collapsible]) {
    flex: 1 1 0;
}

.editor-panel:not([data-panel-collapsible]) > .panel-body {
    flex: 1 1 0;
    min-height: 0;
    overflow-y: auto;
}

.editor-panel + .editor-panel {
    border-block-start: 1px solid var(--line);
}

/* 1219: the panel header is now x-panel's own `.panel-head` (grip + Move ride its right-aligned `.panel-actions`
   seam; the optional collapse toggle rides its left). The grip keeps its drag-handle affordance wherever it sits. */
.editor-panel-grip {
    flex-shrink: 0;
    color: var(--text-muted);
    cursor: grab;
}

.editor-panel-grip:active {
    cursor: grabbing;
}

/* SortableJS's placeholder for the panel being dragged — dimmed, so the rail keeps its height while dragging. */
.editor-panel-ghost {
    opacity: 0.4;
}

/* A panel heading — the shared .ui-label recipe (a word) over a thin rule, never a titled card. A heading that
   is not the first in its column gets top space so stacked sub-panels (Site / Pages / Layers) read apart. */
.editor-panel-label {
    margin: 0 0 var(--space-2);
    padding-block-end: var(--space-2);
    border-block-end: 1px solid var(--line);
}

.editor-panel-label:not(:first-child) {
    margin-block-start: var(--space-4);
}

/* The preview's flat header — the panel label (its own row) over a row of [target-identity strip · Reset layout].
   The Reset control (1210) sits at the row end, hidden by editor-dock.js until a stored layout exists. */
.editor-preview-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-2);
    flex-shrink: 0;
}

.editor-preview-head .editor-panel-label {
    flex-basis: 100%;
}

.editor-reset-layout {
    flex-shrink: 0;
}

/* The target identity strip (tenant · section/page · theme axes) beside the preview title. */
.editor-target {
    margin: 0 0 var(--space-2);
    color: var(--text-muted);
    font-size: var(--text-sm);
}

.editor-target-tenant,
.editor-target-page {
    color: var(--text);
}

.editor-target-sep {
    margin-inline: var(--space-1);
}

/* #1226 Navigator: one x-tabs body inside the dockable panel. The panel itself is the container boundary, so
   custom-width rails yield the hint before any label and only the tablist scrolls below the compact four-up fit. */
/* #1293: ONE panel of stacked sections, not a tabset — the whole hierarchy visible at once (an IDE project-
   explorer). The LEFT RAIL already owns the scroll (it is overflow:auto), so the navigator does NOT nest a
   second scroll — a long section is tamed by COLLAPSING it, not by an inner scrollbar. container-type keeps the
   rows responsive to the panel's own width (a rail the user narrows), never the viewport. */
.editor-navigator {
    min-width: 0;
    container-type: inline-size;
}

/* Each category is a collapsible <x-details>, default OPEN so nothing is hidden on load; the author collapses
   Primitives (35 rows) when it is in the way. Strip the primitive's card chrome to the navigator's tight
   rhythm and separate sections with a hairline. */
.editor-navigator-section.details {
    border: 0;
    border-radius: 0;
    background: transparent;
    /* 1298: the base .details clips with overflow:hidden (for its rounded corners) — but these sections have
       no border/radius, and that clip would trap the sticky section label inside its own short section instead
       of letting it stick against the scrolling panel body. Visible restores the sticky (label below). */
    overflow: visible;
}

.editor-navigator-section + .editor-navigator-section {
    margin-block-start: var(--space-2);
    border-block-start: 1px solid var(--line);
}

/* The section label reuses the group-label idiom — small, muted, uppercase ui-label voice — over the native
   disclosure chevron (which rotates on [open]). Compact padding keeps four labels legible without a tab bar. */
.editor-navigator-section > .details-summary {
    gap: var(--space-1);
    padding: var(--space-2) var(--space-1);
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    /* 1298: sticky within the scrolling Navigator body — a long section (Primitives is 35 rows) stays oriented
       as you scroll past it, the label pinned to the top until the next section pushes it up. The page-bg fill
       (what the transparent Navigator sits on) keeps scrolled rows from showing through the label. */
    position: sticky;
    inset-block-start: 0;
    z-index: 1;
    background: var(--page-bg);
}

.editor-navigator-section > .details-body {
    padding: var(--space-1) 0 0;
    color: var(--text);
}

.editor-navigator-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 0;
}

/* One flat row language: decorative 16px icon → durable label → yielding mono hint → optional state. Links and
   buttons keep their native keyboard activation; no second roving/listbox model is introduced. */
.editor-navigator-row.button,
.editor-navigator-row.link {
    position: relative;
    display: flex;
    align-items: center;
    width: 100%;
    min-width: 0;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border: 0;
    border-radius: var(--radius-control);
    background: transparent;
    color: var(--text);
    font-size: var(--text-sm);
    font-weight: 500;
    line-height: 1.25;
    text-align: start;
    text-decoration: none;
}

.editor-navigator-row.link:visited {
    color: var(--text);
}

.editor-navigator-row:hover {
    background: var(--surface-raised);
    color: var(--text);
}

.editor-navigator-row:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

.editor-navigator-row.is-selected {
    background: color-mix(in srgb, var(--primary) 8%, transparent);
}

.editor-navigator-row.is-selected::before {
    position: absolute;
    inset-block: var(--space-1);
    inset-inline-start: 0;
    width: 2px;
    border-radius: var(--radius-control);
    background: var(--primary);
    content: "";
}

.editor-navigator-icon {
    display: inline-flex;
    flex: 0 0 auto;
    color: var(--text-muted);
}

.editor-navigator-icon svg {
    display: block;
    width: 1rem;
    height: 1rem;
}

.editor-navigator-label {
    flex: 0 1 auto;
    min-width: 0;
}

.editor-navigator-hint {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: 400;
    text-align: end;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.editor-navigator-status {
    width: 0.5rem;
    height: 0.5rem;
    flex: 0 0 auto;
    border-radius: 50%;
    background: var(--text-muted);
}

.editor-navigator-row[data-status="draft"] .editor-navigator-status {
    background: var(--warning);
}

.editor-navigator-row[data-status="published"] .editor-navigator-status {
    background: var(--success);
}

.editor-navigator-chip {
    flex: 0 0 auto;
    padding: var(--space-1);
    border: 1px solid var(--line);
    border-radius: var(--radius-control);
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: 400;
}

.editor-navigator-chrome {
    margin-block-start: var(--space-3);
    padding-block-start: var(--space-3);
    border-block-start: 1px solid var(--line);
}

.editor-navigator-group-label {
    margin: 0 0 var(--space-1);
}

.editor-navigator-empty {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--text-sm);
}

@container (max-width: 18rem) {
    .editor-navigator-tabs > .tabs-nav {
        grid-template-columns: none;
        grid-auto-columns: minmax(4.5rem, 1fr);
        grid-auto-flow: column;
        overflow-x: auto;
    }

    .editor-navigator-hint {
        display: none;
    }
}

/* The preview mount centers the mobile-first device frame in the (taller) preview region; the frame owns
   its own dimensions, so the mount just gives it room + overflow to scroll if the frame exceeds the pane. */
.editor-preview-mount {
    display: flex;
    flex: 1;
    align-items: flex-start;
    justify-content: center;
    overflow: auto;
    padding-block: var(--space-2);
}

/* ── the block canvas: the layers list ─────────────────────────────────────────────────────────────
   The DRAG SURFACE (editor-drag.js binds SortableJS here) + the palette-drag drop target. Now the body of the
   dockable Layers panel (1210); its rail scrolls, so the list flows at natural height. The iframe shows what the
   stack LOOKS like; this shows what it IS — and it's the half you can grab (SortableJS cannot reach the iframe). */
.editor-layers {
    min-height: 0;
}

.editor-layers-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    margin: 0;
    padding: 0;
    list-style: none;
}

.editor-layer {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--line);
    border-radius: var(--global-border-radius);
    background: var(--surface);
    font-size: var(--text-sm);
    color: var(--text);
}

/* Inline heroicons in the editor chrome (1206) — the layer grip/reorder + the repeater reorder. The heroicon
   SVGs carry a Tailwind size class we don't use, so size them here (the details-marker convention). */
.editor-icon {
    display: inline-flex;
}

.editor-icon svg {
    display: block;
    width: 1rem;
    height: 1rem;
}

/* The grip is the drag handle — editor-drag.js binds the reorder gesture to it — so the grab cursor is here. */
.editor-layer-grip {
    line-height: 1;
    color: var(--text-muted);
    cursor: grab;
}

.editor-layer-grip:active {
    cursor: grabbing;
}

/* The keyboard reorder controls (WCAG 2.1.1 / 2.5.7 — the non-drag path). Compact ghost icon buttons pinned to
   the row end; the drag grip stays the pointer affordance. Server-rendered as x-button + minted identically by
   renderLayers, so the .button base plus this compaction apply to both. */
.editor-layer-move-group {
    display: inline-flex;
    flex-shrink: 0;
    gap: var(--space-1);
}

.editor-layer-move {
    min-width: 1.75rem;
    padding-block: var(--space-1);
    padding-inline: var(--space-1);
    line-height: 1;
}

/* SortableJS's placeholder for the row being dragged — dimmed rather than hidden, so the list keeps its
   height and the drop target doesn't jump under the cursor. */
.editor-layer-ghost {
    opacity: 0.4;
}

.editor-layers-empty {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--text-muted);
}

/* ── the Theme tab groups (Color · Typography) ─────────────────────────────────────────────────────
   In the Theme tab (1209), Color is FIRST (no top rule); Typography follows, ruled off by a hairline. */
.editor-tools-typography {
    margin-block-start: var(--space-4);
    border-block-start: 1px solid var(--line);
    padding-block-start: var(--space-3);
}

/* Typography is honest-empty (C3): a one-line reason, no controls, until per-page type controls land. */
.editor-tools-empty-note {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--text-muted);
}

/* The preview's honest fallback (a data-driven target can't render from config) — its own standalone doc,
   centered through the live sheet so it sits in the device frame like any preview. */
.editor-unavailable {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-3);
    min-height: 100vh;
    padding: var(--space-6);
    text-align: center;
    color: var(--text);
}

.editor-unavailable-icon {
    display: inline-flex;
    width: 2.5rem;
    height: 2.5rem;
    margin: 0;
    color: var(--text-muted);
}

.editor-unavailable-title {
    margin: 0;
    font-size: var(--text-lg);
}

.editor-unavailable-body {
    margin: 0;
    max-width: 42ch;
    color: var(--text-muted);
}

/* The bare /editor's tenant-scoped empty state (#1251) — a registered tenant with no editable pages yet. App
   chrome, not the iframe fallback above, so it centers inside the card rather than the viewport. */
.editor-empty-site {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-3);
    padding-block: var(--space-8);
    text-align: center;
}

.editor-empty-site-icon {
    display: inline-flex;
    width: 2.5rem;
    height: 2.5rem;
    margin: 0;
    color: var(--text-muted);
}

.editor-empty-site-title {
    margin: 0;
    font-size: var(--text-lg);
}

.editor-empty-site-body {
    margin: 0;
    max-width: 52ch;
    color: var(--text-muted);
}

/* Mobile boundary: the three columns stack (structure, preview, tools) and the PAGE scrolls again (a phone can't
   hold full-height columns). Each region drops its internal scroll so its content flows into the page, and the
   column hairline becomes a horizontal rule between stacked regions. */
@media (max-width: 768px) {
    .editor-frame {
        flex-direction: column;   /* stack: left rail · preview · right rail — the page scrolls again */
        height: auto;
    }

    .editor-rail,
    .editor-region {
        flex: none;
        overflow: visible;
    }

    /* 1298: the desktop panel-share-and-internal-scroll needs a BOUNDED rail. Stacked, the rail is unbounded
       (flex:none) and the PAGE scrolls, so each panel takes its natural height and its body must not trap a
       second scroll — undo the desktop share here so the Navigator's sections flow into the page like before. */
    .editor-panel:not([data-panel-collapsible]),
    .editor-panel:not([data-panel-collapsible]) > .panel-body {
        flex: none;
    }

    .editor-panel:not([data-panel-collapsible]) > .panel-body {
        overflow-y: visible;
    }

    /* Stacked: the side hairlines become TOP hairlines between sections; the first section (left rail) has none. */
    .editor-rail[data-editor-rail="left"]:not(.is-empty) {
        border-inline-end: 0;
    }

    .editor-rail[data-editor-rail="right"]:not(.is-empty) {
        border-inline-start: 0;
        border-block-start: 1px solid var(--line);
    }

    .editor-region {
        border-block-start: 1px solid var(--line);
    }

    .editor-preview {
        min-height: 50vh;
    }

    /* THE DOCK IS A DESKTOP CONCEPT (1213). Stacked, there are no rails, so "move to left/right rail" names
       nothing — the Move menu is retired here.

       The GRIP goes with it, and that pairing is load-bearing rather than tidiness: the Move menu is the
       keyboard path that exists BECAUSE dragging the grip is pointer-only (WCAG 2.1.1 / 2.5.7). Hiding the menu
       alone would leave drag as the only way to reorder a panel and turn a compliant pair into a pointer-only
       operation. Both go, so the dock is simply absent below this width rather than half-present — and
       editor-dock.js binds the drag to the grip, so removing it takes the drag target with it. */
    .editor-panel-grip,
    .editor-panel-menu {
        display: none;
    }
}

/* ── The inspector (1171) — selection + the per-block properties schedule ─────────────────────────────────
   A blueprint/technical-drawing identity: hairline-ruled rows, compact rhythm, the ring (not a border) for
   selection, mono as the part-number voice. All colour through tokens. */

/* Canvas selection RING — an outline (no layout shift), painted on the block wrapper inside the preview iframe
   (this sheet bundles into /css/styles.css, which the iframe loads). Hover = a faint tier so blocks read clickable. */
.editor-block-wrap {
    position: relative;
}

.editor-block-wrap:hover {
    outline: 1px solid var(--line-strong);
    outline-offset: -1px;
    cursor: pointer;
}

.editor-block-selected,
.editor-block-selected:hover {
    outline: 2px solid var(--primary);
    outline-offset: -2px;
}

/* The layers row's SELECT trigger — the name, now a button that fills the row and left-aligns.

   BORDERLESS AT REST (1213): the ghost variant carries a resting border, which nested a visible chip inside the
   already-bordered row — a double outline, worst in light. Transparent (not `border: 0`) keeps the 1px in the box
   model, so nothing shifts when hover paints it back. Hover is inherited from .button-ghost:hover, which outranks
   this rule; focus is restated below because .button:focus-visible styles the RING, not the border. */
.editor-layer-select {
    flex: 1 1 auto;
    min-width: 0;
    justify-content: flex-start;
    text-align: start;
    overflow: hidden;
    text-overflow: ellipsis;
    border-color: transparent;
}

.editor-layer-select:focus-visible {
    border-color: var(--button-ghost-border-hover);
}

.editor-layer.is-selected {
    outline: 2px solid var(--primary);
    outline-offset: -2px;
    border-radius: var(--radius-md);
}

.editor-inspectors {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.editor-inspector-empty-state {
    margin: 0;
    padding-block: var(--space-4);
    color: var(--text-muted);
    font-size: 0.875rem;
}

/* The inspector — a properties schedule. */
.editor-inspector-head {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--space-2);
    padding-block-end: var(--space-3);
    border-block-end: 1px solid var(--line);
}

.editor-inspector-name {
    font-weight: 600;
}

/* The breadcrumb "<block> on <page>" (fixture S4) — the block's scope at a glance; the page reads muted. */
.editor-inspector-breadcrumb {
    display: inline-flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--space-1);
    margin: 0;
}

.editor-inspector-breadcrumb-sep,
.editor-inspector-page {
    color: var(--text-muted);
    font-weight: 400;
}

/* Scope footer (fixture S4): "Applies to this block on this page." — quiet, ruled off from the controls above. */
.editor-inspector-scope {
    margin: 0;
    padding-block-start: var(--space-3);
    border-block-start: 1px solid var(--line);
    color: var(--text-muted);
    font-size: 0.75rem;
}

/* The control + its trailing info glyph, side by side (the info-tooltip recipe, #1220): the control takes the
   width, the glyph sits at the end. */
.editor-inspector-control {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.editor-inspector-control > :first-child {
    flex: 1;
    min-width: 0;
}

/* The SHARED control field (the Inspector-polish foundation): an optional orienting icon, the primitive, an
   optional display-only unit adornment, and the info affordance — one flex row both instrument panes render
   (editor/_control-field), so icon + unit land in ONE place. Replaces the per-pane control wrapper's flex. */
.editor-control-field {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.editor-control-field-icon {
    flex: none;
    display: inline-flex;
    width: 1rem;
    height: 1rem;
    color: var(--text-muted);
}

.editor-control-field-body {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: flex-end;
    gap: var(--space-1);
}

/* the primitive grows; the unit suffix stays its own size, aligned to the input row (the primitive's field). */
.editor-control-field-body > :first-child {
    flex: 1;
    min-width: 0;
}

.editor-control-field-unit {
    flex: none;
    padding-block-end: var(--space-2);
    color: var(--text-muted);
    font-size: var(--text-sm);
    line-height: 1;
}

/* An ordered named GROUP section (the group/order keys). Ungrouped controls render in a headerless section. */
.editor-control-group-heading {
    margin-block: var(--space-3) var(--space-1);
    color: var(--text-muted);
}

.editor-control-group:first-child > .editor-control-group-heading {
    margin-block-start: 0;
}

/* SINGLE-GROUP heading suppression (the #1213 precedent, applied to control groups): a controls list of exactly
   one section prints a heading that only repeats what the panel already says — suppress it, so a component with
   0 or 1 group reads flat, exactly as before this foundation. :only-child IS "exactly one section". */
.editor-inspector-controls > .editor-control-group:only-child > .editor-control-group-heading {
    display: none;
}

/* The info button — a quiet focusable tooltip trigger. x-tooltip reveals on hover OR focus; the associated
   control separately keeps its persistent aria-describedby to the sr-only carrier. */
.editor-field-info {
    flex: none;
    padding: var(--space-1);
    color: var(--text-muted);
    cursor: help;
}

.editor-field-info svg {
    width: 1rem;
    height: 1rem;
}

.editor-inspector-id {
    font-family: var(--font-mono);
    font-size: 0.75rem;
    color: var(--text-muted);
}

.editor-inspector-actions {
    display: inline-flex;
    gap: var(--space-2);
    margin-inline-start: auto;
}

.editor-inspector-controls {
    display: flex;
    flex-direction: column;
}

/* The read-only reason line (#1211/#1269): shown above the controls on an unpreviewable target. Each control
   carries its own native `disabled` state (dimmed + inert) — the reason names WHY, in plain language, so it is
   never a silently dead control. The container itself is not additionally dimmed (that would double-dim the
   already-greyed controls); `.is-readonly` is the state hook. */
.editor-inspector-readonly-note {
    margin-block: 0 var(--space-3);
    color: var(--text-muted);
    font-size: var(--text-sm);
}

.editor-inspector-row {
    padding-block: var(--space-3);
    border-block-end: 1px solid var(--line);
}

.editor-inspector-row.is-not-saved {
    border-inline-start: 2px solid var(--danger);
    padding-inline-start: var(--space-2);
}

.editor-inspector-row.is-not-saved::after {
    content: "not saved — retrying on next edit";
    display: block;
    margin-block-start: var(--space-1);
    color: var(--danger);
    font-size: 0.75rem;
}

/* Clear-to-base (§3.1): a row overriding the default carries a modified dot + a Clear action, laid out beside
   the control. A row at base renders neither (and stays a plain block). */
.editor-inspector-row.is-modified {
    display: flex;
    align-items: flex-end;
    gap: var(--space-2);
}

.editor-inspector-control {
    flex: 1 1 auto;
    min-width: 0;
}

.editor-inspector-row-meta {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    flex: 0 0 auto;
    padding-block-end: var(--space-1);
}

.editor-inspector-modified {
    inline-size: 0.375rem;
    block-size: 0.375rem;
    border-radius: 50%;
    background: var(--primary);
}

.editor-inspector-uneditable {
    margin: 0;
    padding-block: var(--space-3);
    border-block-end: 1px solid var(--line);
    color: var(--text-muted);
    font-size: 0.8125rem;
}

.editor-inspector-uneditable-key {
    display: block;
    color: var(--text);
    font-weight: 500;
}

.editor-inspector-empty {
    margin: 0;
    padding-block: var(--space-4);
    color: var(--text-muted);
}

/* The list control — a REPEATER (§3.2): rows of the element's scalar controls, with Add / Remove / move. Each
   row is a hairline-ruled card; the cells flow, the row actions sit at the end. */
.editor-inspector-list-head {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-block-end: var(--space-2);
}

.editor-inspector-list-hint {
    color: var(--text-muted);
    font-size: 0.75rem;
}

.editor-inspector-list-items {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin: 0;
    padding: 0;
    list-style: none;
}

.editor-inspector-list-item {
    display: flex;
    align-items: flex-end;
    flex-wrap: wrap;
    gap: var(--space-2);
    padding: var(--space-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
}

.editor-inspector-list-cells {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    flex: 1 1 12rem;
    min-width: 0;
}

.editor-inspector-list-cell {
    flex: 1 1 8rem;
    min-width: 0;
}

/* A GROUP field (1206 — section-stats' source) is a nested sub-group: it spans the row (its own scalar
   sub-fields flow inside it), ruled off so it reads as one grouped unit, not two more loose cells. */
.editor-inspector-list-cell-group {
    flex: 1 1 100%;
    padding: var(--space-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
}

.editor-inspector-list-group-label {
    display: block;
    margin-block-end: var(--space-1);
}

.editor-inspector-list-group-fields {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}

.editor-inspector-list-group-fields > .input,
.editor-inspector-list-group-fields > .select {
    flex: 1 1 8rem;
    min-width: 0;
}

.editor-inspector-list-row-actions {
    display: inline-flex;
    gap: var(--space-1);
    flex: 0 0 auto;
}

.editor-inspector-list-empty,
.editor-inspector-list-floor {
    margin: var(--space-2) 0 0;
    color: var(--text-muted);
    font-size: 0.75rem;
}
/* The floor-guard line stays --text-muted (inherited above): it is GUIDANCE about a disabled state,
   not an error the user caused — danger is reserved for .is-not-saved, which IS one (the design
   review's one required fix; GOV.UK grammar: error = something went wrong). */

.editor-inspector-list-add {
    margin-block-start: var(--space-2);
}

/* The Add template is inert (native <template> is display:none) — guarded here in case a reset unhides it. */
[data-editor-list-template] {
    display: none;
}

/* Honest-read-only list (a nested-field list, pending the nested-field seam) — current values, no editing. */
.editor-inspector-list-readonly .editor-inspector-list-item {
    font-size: 0.8125rem;
}

.editor-inspector-list-cell-ro {
    display: inline-flex;
    align-items: baseline;
    gap: var(--space-1);
}

.editor-inspector-list-cell-key {
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: 0.6875rem;
}

.editor-inspector-list-note {
    margin: var(--space-2) 0 0;
    color: var(--text-muted);
    font-size: 0.75rem;
}

/* REGION 2 (#1292) — the read-only PROP REFERENCE for a browsed component (the spec sheet). A value-as-text row
   per prop, never a form: label · value · optional icon/unit. Set off from region 1 by a rule + heading so the
   editable controls above and the reference below read as two distinct regions. */
.editor-inspector-reference {
    margin-block-start: var(--space-4);
    padding-block-start: var(--space-3);
    border-block-start: 1px solid var(--line);
}

.editor-inspector-reference-heading {
    margin: 0;
}

.editor-inspector-reference-note {
    margin: var(--space-1) 0 var(--space-3);
    color: var(--text-muted);
    font-size: var(--text-sm);
}

.editor-inspector-reference-row {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    padding-block: var(--space-2);
    border-block-end: 1px solid var(--line);
}

.editor-inspector-reference-label {
    flex: none;
    color: var(--text-muted);
}

.editor-inspector-reference-value {
    flex: 1;
    min-width: 0;
    display: inline-flex;
    align-items: baseline;
    gap: var(--space-1);
    color: var(--text);
    word-break: break-word;
}

/* #1227 — tenant navigation is another Inspector LIST state, never another rail panel. The accepted fixture is a
   compact stack: grip · label + one route hint · pencil, with the existing list-tier Add ghost beneath it. */
.editor-nav-inspector[hidden] {
    display: none;
}

.editor-nav-row {
    align-items: center;
    flex-wrap: nowrap;
}

.editor-nav-row.is-dragging {
    opacity: 0.45;
}

.editor-nav-grip,
.editor-nav-edit,
.editor-nav-move,
.editor-nav-remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    padding: var(--space-1);
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
}

.editor-nav-grip {
    cursor: grab;
}

.editor-nav-grip:active {
    cursor: grabbing;
}

.editor-nav-grip:hover,
.editor-nav-edit:hover,
.editor-nav-move:hover:not(:disabled),
.editor-nav-remove:hover:not(:disabled) {
    background: var(--surface-raised);
    color: var(--text);
}

.editor-nav-grip:focus-visible,
.editor-nav-edit:focus-visible,
.editor-nav-move:focus-visible,
.editor-nav-remove:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

.editor-nav-grip svg,
.editor-nav-edit svg,
.editor-nav-move svg,
.editor-nav-remove svg {
    inline-size: 1rem;
    block-size: 1rem;
}

.editor-nav-summary {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    flex: 1 1 auto;
    min-width: 0;
}

.editor-nav-label {
    overflow: hidden;
    color: var(--text);
    font-size: 0.8125rem;
    font-weight: 600;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.editor-nav-route {
    overflow: hidden;
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.editor-nav-fields {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-2);
    flex: 1 1 100%;
    inline-size: 100%;
    padding-block-start: var(--space-2);
    border-block-start: 1px solid var(--line);
}

.editor-nav-fields[hidden] {
    display: none;
}

.editor-nav-field {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 0;
    color: var(--text-muted);
    font-size: 0.6875rem;
}

.editor-nav-input {
    min-width: 0;
    inline-size: 100%;
    padding: var(--space-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text);
}

.editor-nav-row-actions {
    display: inline-flex;
    gap: var(--space-1);
    flex: 0 0 auto;
}

.editor-nav-move:disabled,
.editor-nav-remove:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

.editor-nav-footer-note {
    padding-block-start: var(--space-2);
    border-block-start: 1px solid var(--line);
}

.editor-nav-status {
    min-block-size: 1.25rem;
    margin: var(--space-2) 0 0;
    color: var(--text-muted);
    font-size: 0.75rem;
}

.editor-nav-status.is-ok {
    color: var(--success);
}

.editor-nav-status.is-error {
    color: var(--danger);
}

@container (max-width: 24rem) {
    .editor-nav-fields {
        grid-template-columns: 1fr;
    }

    .editor-nav-route {
        display: none;
    }
}

/* @toc Games dante page */
/* Games · Dante dashboard (/games/dante) — the slots view. Entities in dense bordered rows (dense-list
   law, not cards). Empty slots are the emphasised state (dashed, neutral) so the gaps itch. Status
   colour rides the categorical token set only (via x-badge); the empty chip is neutral + a dashed line. */

.game-dash {
    display: flex;
    flex-direction: column;
    gap: var(--space-8);
}

.game-dash-tagline {
    margin: 0;
    max-width: 70ch;
    color: var(--text-muted);
}

.game-dash-fill {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    max-width: 32rem;
}

.game-dash-fill-track {
    height: 10px;
    border-radius: var(--radius-full);
    background: var(--surface-muted);
    overflow: hidden;
}

.game-dash-fill-done {
    height: 100%;
    border-radius: var(--radius-full);
    background: var(--success);
}

.game-dash-fill-label {
    font-size: 0.8125rem;
    color: var(--text-muted);
}

.game-section {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.game-section-title {
    margin: 0;
    font-size: 1.125rem;
}

.game-rows {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.game-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-4);
    padding: var(--space-4);
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    background: var(--surface);
}

.game-row-head {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-3);
    flex: 1 1 40%;
    min-width: 0;
}

.game-row-name {
    font-weight: 600;
}

.game-row-oneliner {
    color: var(--text-muted);
    font-size: 0.875rem;
}

.game-slots {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    flex: 2 1 55%;
}

.game-slot-empty {
    display: inline-flex;
    align-items: center;
    padding-block: var(--space-1);
    padding-inline: var(--space-2);
    border: 1px dashed var(--line);
    border-radius: var(--radius-control);
    background: var(--surface);
    color: var(--text-muted);
    font-size: 0.75rem;
}

.game-row-chevron {
    display: inline-flex;
    flex: 0 0 auto;
    color: var(--text-muted);
}

.game-row-chevron svg {
    width: 1rem;
    height: 1rem;
}

/* @toc Games entity page */
/* Games · entity page (/games/dante/{type}/{slug}) — hero + stats + graduated frame strips. Sprite
   imgs carry the pixel law (image-rendering: pixelated) and render at natural size: the cuts are
   box-composed to a union rect, so equal, un-scaled frames keep a strip's animation aligned. */

.game-entity {
    display: flex;
    flex-direction: column;
    gap: var(--space-8);
}

.game-entity-hero {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: var(--space-6);
}

.game-entity-art {
    flex: 0 0 auto;
    width: min(240px, 100%);
}

.game-entity-art-img {
    width: 100%;
    height: auto;
    border-radius: var(--radius-lg);
    background: var(--surface-muted);
}

.game-entity-art-empty {
    display: flex;
    align-items: center;
    justify-content: center;
    aspect-ratio: 1 / 1;
    padding: var(--space-6);
    border: 1px dashed var(--line);
    border-radius: var(--radius-lg);
    background: var(--surface-muted);
    color: var(--text-muted);
    text-align: center;
    font-size: 0.875rem;
}

.game-entity-identity {
    flex: 1 1 320px;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.game-entity-role {
    margin: 0;
    color: var(--text-muted);
    font-size: 1rem;
}

.game-entity-zone {
    margin: 0;
}

.game-entity-zone-long {
    color: var(--text-muted);
    font-size: 0.875rem;
}

.game-entity-lore {
    margin: 0;
    max-width: 65ch;
    line-height: 1.6;
}

.game-entity-quote {
    margin: 0;
    padding-inline-start: var(--space-4);
    border-inline-start: 3px solid var(--line);
    color: var(--text-muted);
    font-style: italic;
}

.game-entity-quote p {
    margin: 0 0 var(--space-2);
}

.game-entity-quote cite {
    font-size: 0.8125rem;
    font-style: normal;
}

.game-entity-section {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
}

.game-entity-section h2 {
    margin: 0;
    font-size: 1.25rem;
}

.game-empty {
    margin: 0;
    color: var(--text-muted);
}

.game-strip-block {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.game-strip-title {
    margin: 0;
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    font-size: 1rem;
    text-transform: capitalize;
}

.game-strip-count {
    font-size: 0.8125rem;
    font-weight: 400;
    color: var(--text-muted);
    text-transform: none;
}

.game-strip {
    display: flex;
    flex-wrap: nowrap;
    gap: var(--space-4);
    padding: var(--space-4);
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    background: var(--surface-muted);
    overflow-x: auto;
}

.game-frame {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-2);
    flex: 0 0 auto;
}

.game-pixel {
    image-rendering: pixelated;
}

.game-frame-img {
    display: block;
    width: auto;
    height: auto;
    max-width: none;
    border-radius: var(--radius-sm);
    background: var(--surface);
}

.game-frame-tag {
    display: inline-flex;
}

.game-frame-ord {
    font-size: 0.75rem;
    color: var(--text-muted);
}

.game-rulings {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.game-ruling {
    margin: 0;
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    color: var(--text-muted);
    font-size: 0.875rem;
}

.game-provenance {
    margin: 0;
    padding-inline-start: var(--space-5);
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    color: var(--text-muted);
    font-size: 0.875rem;
}

/* @toc Games index page */
/* Games index (/games) — the section shelf: a reflowing card grid, one card per game. */

.games-index {
    display: flex;
    flex-direction: column;
    gap: var(--space-6);
}

.games-index-lead {
    margin: 0;
    max-width: 60ch;
    color: var(--text-muted);
}

.games-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: var(--space-5);
}

.games-card-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    margin-block-start: var(--space-4);
}

.games-fill {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.games-fill-track {
    height: 8px;
    border-radius: var(--radius-full);
    background: var(--surface-muted);
    overflow: hidden;
}

.games-fill-done {
    height: 100%;
    border-radius: var(--radius-full);
    background: var(--success);
}

.games-fill-label {
    font-size: 0.8125rem;
    color: var(--text-muted);
}

.games-card-foot {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
}

.games-empty {
    margin: 0;
    color: var(--text-muted);
}

/* @toc Landing page */
/* Landing page — the public eightythree.com home: just the brand mark, centered.
 * Renders inside the public shell (x-app-page: topbar + main).
 * View: /resources/views/pages/landing.blade.php. */

.landing {
    display: flex;
    flex-direction: column;
    align-items: center;
}

/* @toc Login page */
/* Shared tenant entry surface: one responsive request/confirmation shell for every registered host. */

.tenant-entry {
    width: min(100%, 360px);
    display: grid;
    gap: 1.5rem;
}

.tenant-entry-brand {
    width: fit-content;
    max-width: 100%;
    margin-inline: auto;
    color: var(--text);
    text-decoration: none;
    border-radius: 0.5rem;
}

.tenant-entry-brand:focus-visible {
    outline: 3px solid var(--primary-500);
    outline-offset: 0.375rem;
}

.tenant-entry-brand .brand-logo svg {
    width: auto;
    max-width: min(100%, 20rem);
    height: clamp(2.5rem, 10vw, 4rem);
}

.tenant-entry-brand .brand-logo-wordmark {
    display: block;
    max-width: 100%;
    color: var(--text);
    font-family: var(--font-body, sans-serif);
    font-size: clamp(2rem, 9vw, 3.5rem);
    font-weight: 700;
    line-height: 1;
    overflow-wrap: anywhere;
    text-align: center;
}

.tenant-entry .card {
    width: 100%;
}

.tenant-entry-title {
    margin: 0 0 0.75rem;
    font-size: clamp(1.5rem, 6vw, 2rem);
    overflow-wrap: anywhere;
}

.tenant-entry form {
    display: grid;
    gap: 1rem;
    margin-top: 1.25rem;
}

.tenant-entry form .button {
    width: 100%;
    min-height: 2.75rem;
    justify-content: center;
}

.tenant-entry-progress,
.tenant-entry-no-js {
    min-height: 1.5em;
    margin: 0;
    color: var(--text-muted);
    font-size: var(--text-sm);
}

.tenant-entry-form-loading .button {
    cursor: wait;
}

@media (max-width: 360px) {
    .tenant-entry {
        gap: 1rem;
    }

    .tenant-entry .card-body {
        padding: 1rem;
    }
}

@media (prefers-reduced-motion: reduce) {
    .tenant-entry *,
    .tenant-entry *::before,
    .tenant-entry *::after {
        scroll-behavior: auto !important;
        transition-duration: 0.01ms !important;
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
    }
}

/* @toc Planner card page */
/* Card-open (⑨) — the five-pane ticket workspace. Mobile (< 1024px): x-tabs default, one pane at a time,
 * the tablist visible. Desktop (>= 1024px, the x-tabs `unwrap` seam): the tablist is hidden (tabs.js drops
 * its tab semantics to match) and every panel shows as a labelled column, side by side. Tokens only. */

.card-open {
    max-width: 100%;
}

/* The return path to the board (this page is reached from a board card's open affordance). */
.cardo-back {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    margin-bottom: 14px;
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--text-muted);
    text-decoration: none;
    transition: color 120ms ease;
}

.cardo-back svg {
    width: 16px;
    height: 16px;
}

.cardo-back:hover {
    color: var(--text);
}

.cardo-back:focus-visible {
    outline: 2px solid var(--primary-500);
    outline-offset: 2px;
    border-radius: 4px;
    color: var(--text);
}

/* Card redesign — compact HEADER: the title, the labels (tier · project chips), and the de-throned
   reporter/assignee. People are secondary metadata now (small + muted with a role caption), not the accent
   banner they used to be, and the Lane field is gone (redundant with the board column). */
.cardo-head {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 14px 20px;
    border-bottom: 1px solid var(--line);
}

.cardo-head-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 10px 16px;
}

.cardo-labels {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

.cardo-people {
    display: flex;
    flex-wrap: wrap;
    gap: 14px;
}

.cardo-person {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

/* One consistent icon size across reporter / assignee / lock — the oversized-lock fix. */
.cardo-person svg {
    width: 14px;
    height: 14px;
    flex-shrink: 0;
}

.cardo-person-name {
    font-weight: 600;
    color: var(--text);
}

/* The assignee POPS in the tenant primary (ticket 1046) so the dialog agrees with the tile; reporter stays
   quiet-neutral. Name + icon (incl. the live-checkout lock) take the primary; AA via the blend toward --text. */
.cardo-person-assignee {
    color: color-mix(in srgb, var(--primary) 46%, var(--text));
}

.cardo-person-assignee .cardo-person-name {
    color: inherit;
}

.cardo-person-role {
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: 10px;
}

/* The card body runs inside the modal's flush (padded=false) dialog body, so the panes own their padding
   (the Details tab breathes) while the tab BAR spans full-width. On the deep-link page the same padding reads
   as normal content inset. The Code tab is the exception — its own tree·code·explanation grid pads its cells,
   so the pane itself runs flush (full-bleed) below the full-width tab bar. */
.card-open .pane {
    padding: 16px 20px;
}

.card-open .pane-code-tab {
    padding: 0;
}

/* Full-width tab bar: its bottom rule spans edge-to-edge; the tab buttons keep a small inset so they're not
   jammed against the flush body's edge. */
.card-open .tabs-nav {
    padding-inline: 12px;
}

.pane-empty,
.pane-meaning-state {
    color: var(--text-muted);
    font-size: var(--text-sm);
}

/* Description */
.pane-status {
    font-size: var(--text-sm);
    color: var(--text);
    margin-bottom: 12px;
}

.pane-status-label {
    display: inline-block;
    font-weight: 600;
    color: var(--text-muted);
    text-transform: uppercase;
    font-size: var(--text-xs);
    letter-spacing: 0.04em;
    margin-right: 6px;
}

.pane-prose {
    font-size: var(--text-sm);
    line-height: 1.6;
    color: var(--text);
}

.pane-prose :is(p, ul, ol) {
    margin: 0 0 10px;
}

/* The Acceptance checklist is the <x-checklist> primitive now (#838) — it owns the list, badge and
   ticked-row styling (checklist.css). This page keeps only the SECTION delineation: a clear gap + a hairline
   rule above it, so the Checklist reads as its own stacked section below the Description prose (the Trello
   card-back model), not a continuation of it. */
/* #1359 — the 70%-transparent thinning is GONE. It created an ad-hoc third tier below --line, and a rule
   that dilutes the token defeats a fix to the token: sampled on the real card back, this line was
   neutral-800 on a neutral-800 dialog — the SAME hex as its own background, 1.00:1, invisible at any
   alpha. Even after the token step it stayed ~1.29:1 while diluted, i.e. still at the faintness James
   reported. Undiluted it lands at 1.46:1 dark / 1.42:1 light and matches every other divider. Subtlety
   is the token's job (--line is already the quiet tier); a per-rule alpha is how a system grows a
   fourth colour nobody named. */
.pane-checklist {
    margin-block-start: 16px;
    padding-block-start: 14px;
    border-top: 1px solid var(--line);
}

/* Files — a flat clickable list (the pre-Tree fallback) */
.cardo-files {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.cardo-file {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    text-align: left;
    background: transparent;
    border: none;
    border-radius: var(--radius-md);
    padding: 6px 8px;
    cursor: pointer;
    color: var(--text);
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: var(--text-xs);
}

.cardo-file:hover,
.cardo-file.is-active {
    background: color-mix(in srgb, var(--primary) 12%, transparent);
}

.cardo-file-kind {
    flex-shrink: 0;
    font-family: var(--font-body, sans-serif);
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--text-muted);
}

.cardo-file-kind-touched {
    color: var(--primary);
}

.cardo-file-path {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Code — a bare <pre> live window */
.pane-code-ref {
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: var(--text-xs);
    color: var(--text-muted);
    margin-bottom: 8px;
}

.cardo-code {
    margin: 0;
    padding: 12px;
    background: var(--neutral-900);
    color: var(--neutral-100);
    border-radius: var(--radius-md);
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: var(--text-xs);
    line-height: 1.5;
    white-space: pre;
    overflow: auto;
    max-height: 60vh;
}

:root[data-theme="light"] .cardo-code {
    background: var(--neutral-100);
    color: var(--neutral-900);
}

/* Explanation */
.pane-meaning-prose {
    font-size: var(--text-sm);
    line-height: 1.6;
    color: var(--text);
    white-space: pre-wrap;
}

/* Settings rail */
.cardo-rail {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 6px 12px;
    margin: 0;
    font-size: var(--text-sm);
}

.cardo-rail dt {
    color: var(--text-muted);
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.03em;
    align-self: center;
}

.cardo-rail dd {
    margin: 0;
    color: var(--text);
}

.cardo-rail-note {
    margin-top: 16px;
    font-size: var(--text-xs);
    color: var(--text-muted);
    font-style: italic;
}

/* Details tab — a comfortable reading measure for the description-led pane. */
.pane-details {
    max-width: 72ch;
}

/* Peripheral metadata footer (Source · Size · Created · Commit) — de-emphasised inline pairs under the
   description + checklist; the old Settings pane is retired into this quiet strip (the Lane field dropped). */
.cardo-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 6px 20px;
    margin: 18px 0 0;
    padding-top: 14px;
    /* #1359 — undiluted, for the reason spelled out on .pane-checklist above: the strip being
       "de-emphasised" is carried by its muted TEXT, not by thinning the rule that separates it. */
    border-top: 1px solid var(--line);
}

.cardo-meta-item {
    display: flex;
    align-items: baseline;
    gap: 6px;
}

.cardo-meta dt {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--text-muted);
}

.cardo-meta dd {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--text);
}

.cardo-meta code {
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: var(--text-xs);
}

/* Code tab — files tree · code · explanation, three regions with room to breathe. One column when narrow;
   a three-column grid once the MODAL has width. @container (not @media): it reflows to the card's own box,
   not the viewport — component-internal responsiveness, the CSS-modernity contract's split. */
.pane-code-tab {
    container-type: inline-size;
}

.cardo-code-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 14px;
}

@container (min-width: 720px) {
    .cardo-code-grid {
        grid-template-columns: minmax(150px, 200px) minmax(0, 1fr) minmax(170px, 240px);
        gap: 1px;
        background: var(--line);
        border: 1px solid var(--line);
        border-radius: var(--radius-lg);
        overflow: hidden;
    }

    .cardo-code-grid > div {
        background: var(--surface);
        padding: 14px;
        overflow: auto;
        max-height: calc(100vh - 240px);
    }
}

/* @toc Uploads page */
/* Uploads page — the single-file viewer (/uploads/<slug>). The gallery itself is the
 * uploads-gallery component; this is just the embedded-file page. Page CSS, bundled by
 * CssService. */

.uploads-view {
    display: flex;
    flex-direction: column;
    gap: 12px;
    height: calc(100vh - 180px);
    min-height: 460px;
}

.uploads-view-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
}

.uploads-view-meta {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--text-sm);
}

.uploads-view-frame {
    flex: 1;
    width: 100%;
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--surface);
}

