/*
 * Paligo documentation — main stylesheet.
 *
 * A modern documentation-portal layout (app shell: header, persistent nav
 * sidebar, reading column, in-page TOC, footer) built entirely on CSS custom-
 * property design tokens. Branding is a thin, contrast-checkable layer: every
 * colour, font, radius and metric below is a --paligo-* token, so a customer
 * rebrands their portal by overriding tokens — no stylesheet forking. The
 * Paligo palette here is only the DEFAULT.
 *
 * Themes: the token layer is defined once, then redefined for dark. The OS
 * preference drives it via prefers-color-scheme; the header toggle stamps
 * data-theme on <html> and wins over the media query in both directions.
 *
 * Class names: body > nav.top (header), aside.site-nav (nav tree),
 * .pagebody > (main + nav.pagetoc), body > nav.bottom (pager + footer).
 * WCAG AA throughout.
 *
 * Google Fonts (Inter) is loaded with a full system fallback stack, so output
 * still RENDERS when the CDN is unreachable.
 *
 * Rendering is all the fallback covers, and the line below is worth reading as
 * what it is: an unconditional request to fonts.googleapis.com on every page
 * view, which discloses the reader's IP to a third party and delays first paint
 * of themed text until it resolves or fails. An output that has to be private or
 * air-gapped therefore needs the font SELF-HOSTED - the fallback stack is not a
 * substitute, and this comment used to imply it was by calling those "GDPR
 * builds". Inter is SIL OFL 1.1, so shipping it with the theme is permitted;
 * doing so is a deliberate decision about theme payload size, not a fix, which
 * is why it is named here rather than quietly done.
 */

/* Bare-string notation: `url()` inside @import is redundant, and stylelint's
   import-notation rule flags it. */
@import 'https://fonts.googleapis.com/css2?family=Inter:wght@400;450;500;600;700;800&display=swap';

/* ===== Design tokens ====================================================== */
:root {
  /* Brand — the AI-era Paligo identity. The gradient is used sparingly (logo,
     active nav rail, Ask-AI); indigo is the working primary. */
  --paligo-brand:        #626cff;
  --paligo-brand-end:    #c55cbd;
  --paligo-gradient:     linear-gradient(135deg, var(--paligo-brand) 0%, var(--paligo-brand-end) 100%);
  --paligo-primary:      #3a46e8;   /* AA on white (5.9:1) — links, buttons, focus */
  --paligo-primary-hover:#2a34c4;
  --paligo-brand-wash:   #eceeff;   /* soft indigo wash behind active/hover */

  /* Neutrals — biased toward indigo, not pure grey. */
  --paligo-bg:           #ffffff;
  --paligo-surface:      #f4f5fb;   /* sidebar / code / admonition ground */
  --paligo-surface-2:    #eceef6;
  --paligo-border:       #e4e5f0;
  --paligo-border-strong:#d3d5e4;
  --paligo-text:         #161617;   /* 16.9:1 on white */
  --paligo-muted:        #5c5c6a;   /* 6.5:1 on white  */
  --paligo-faint:        #8a8a99;
  --paligo-focus:        var(--paligo-primary);

  /* Semantic (admonitions) — separate from the brand accent. */
  --paligo-note:    #3a46e8;
  --paligo-tip:     #1f9d6b;
  --paligo-important:#7c3aed;
  --paligo-caution: #b26a00;
  --paligo-warning: #c10f0f;
  --paligo-danger:  #c10f0f;

  /* Typography */
  --paligo-font: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
  --paligo-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
  --paligo-font-size: 16px;
  --paligo-line-height: 1.65;
  --paligo-measure: 68ch;           /* readable line length */
  --paligo-breadcrumb-sep: "›";     /* breadcrumb separator; override in custom CSS, e.g. "/" */

  /* Radii, spacing, shadow */
  --paligo-radius-sm: 6px;
  --paligo-radius: 10px;
  --paligo-radius-lg: 16px;
  --paligo-radius-pill: 999px;
  --paligo-space: 1rem;
  --paligo-shadow: 0 1px 2px rgba(22,22,40,.04), 0 8px 24px -12px rgba(60,64,150,.18);
  --paligo-shadow-lg: 0 24px 48px -20px rgba(60,64,150,.35);

  /* App-shell metrics */
  --paligo-header-h: 60px;
  --paligo-sidebar-w: 288px;
  --paligo-toc-w: 15rem;
  --paligo-shell-max: 1440px;       /* cap width in the "centered" layout mode */
  /* Inline padding that aligns a full-bleed bar's (header/footer) inner content
     to the centered shell: a base gutter until the viewport exceeds the shell,
     then the gutter that keeps the content flush with the shell edges. */
  --paligo-bleed-pad: max(1.5rem, calc((100% - var(--paligo-shell-max)) / 2));
  color-scheme: light;
}

@media (prefers-color-scheme: dark) {
  :root {
    --paligo-primary:      #8b93ff;   /* 7.4:1 on --paligo-bg */
    --paligo-primary-hover:#a7adff;
    --paligo-brand-wash:   #1d1e30;
    --paligo-bg:           #0e0e15;
    --paligo-surface:      #16161f;
    --paligo-surface-2:    #1d1d29;
    --paligo-border:       #292a3a;
    --paligo-border-strong:#363750;
    --paligo-text:         #e9e9f1;
    --paligo-muted:        #a6a7ba;
    --paligo-faint:        #71728a;
    --paligo-note:     #8b93ff;
    --paligo-tip:      #34d399;
    --paligo-important:#c4b5fd;
    --paligo-caution:  #e0a24a;
    --paligo-warning:  #f6867a;
    --paligo-danger:   #f6867a;
    --paligo-shadow: 0 1px 2px rgba(0,0,0,.4), 0 8px 24px -12px rgba(0,0,0,.6);
    --paligo-shadow-lg: 0 24px 48px -20px rgba(0,0,0,.7);
    color-scheme: dark;
  }
}
/* Manual toggle wins over the OS media query in both directions. */
:root[data-theme="light"] {
  --paligo-primary:#3a46e8; --paligo-primary-hover:#2a34c4; --paligo-brand-wash:#eceeff;
  --paligo-bg:#ffffff; --paligo-surface:#f4f5fb; --paligo-surface-2:#eceef6;
  --paligo-border:#e4e5f0; --paligo-border-strong:#d3d5e4;
  --paligo-text:#161617; --paligo-muted:#5c5c6a; --paligo-faint:#8a8a99;
  --paligo-note:#3a46e8; --paligo-tip:#1f9d6b; --paligo-important:#7c3aed; --paligo-caution:#b26a00; --paligo-warning:#c10f0f; --paligo-danger:#c10f0f;
  --paligo-shadow: 0 1px 2px rgba(22,22,40,.04), 0 8px 24px -12px rgba(60,64,150,.18);
  --paligo-shadow-lg: 0 24px 48px -20px rgba(60,64,150,.35);
  color-scheme: light;
}
:root[data-theme="dark"] {
  --paligo-primary:#8b93ff; --paligo-primary-hover:#a7adff; --paligo-brand-wash:#1d1e30;
  --paligo-bg:#0e0e15; --paligo-surface:#16161f; --paligo-surface-2:#1d1d29;
  --paligo-border:#292a3a; --paligo-border-strong:#363750;
  --paligo-text:#e9e9f1; --paligo-muted:#a6a7ba; --paligo-faint:#71728a;
  --paligo-note:#8b93ff; --paligo-tip:#34d399; --paligo-important:#c4b5fd; --paligo-caution:#e0a24a; --paligo-warning:#f6867a; --paligo-danger:#f6867a;
  --paligo-shadow: 0 1px 2px rgba(0,0,0,.4), 0 8px 24px -12px rgba(0,0,0,.6);
  --paligo-shadow-lg: 0 24px 48px -20px rgba(0,0,0,.7);
  color-scheme: dark;
}

/* ===== Reset & base ======================================================= */
*, *::before, *::after { box-sizing: border-box; }
html {
  -webkit-text-size-adjust: 100%; scroll-behavior: smooth; background: var(--paligo-bg);
  scroll-padding-top: calc(var(--paligo-header-h) + 1rem);
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  * { animation-duration:.01ms !important; transition-duration:.01ms !important; }
}

body {
  margin: 0;
  font-family: var(--paligo-font);
  font-size: var(--paligo-font-size);
  line-height: var(--paligo-line-height);
  color: var(--paligo-text);
  background: var(--paligo-bg);
  -webkit-font-smoothing: antialiased;

  /* App shell (grid-template-areas). The sidebar + content form a centered
     shell capped at --paligo-shell-max: two minmax(0,1fr) gutter columns absorb
     the extra width on wide screens and centre the pair. Header + footer span
     all four columns so their backgrounds bleed edge-to-edge, while their inner
     content is aligned to the shell via --paligo-bleed-pad. nav.bottom uses
     display:contents so its .site-footer lands in the footer area. */
  display: grid;
  grid-template-columns:
    minmax(0, 1fr)
    var(--paligo-sidebar-w)
    minmax(0, calc(var(--paligo-shell-max) - var(--paligo-sidebar-w)))
    minmax(0, 1fr);
  grid-template-rows: var(--paligo-header-h) auto minmax(0, 1fr) auto;
  grid-template-areas:
    "header  header  header  header"
    ".       sidebar crumb   ."
    ".       sidebar content ."
    "footer  footer  footer  footer";
  min-height: 100vh;
}
/* nav.top / nav.bottom are display:contents, so the header bar (.topbar), the
   breadcrumb and the footer become direct grid items. The breadcrumb sits above
   the reading column; the pager is relocated INTO <main> on the server
   so it tracks the content's dynamic width. DOM order stays header → breadcrumb →
   content(+pager) → footer for a11y. */
body > nav.top     { display: contents; }
.topbar            { grid-area: header; }
.breadcrumb        { grid-area: crumb; }
body > aside.site-nav { grid-area: sidebar; }
body > .pagebody,
body > main        { grid-area: content; }
body > nav.bottom  { display: contents; }
nav.bottom .site-footer { grid-area: footer; }
/* No sidebar (no-JS, or the sidebar-less layout) → a single centered content
   column in the same shell; header + footer still bleed. */
body:not(:has(aside.site-nav)) {
  grid-template-columns: minmax(0, 1fr) minmax(0, var(--paligo-shell-max)) minmax(0, 1fr);
  grid-template-areas:
    "header  header  header"
    ".       crumb   ."
    ".       content ."
    "footer  footer  footer";
}

:focus-visible { outline: 2px solid var(--paligo-focus); outline-offset: 2px; border-radius: 3px; }
a { color: var(--paligo-primary); text-decoration: none; }
a:hover { color: var(--paligo-primary-hover); text-decoration: underline; text-underline-offset: 2px; }
img, svg, video { max-width: 100%; height: auto; }

.skip-link {
  position: absolute; left: -999px; top: .5rem; z-index: 60;
  background: var(--paligo-bg); color: var(--paligo-primary);
  padding: .5rem 1rem; border-radius: var(--paligo-radius-sm); box-shadow: var(--paligo-shadow);
}
.skip-link:focus { left: .5rem; }

/* ===== Header (nav.top → .topbar) ========================================= */
/* nav.top is display:contents (see the grid block); .topbar is the actual
   sticky, full-bleed header bar that occupies the header grid row. */
.topbar {
  position: sticky; top: 0; z-index: 40;
  display: flex; align-items: center; gap: 1rem;
  padding: 0 var(--paligo-bleed-pad); height: var(--paligo-header-h);
  background: color-mix(in srgb, var(--paligo-bg) 85%, transparent);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--paligo-border);
}
.brand { display: flex; align-items: center; gap: .75rem; margin-right: auto; font-weight: 650; letter-spacing:-.01em; color: var(--paligo-text); flex-shrink:0; }
.brand:hover { text-decoration: none; color: var(--paligo-text); }
.brand-mark {
  width: 30px; height: 30px; border-radius: 9px; overflow: hidden;
  background: var(--paligo-gradient); display: grid; place-items: center; flex-shrink: 0;
  box-shadow: 0 4px 12px -4px rgba(98,108,255,.6);
}
.brand-mark svg { width: 17px; height: 17px; color: #fff; }
/* An uploaded logo stands on its own (see brand.html.twig): its natural
   proportions, capped in height to the header and in width so a very wide
   lockup cannot crowd out the nav. No gradient ground - that one is ours. */
.brand-logo { height: 26px; width: auto; max-width: 200px; object-fit: contain; display: block; flex-shrink: 0; }

/* Two logos, one shown. Which one depends on the ground BEHIND it, which is
   not one answer per page: the theme toggle changes it without a reload, and
   the portal topbar floats on the hero until it scrolls, at which point the
   page background takes over. Hence CSS rather than a choice made at render.
   The same light/dark selector shape the theme toggle's own icons use. */
.brand-logo-dark { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .brand-logo-light { display: none; }
  :root:not([data-theme="light"]) .brand-logo-dark { display: block; }
}
:root[data-theme="dark"] .brand-logo-light { display: none; }
:root[data-theme="dark"] .brand-logo-dark { display: block; }
:root[data-theme="light"] .brand-logo-light { display: block; }
:root[data-theme="light"] .brand-logo-dark { display: none; }

/* On the portal, before the topbar scrolls, the hero is the ground - and it is
   an absolute colour, the same in both schemes. So the hero decides, overriding
   the reader's scheme for as long as the bar is transparent. data-hero is only
   set when the hero IS a flat colour (base.html.twig), so a hero image or a
   plain hero leaves the scheme rules above in charge. */
:root[data-hero="dark"] body.portal .topbar:not(.is-scrolled) .brand-logo-light,
:root[data-hero="light"] body.portal .topbar:not(.is-scrolled) .brand-logo-dark { display: none; }
:root[data-hero="dark"] body.portal .topbar:not(.is-scrolled) .brand-logo-dark,
:root[data-hero="light"] body.portal .topbar:not(.is-scrolled) .brand-logo-light { display: block; }
.brand-name { font-size: 15px; }
.brand-name b { font-weight: 750; }
.brand-name .brand-sub { color: var(--paligo-muted); font-weight: 500; margin-left: .5rem; padding-left: .55rem; border-left: 1px solid var(--paligo-border); }



.header-tools { display: flex; align-items: center; gap: .5rem; }
/* Header controls read as buttons: a subtle bordered surface. */
.icon-btn {
  width: 38px; height: 38px; display: grid; place-items: center;
  border: 1px solid var(--paligo-border); border-radius: var(--paligo-radius-sm);
  background: var(--paligo-surface); color: var(--paligo-muted); cursor: pointer;
}
.icon-btn:hover { background: var(--paligo-surface-2); color: var(--paligo-text); }
.icon-btn svg { width: 18px; height: 18px; }
.theme-toggle .i-moon { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .theme-toggle .i-sun { display: none; }
  :root:not([data-theme]) .theme-toggle .i-moon { display: block; }
}
:root[data-theme="dark"] .theme-toggle .i-sun { display: none; }
:root[data-theme="dark"] .theme-toggle .i-moon { display: block; }
:root[data-theme="light"] .theme-toggle .i-sun { display: block; }
:root[data-theme="light"] .theme-toggle .i-moon { display: none; }

/* Language switcher: a button (summary) that opens a dropdown, rendered by
   base.html.twig around paligo_language_switcher(). The chrome below is the
   theme's; the links inside the menu are the component's. Paligo used to splice
   a ready-made switcher into .header-tools after the build, which is why this
   used to carry a fallback rule for one that had not been placed yet — nothing
   arrives unplaced now, so there is nothing to catch. */
.langswitch { position: relative; }
.langswitch > summary {
  list-style: none; display: flex; align-items: center; gap: 6px; height: 38px; padding: 0 .7rem;
  border: 1px solid var(--paligo-border); border-radius: var(--paligo-radius-sm);
  background: var(--paligo-surface); color: var(--paligo-text); cursor: pointer; font-size: 14px; font-weight: 550;
}
.langswitch > summary::-webkit-details-marker { display: none; }
.langswitch > summary:hover { background: var(--paligo-surface-2); }
.langswitch > summary .globe { width: 16px; height: 16px; color: var(--paligo-muted); }
.langswitch > summary .chevron { width: 13px; height: 13px; color: var(--paligo-faint); transition: transform .15s ease; }
/* Summary label: full native name on desktop, compact language code on mobile. */
/* Native language names capitalize per their own convention (English vs
   svenska), which reads inconsistently on the button; force a leading capital. */
.langswitch-label { text-transform: capitalize; }
.langswitch-label-short { display: none; text-transform: uppercase; }
.langswitch[open] > summary .chevron { transform: rotate(180deg); }
.langswitch-menu {
  position: absolute; right: 0; top: calc(100% + 6px); min-width: 190px;
  background: var(--paligo-bg); border: 1px solid var(--paligo-border);
  border-radius: var(--paligo-radius); box-shadow: var(--paligo-shadow-lg); padding: 4px; z-index: 50;
}
/* The menu's contents are the component's <nav class="paligo-language-switcher">,
   which theme.css styles as an inline row for a theme that drops it into running
   text. Inside this dropdown it has to stack instead, and theme.css loads AFTER
   main.css — so every rule here carries the component class as well, to win on
   specificity rather than on order. */
.langswitch-menu .paligo-language-switcher ul { display: block; gap: 0; }
.langswitch-menu .paligo-language-switcher a {
  display: flex; padding: 8px 10px;
  border-radius: var(--paligo-radius-sm); color: var(--paligo-text); font-size: 14px; text-decoration: none; text-transform: capitalize;
}
.langswitch-menu .paligo-language-switcher a:hover { background: var(--paligo-surface); text-decoration: none; }
.langswitch-menu .paligo-language-switcher a[aria-current] { color: var(--paligo-primary); font-weight: 650; }

/* Hamburger + mobile search button: narrow viewports only (shown in the mobile
   media query). The sidebar footer (theme toggle) is likewise desktop-hidden —
   on desktop the theme toggle lives in the header. */
.menu-toggle { display: none; }
.search-toggle { display: none; }
.nav-footer { display: none; }

/* Search presentation mode ($header-search-mode = 'icon', set as data-search on
   <html> by the head bootstrap). Icon mode swaps the inline header box for the
   compact search button at desktop widths too; the theme toggle stays put (only
   the narrow-viewport media query hides it). Below the 860px breakpoint the
   media query already forces the icon, so these rules are the desktop half. */
:root[data-search="icon"] .header-search { display: none; }
:root[data-search="icon"] .search-toggle { display: grid; }

/* ===== Top-nav menu (role=top-nav-link) ================================== */
/* A horizontal menu in the header; nested top-nav-link sections + external links
   fold into a one-level drop-down. Hidden on narrow viewports (the sidebar covers
   navigation there). */
.top-nav { display: flex; min-width: 0; }
.top-nav > ul { display: flex; align-items: center; gap: .25rem; margin: 0; padding: 0; list-style: none; flex-wrap: wrap; }
.top-nav li { position: relative; margin: 0; }
.top-nav a { display: inline-block; padding: .4rem .6rem; border-radius: 7px; color: var(--paligo-text); font-weight: 550; font-size: 14.5px; text-decoration: none; }
.top-nav a:hover { background: var(--paligo-surface); color: var(--paligo-text); }
.top-nav .top-nav-divider { width: 1px; align-self: stretch; margin: .3rem .35rem; background: var(--paligo-border); padding: 0; }
.top-nav .submenu-toggle {
  display: inline-flex; align-items: center; justify-content: center; vertical-align: middle;
  width: 1.4rem; height: 1.4rem; padding: 0; margin-left: -.35rem;
  background: none; border: 0; color: var(--paligo-muted); cursor: pointer;
}
.top-nav .submenu-toggle svg { width: .8rem; height: .8rem; }
/* Drop-down: hidden until hover (desktop) or .open (click/keyboard). */
.top-nav .submenu {
  position: absolute; left: 0; top: 100%; z-index: 40; min-width: 12rem;
  margin: .25rem 0 0; padding: .35rem; list-style: none;
  background: var(--paligo-bg); border: 1px solid var(--paligo-border);
  border-radius: var(--paligo-radius); box-shadow: 0 6px 24px rgba(0,0,0,.14);
  display: none;
}
.top-nav .submenu a { display: block; }
.top-nav li.has-submenu:hover > .submenu,
.top-nav li.has-submenu.open > .submenu { display: block; }
@media (max-width: 60rem) { .top-nav { display: none; } }

/* ===== Sidebar (aside.site-nav) ==========================================
 * Fixed-height sticky column pinned under the header (always fills the viewport
 * height, so it never cuts off), scrolling internally. */
aside.site-nav {
  border-right: 1px solid var(--paligo-border);
  /* Left border too: in the centered layout the shell has gutters on wide
     screens, so the sidebar surface floats — the left border frames it against
     the gutter. Removed in the full-width layout, where the sidebar sits at the
     viewport edge (see below). */
  border-left: 1px solid var(--paligo-border);
  background: var(--paligo-surface);
  /* Fill the full content height (down to the footer) so the sidebar surface +
     border reach the footer even when the tree is short. The scrolling happens
     in .nav-inner, not here. */
  align-self: stretch;
}
:root[data-layout="full"] aside.site-nav { border-left: 0; }
/* The actual scroller: sticky under the header, capped to the viewport, scrolls
   internally. Sitting inside the full-height aside keeps the surface flush with
   the footer while the tree stays pinned. */
aside.site-nav > .nav-inner {
  position: sticky; top: var(--paligo-header-h);
  max-height: calc(100vh - var(--paligo-header-h));
  overflow-y: auto;
  padding: 0 .75rem 2rem;
}
/* Sticky filter band at the top of the scrolling sidebar; a bottom border fades
   in once the tree scrolls beneath it. */
.nav-filter-wrap {
  position: sticky; top: 0; z-index: 2;
  margin: 0 -.75rem .5rem; padding: 1rem .75rem .6rem;
  background: var(--paligo-surface);
  border-bottom: 1px solid transparent; transition: border-color .2s ease;
}
.nav-filter-wrap.scrolled { border-bottom-color: var(--paligo-border); }
.nav-filter {
  display: flex; align-items: center; gap: .5rem; padding: 0 .4rem 0 .75rem; height: 36px;
  background: var(--paligo-bg); border: 1px solid var(--paligo-border); border-radius: var(--paligo-radius-sm); color: var(--paligo-faint);
}
.nav-filter:focus-within { border-color: var(--paligo-primary); }
.nav-filter > svg { width: 15px; height: 15px; flex-shrink: 0; color: var(--paligo-muted); }
.nav-filter input { border: 0; background: transparent; font: inherit; font-size: 13px; color: var(--paligo-text); width: 100%; }
.nav-filter input:focus, .nav-filter input:focus-visible { outline: none; }
/* Match the search inputs' placeholder contrast (browser default is too faint). */
.nav-filter input::placeholder { color: var(--paligo-muted); opacity: 1; }
/* Chrome and Safari draw their own clear button on a search input, beside
   .nav-filter-clear, so the filter showed two. The same guard the site search
   carries. */
.nav-filter input::-webkit-search-decoration,
.nav-filter input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
.nav-filter-clear {
  flex-shrink: 0; display: grid; place-items: center; width: 22px; height: 22px;
  border: 0; border-radius: 4px; background: transparent; color: var(--paligo-faint); cursor: pointer;
}
.nav-filter-clear:hover { background: var(--paligo-surface-2); color: var(--paligo-text); }
.nav-filter-clear svg { width: 13px; height: 13px; }
.nav-filter-clear[hidden] { display: none; }

aside.site-nav ul { list-style: none; margin: 0; padding: 0; }
aside.site-nav li { position: relative; }
aside.site-nav a {
  position: relative; display: block; padding: 6px .7rem 6px 2rem;
  border-radius: var(--paligo-radius-sm); color: var(--paligo-muted);
  font-size: 14px; line-height: 1.45; text-decoration: none;
}
aside.site-nav a:hover { background: var(--paligo-surface-2); color: var(--paligo-text); text-decoration: none; }
aside.site-nav ul ul { margin-left: .8rem; }
aside.site-nav ul ul li > a { font-size: 13.5px; }
/* No special weight on the top level. It carried one back when that level was
   the publication's own structural divisions — headings standing over the
   branches beneath them. The tree now lifts the pages out from under the
   top-level article, so those entries are ordinary pages like any other, and a
   whole column of them in bold only read as loud. The current page is still
   picked out on its own, by a[aria-current="page"] below. */

/* Collapsible branches — a left caret in the reserved caret column. */
.nav-tree li.has-children > ul { display: none; }
.nav-tree li.has-children.open > ul { display: block; }
.nav-tree .nav-toggle {
  position: absolute; left: .65rem; top: 4px; z-index: 1;
  width: 1.15rem; height: 1.5rem; display: grid; place-items: center;
  border: 0; background: transparent; color: var(--paligo-faint); cursor: pointer;
}
.nav-tree .nav-toggle:hover { color: var(--paligo-text); }
/* max-width:none defeats the global `svg { max-width:100% }` reset, which would
   otherwise clamp the caret to its narrow button box (collapsing it to a sliver
   on some displays). */
.nav-tree .nav-toggle svg { width: 13px; height: 13px; max-width: none; transition: transform .15s ease; }
.nav-tree li.open > .nav-toggle svg { transform: rotate(90deg); }
aside.site-nav.filtering .nav-tree li.has-children > ul { display: block; }

/* Current page — full-width soft wash + a gradient rail at the item's left edge
   (rail is on the <a>, the row, so it never runs down a parent's children). */
aside.site-nav a[aria-current="page"] {
  color: var(--paligo-primary); font-weight: 650; background: var(--paligo-brand-wash);
}
aside.site-nav a[aria-current="page"]::before {
  content: ""; position: absolute; left: 4px; top: 5px; bottom: 5px; width: 3px;
  border-radius: 999px; background: var(--paligo-gradient);
}
aside.site-nav .nav-empty { padding: .75rem; color: var(--paligo-faint); font-size: 13px; }

/* ===== Content column ===================================================== */
.pagebody {
  display: grid; grid-template-columns: minmax(0, 1fr) var(--paligo-toc-w);
  gap: 3rem; padding: 2.5rem 3rem; align-items: start;
  /* Centered content region. width:100% fills the grid track; without it, the
     auto inline-margins would make this grid item shrink-wrap to its content —
     collapsing (and cramming the pager) on near-empty pages. */
  width: 100%; max-width: 64rem; margin-inline: auto;
}
/* "Full" layout: uncap the shell so the sidebar + content fill the width, and
   drop the centering gutter from the full-bleed bars. The grid override is
   gated to desktop so it can't out-specify the mobile single-column layout. */
:root[data-layout="full"] { --paligo-bleed-pad: clamp(1.5rem, 4vw, 3rem); }
@media (min-width: 861px) {
  :root[data-layout="full"] body {
    grid-template-columns: var(--paligo-sidebar-w) minmax(0, 1fr);
    grid-template-areas:
      "header  header"
      "sidebar content"
      "footer  footer";
  }
  :root[data-layout="full"] body:not(:has(aside.site-nav)) {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "header" "content" "footer";
  }
}
:root[data-layout="full"] .pagebody { max-width: none; }
/* Division / landing chunks (the publication home page, chapter landings) have
   no .pagebody wrapper — <main> is the direct content grid child. Match the
   .pagebody geometry (the same centered 64rem region) and cap the content at the
   reading measure, so these pages line up left-aligned with the topic pages
   instead of rendering wider, centered, and with a stretched pager. The portal
   and the full search page keep their own full-width main layouts. */
body:not(.portal):not(.searchpage) > main { padding: 2.5rem 3rem; width: 100%; max-width: 64rem; margin-inline: auto; }
body:not(.portal):not(.searchpage) > main > * { max-width: var(--paligo-measure); }
:root[data-layout="full"] body:not(.portal):not(.searchpage) > main { max-width: none; }
main { min-width: 0; max-width: var(--paligo-measure); }

/* Breadcrumb sits in its own grid row above the content column (emitted as
   <nav class="breadcrumb"><ol><li>). Inline padding matches the reading column
   so the trail lines up with the topic heading below. */
/* Same 64rem centered box as .pagebody (so it centers identically at every
   width), with 3rem left padding so the trail lines up with the reading text. */
.breadcrumb { font-size: 13px; color: var(--paligo-faint); width: 100%; max-width: 64rem; margin-inline: auto; padding: 1.25rem 3rem 0; }
.breadcrumb ol { list-style: none; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0; padding: 0; }
.breadcrumb li { display: flex; align-items: center; gap: 6px; min-width: 0; }
/* Separator is a configurable design token (default "›"); a customer overrides
   --paligo-breadcrumb-sep in their custom CSS. Coloured --paligo-faint so it is
   clearly subordinate to the text without washing out like a border tone. */
.breadcrumb li + li::before { content: var(--paligo-breadcrumb-sep); color: var(--paligo-faint); }
.breadcrumb a { color: var(--paligo-muted); text-decoration: none; }
.breadcrumb a:hover { color: var(--paligo-primary); text-decoration: underline; }
.breadcrumb [aria-current="page"] { color: var(--paligo-muted); }
/* Root home crumb: an icon-only link back to the portal. The icon sits on the
   text baseline via inline-flex; no underline on hover (icon links read cleaner
   with just a colour shift). */
.breadcrumb .crumb-home a { display: inline-flex; align-items: center; }
.breadcrumb .crumb-home a:hover { text-decoration: none; }
.breadcrumb .crumb-home svg { width: 16px; height: 16px; }

/* ===== Typography ========================================================= */
main h1 { font-size: 2rem; line-height: 1.15; font-weight: 780; letter-spacing: -.02em; margin: 0 0 .75rem; text-wrap: balance; }
main h2 { font-size: 1.4rem; line-height: 1.25; font-weight: 700; letter-spacing: -.01em; margin: 2.5rem 0 .75rem; text-wrap: balance; }
main h3 { font-size: 1.15rem; font-weight: 650; margin: 2rem 0 .5rem; }
main h4 { font-size: 1.02rem; font-weight: 650; margin: 1.5rem 0 .35rem; }
/* The chunk's own title (first heading in main) sits flush with the top of the
   content pane — its heading top-margin would otherwise push it down from the
   breadcrumb/padding. Deeper section headings keep their spacing. */
main > :first-child > header > :is(h1, h2, h3, h4, h5, h6) { margin-top: 0; }
main p, main ul, main ol, main dl, main pre, main figure, main blockquote { margin: 0 0 var(--paligo-space); }
main ul, main ol { padding-left: 1.35em; }
main li { margin: .3em 0; }
main li::marker { color: var(--paligo-faint); }
main strong { font-weight: 680; }

/* ===== Inline semantics ================================================== */
/* UI labels render bold, matching the legacy output. */
.guilabel, .guibutton, .guimenu, .guimenuitem, .guisubmenu, .guiicon { font-weight: 650; }
/* Keyboard keys render as physical key caps. Each <span class="keycap"> becomes
   one key; a keycombo of separate keycaps yields one key per element. */
.keycap {
  font-family: var(--paligo-mono); font-size: .82em; line-height: 1; white-space: nowrap;
  display: inline-block; padding: .12em .45em; margin: 0 .05em;
  border: 1px solid var(--paligo-border-strong); border-bottom-width: 2px;
  border-radius: 5px; background: var(--paligo-surface); color: var(--paligo-text);
}
/* Underlined phrases are emitted as <em class="underline">, which is italic
   by default; render the intended underline instead. */
.underline { font-style: normal; text-decoration: underline; }

/* Figures: rendered as div.figure.formalobject (not the HTML5 <figure>). */
.figure { margin: 0 0 1.5rem; }
.figure img { border: 1px solid var(--paligo-border); border-radius: var(--paligo-radius); display: block; }
/* Figure/example caption text (div.caption). Muted; alignment is themeable via
   --paligo-caption-align (default left; set to center to centre captions). */
.figure .caption, .informalfigure .caption { color: var(--paligo-muted); font-size: 13px; margin-top: .5rem; text-align: var(--paligo-caption-align, left); }
.figure .caption > :last-child, .informalfigure .caption > :last-child { margin-bottom: 0; }

/* Formal-object headings: the "Figure 2.", "Table 1.", procedure/example titles
   (div.title inside a <header>) and titled lists. A bold caption above the
   object, matching the legacy bold titles. */
.formalobject > header > .title,
.orderedlist > header > .title, .itemizedlist > header > .title,
.variablelist > header > .title { font-weight: 650; color: var(--paligo-text); margin: 0 0 .4rem; }

/* ===== Code =============================================================== */
code, kbd, samp { font-family: var(--paligo-mono); font-size: .9em; }
:not(pre) > code { background: var(--paligo-surface); border: 1px solid var(--paligo-border); padding: .1em .4em; border-radius: 5px; }
pre {
  background: var(--paligo-surface); border: 1px solid var(--paligo-border);
  border-radius: var(--paligo-radius); padding: var(--paligo-space); overflow-x: auto;
  font-family: var(--paligo-mono); font-size: 13.5px; line-height: 1.6;
}
pre code { background: none; padding: 0; border: 0; font-size: inherit; }


/* Code-copy button (code-copy.js — a platform runtime script, always shipped —
   wraps each <pre> in a .code-block and injects the button). The wrapper — not
   the scrolling <pre> — is the positioning context, so the button stays pinned
   top-right while the code scrolls under it. Shown on hover / keyboard focus;
   always visible on touch (no hover). Whether the button shows is the theme's
   call: the "code-copy" theme option toggles it (off adds .no-code-copy). */
.no-code-copy .code-copy { display: none; }
.code-block { position: relative; }
.code-block .code-copy {
  position: absolute; top: .5rem; right: .5rem; z-index: 1;
  width: 2rem; height: 2rem; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--paligo-muted); background: var(--paligo-bg);
  border: 1px solid var(--paligo-border); border-radius: 7px;
  cursor: pointer; opacity: 0; transition: opacity .12s, color .12s, border-color .12s;
}
.code-block .code-copy svg { width: 1rem; height: 1rem; }
.code-block:hover .code-copy,
.code-block:focus-within .code-copy { opacity: 1; }
.code-block .code-copy:hover { color: var(--paligo-text); border-color: var(--paligo-border-strong); }
.code-block .code-copy.copied { color: var(--paligo-primary); border-color: var(--paligo-primary); opacity: 1; }
@media (hover: none) { .code-block .code-copy { opacity: 1; } }

/* Responsive video embed: a 16:9 box the iframe fills.
   .video-container caps the width in the reading column; .videoobject is the
   aspect-ratio box (padding-top fallback for browsers without aspect-ratio). */
.video-container { margin: 0 0 var(--paligo-space); max-width: 100%; }
.video-container .videoobject {
  position: relative; width: 100%; aspect-ratio: 16 / 9;
  border-radius: var(--paligo-radius); overflow: hidden; background: #000;
}
@supports not (aspect-ratio: 16 / 9) {
  .video-container .videoobject { height: 0; padding-top: 56.25%; }
}
.video-container .videoobject > iframe,
.video-container .videoobject > video {
  position: absolute; inset: 0; width: 100%; height: 100%; border: 0;
}

/* Glossary popover (glossary-popover.js). The glossterm link keeps its normal
   look; the popover is an absolutely-positioned card carrying the definition. */
a[data-glossary-def] { text-decoration-style: dotted; cursor: help; }
.glossary-popover {
  position: absolute; z-index: 60; max-width: min(28rem, 90vw);
  background: var(--paligo-bg); color: var(--paligo-text);
  border: 1px solid var(--paligo-border-strong); border-radius: var(--paligo-radius);
  box-shadow: 0 6px 24px rgba(0,0,0,.16); padding: .6rem .8rem;
  font-size: 14px; line-height: 1.5;
}
.glossary-popover[hidden] { display: none; }

/* ===== Tables ============================================================= */
/* The wrapper carries no border (it also holds the formal title header); the
   border boxes the <table> itself, so the "Table 1. …" title sits above it.
   border-collapse:separate + overflow:hidden let border-radius round the corners
   (collapsed borders ignore it) and clip the header background to them; the
   wrapper's overflow-x still scrolls wide tables. */
.informaltable, .table { overflow-x: auto; margin: 0 0 var(--paligo-space); }
.informaltable > table, .table > table {
  margin: 0; border: 1px solid var(--paligo-border); border-radius: var(--paligo-radius);
  border-collapse: separate; border-spacing: 0; overflow: hidden;
}
main table { border-collapse: collapse; width: 100%; font-size: 14.5px; }
main th, main td { text-align: left; padding: .55em .85em; vertical-align: top; border: 0; border-bottom: 1px solid var(--paligo-border); }
/* table cells wrap their text in <p>; drop its block margins so rows aren't tall. */
main th > p, main td > p { margin: 0; }
main th > p + p, main td > p + p { margin-top: .5em; }
main thead th { background: var(--paligo-surface); font-weight: 650; color: var(--paligo-text); border-bottom: 1px solid var(--paligo-border-strong); }
main tbody tr:last-child td { border-bottom: 0; }
main tbody tr:hover td { background: color-mix(in srgb, var(--paligo-surface) 55%, transparent); }
main > table { display: block; overflow-x: auto; }
caption { caption-side: top; text-align: left; font-weight: 650; color: var(--paligo-text); margin: 0 0 .4rem; }

/* "In this section": a parent topic's list of child topics. */
.section-toc { margin: 2rem 0 0; padding-top: 1.25rem; border-top: 1px solid var(--paligo-border); }
/* Shared eyebrow style for TOC-like labels: the on-page "In this section"
   sub-TOC title and the publication landing's list-of-titles heading ("Table of
   Contents" / list of figures / tables), so both read as the same muted label. */
.section-toc-title, .lot > .title { font-weight: 650; font-size: 12px; letter-spacing: .06em; text-transform: uppercase; color: var(--paligo-muted); margin: 0 0 .6rem; }
.section-toc ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .4rem; }
.section-toc li { margin: 0; }
.section-toc a { color: var(--paligo-primary); text-decoration: none; font-weight: 550; }
.section-toc a:hover { text-decoration: underline; }

/* ===== Blockquote & admonitions ========================================== */
blockquote { margin-inline: 0; padding: .5em var(--paligo-space); border-left: 3px solid var(--paligo-border-strong); color: var(--paligo-muted); }
.admonition {
  margin: 0 0 var(--paligo-space); background: var(--paligo-surface);
  border: 1px solid var(--paligo-border); border-left: 3px solid var(--adm, var(--paligo-note)); border-radius: var(--paligo-radius);
}
.admonition > div { display: grid; grid-template-columns: auto 1fr; gap: .75rem; padding: .75rem 1rem; }
.admonition .icon { width: 20px; margin-top: 2px; }
.admonition .icon-container {
  width: 20px; height: 20px; background: var(--adm, var(--paligo-note));
  -webkit-mask: var(--adm-icon) center / contain no-repeat; mask: var(--adm-icon) center / contain no-repeat;
}
.admonition .body { min-width: 0; }
.admonition .title { font-weight: 680; color: var(--paligo-text); margin: 0 0 2px; font-size: 14px; }
.admonition .body > div > :last-child { margin-bottom: 0; }
.admonition { --adm-icon: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 8v.01"/></svg>'); }
.note      { --adm: var(--paligo-note); }
.tip       { --adm: var(--paligo-tip); --adm-icon: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round"><path d="M9 18h6M10 21h4"/><path d="M12 3a6 6 0 0 0-4 10.5c.7.7 1 1.2 1 2.5h6c0-1.3.3-1.8 1-2.5A6 6 0 0 0 12 3Z"/></svg>'); }
.important { --adm: var(--paligo-important); }
.caution, .warning, .danger { --adm-icon: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round"><path d="M12 3 2 21h20L12 3Z"/><path d="M12 10v5M12 18.5v.01"/></svg>'); }
.caution { --adm: var(--paligo-caution); }
.warning, .danger { --adm: var(--paligo-warning); }

/* ===== Glossary =========================================================== */
dl.glossary { margin: 0 0 var(--paligo-space); }
.glossentry { margin: 0 0 .75rem; }
.glossterm { font-weight: 650; color: var(--paligo-text); }

/* ===== Accordion (role=accordion -> <details>) =========================== */
details.accordion, main > details, .accordion {
  margin: 0 0 .75rem; border: 1px solid var(--paligo-border);
  border-radius: var(--paligo-radius); background: var(--paligo-surface); overflow: hidden;
}
details > summary { list-style: none; cursor: pointer; display: flex; align-items: center; gap: .75rem; padding: .75rem 1rem; font-weight: 600; }
details > summary::-webkit-details-marker { display: none; }
/* These two are qualified with `main` because the accordion is not the only
   <details> in the page: Paligo injects the cross-language switcher as one
   (.langswitch, above) and splices it into the HEADER. The switcher overrides
   everything else this block sets — its own rule gives it a height, a padding,
   a flex row and a weight — but not these two, and both leaked visibly:

     :hover         painted the switcher's LABEL --paligo-primary, i.e. the LINK
                    colour, on a control that is not a link and was the only
                    thing in the header bar that changed colour on hover at all.
     details[open]  gave it a border-bottom, so one edge of the button was a
                    different shade from the other three while it was open.

   `main` rather than the container list above (details.accordion, main >
   details, .accordion): an accordion nested inside a topic is a <details> that
   carries neither the class nor a direct-child relationship to <main>, and it
   still needs its summary styled. Location is the real distinction here — the
   accordion is content, the switcher is chrome.

   The search facet panel is a THIRD such <details> inside <main> - it became
   one again when the narrow layout needed it collapsible - and it does reach
   both of these. It opts out explicitly in its own block (padding and the
   [open] border-bottom), rather than by narrowing these selectors, which
   content accordions depend on. An earlier note here claimed the facets were
   <fieldset> and out of reach; the fieldsets are the groups INSIDE the panel,
   and were never what these rules matched. */
main details > summary:hover { color: var(--paligo-primary); }
main details[open] > summary { border-bottom: 1px solid var(--paligo-border); }
details > .accordion-body { padding: 1rem; background: var(--paligo-bg); }
details > .accordion-body > :last-child { margin-bottom: 0; }

/* ===== In-page TOC (nav.pagetoc) ========================================== */
nav.pagetoc { position: sticky; top: calc(var(--paligo-header-h) + 1.5rem); align-self: start; font-size: 13.5px; border: 0; padding: 0; }
nav.pagetoc::before {
  /* The visible heading, from the same string as the landmark's accessible
     name (theme.strings.onThisPage). It cannot be written literally here:
     CSS content is not translatable, so a Swedish build used to show a
     Swedish accessible name above an English visible heading. The property
     is declared on <html> by base.html.twig; the literal stays as the
     fallback, so a theme that declares nothing renders exactly as before. */
  content: var(--paligo-s-on-this-page, "On this page"); display: block;
  font-size: 11px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
  color: var(--paligo-faint); margin-bottom: .75rem;
}
nav.pagetoc ul { list-style: none; margin: 0; padding: 0; border-left: 1px solid var(--paligo-border); }
nav.pagetoc li { margin: 0; }
nav.pagetoc a { display: block; padding: 5px 0 5px 1rem; margin-left: -1px; border-left: 2px solid transparent; color: var(--paligo-muted); text-decoration: none; }
nav.pagetoc a:hover { color: var(--paligo-text); }
nav.pagetoc a.active { color: var(--paligo-primary); border-left-color: var(--paligo-primary); font-weight: 600; }
nav.pagetoc .lvl-3 a { padding-left: 1.75rem; }
nav.pagetoc .lvl-4 a { padding-left: 2.5rem; }
.pagebody:not(:has(nav.pagetoc)) { grid-template-columns: minmax(0, 1fr); }

/* ===== Pager + footer ===================================================== */
/* The pager stays where it is emitted and is grid-placed
   into the content column (see the grid block) so it aligns with the reading
   text — no DOM move. Padding-inline matches main's reading column. */
/* The pager is relocated into <main> on the server, so it flows in the
   reading column and inherits its width automatically — no box/measure matching.
   Just a top gap from the content above it. */
.pager {
  display: grid; grid-template-columns: 1fr 1fr; gap: .75rem;
  margin: 3rem 0 0; padding: 0;
}
.pager a { display: flex; flex-direction: column; gap: 2px; padding: .75rem 1rem; border: 1px solid var(--paligo-border); border-radius: var(--paligo-radius); color: var(--paligo-text); }
.pager a:hover { border-color: var(--paligo-primary); background: var(--paligo-surface); text-decoration: none; }
.pager .dir { font-size: 12px; color: var(--paligo-muted); font-weight: 600; }
.pager .ttl { font-weight: 600; color: var(--paligo-primary); }
.pager a.next { text-align: right; }
.pager a.next .ttl, .pager a.next .dir { align-self: flex-end; }

.site-footer {
  /* No top margin: a transparent margin here shows the page background between
     the (full-height) sidebar and the footer. Spacing before the footer is the
     pager's / content's bottom padding instead. */
  margin-top: 0; border-top: 1px solid var(--paligo-border); background: var(--paligo-surface);
  padding: 2rem var(--paligo-bleed-pad);
  display: flex; flex-wrap: wrap; align-items: center; gap: 1rem 2rem;
  font-size: 13.5px; color: var(--paligo-muted);
}
.site-footer .brand-name { font-size: 14px; }
.site-footer nav { display: flex; flex-wrap: wrap; gap: 1rem; margin-left: auto; }
.site-footer nav a { color: var(--paligo-muted); }
.site-footer .copyright { flex-basis: 100%; color: var(--paligo-faint); font-size: 12.5px; padding-top: .75rem; border-top: 1px solid var(--paligo-border); }
/* Customer-supplied footer (filled verbatim by Paligo). Replaces the standard
   footer contents, so it is the sole full-width child of the footer; the
   customer's own markup controls its layout. */
.site-footer .custom-footer { flex-basis: 100%; width: 100%; }
/* Footer identity as the site LOGO (theme option "footer-logo"). Capped by
   height so a wide lockup and a square mark both sit on the footer's line; the
   light/dark variants are the header's own brand-logo-* pair, so nothing new
   decides which one shows. */
.site-footer .footer-brand-link { display: inline-flex; align-items: center; }
.site-footer .footer-logo { height: 24px; width: auto; max-width: 220px; object-fit: contain; }
/* The portal's featured topics repeated in the footer (theme option
   "footer-featured"). Laid out as a wrapping row so it sits on the footer's
   existing single line beside the identity, and pushed right the way
   .site-footer nav already is. */
.site-footer .footer-featured ul {
  list-style: none; display: flex; flex-wrap: wrap; gap: .5rem 1.25rem;
  margin: 0; padding: 0;
}
.site-footer .footer-featured a { color: var(--paligo-muted); text-decoration: none; }
.site-footer .footer-featured a:hover { color: var(--paligo-primary); text-decoration: underline; }

nav.tocopen, nav.toc { display: none !important; }


/* ===== Search =============================================================
 * The theme's own search box (partials/search-box.html.twig), driven by
 * js/pf-search.js through the Pagefind API.
 *
 * This section replaces ~120 rules that styled Pagefind's generated classes.
 * Three things went away with them and are worth knowing did not need replacing:
 *
 *   - the !important on nearly every declaration. Pagefind's stylesheet is
 *     Svelte-scoped, so its selectors out-specified plain classes and load order
 *     decided the rest. These are our own classes; nothing competes.
 *   - the mark placement. Pagefind absolutely positioned the magnifier and the
 *     clear button for a field of `64px * scale`, so changing a field's height
 *     drifted both marks unless the scale moved with it. They are flex items in
 *     a flex form here: the height is whatever the field is.
 *   - the click-target gymnastics. Pagefind set position:relative on the result
 *     LINK, so a stretched ::after inset to the title text rather than the card;
 *     the fix was forcing static on its element. Our card is the link's
 *     containing block by construction.
 */
.site-search { position: relative; min-width: 0; }

/* Restore `hidden`. The UA sheet hides it with a bare `[hidden]` selector, so
   ANY class rule that sets display beats it - and three below do (the clear
   button, the "see all" link, the "load more" button), which showed an empty
   box its clear button and its results footer. Scoping the guard to a
   descendant of .site-search out-specifies all three, so no !important. The
   script toggles the ATTRIBUTE and never touches display, so nothing needs to
   win back. Same guard .nav-filter-clear carries, for the same reason. */
.site-search [hidden] { display: none; }

/* -- the field ----------------------------------------------------------- */
.site-search__form {
  display: flex; align-items: center; gap: .5rem;
  height: 38px; padding: 0 .5rem 0 .75rem;
  background: var(--paligo-surface);
  border: 1px solid var(--paligo-border);
  border-radius: var(--paligo-radius-pill);
  color: var(--paligo-muted);
}
.site-search__form:focus-within { border-color: var(--paligo-primary); background: var(--paligo-bg); }
.site-search__icon { width: 16px; height: 16px; flex: none; }
.site-search__input {
  flex: 1 1 auto; min-width: 0;
  border: 0; background: transparent; font: inherit; font-size: 14px;
  color: var(--paligo-text);
}
.site-search__input::placeholder { color: var(--paligo-muted); opacity: 1; }
.site-search__input:focus, .site-search__input:focus-visible { outline: none; }
/* Chrome and Safari draw their own clear affordance on a search input; ours is
   the one that knows how to reset the results. */
.site-search__input::-webkit-search-decoration,
.site-search__input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
.site-search__clear {
  flex: none; width: 22px; height: 22px; padding: 0;
  display: grid; place-items: center;
  border: 0; border-radius: 999px; background: transparent;
  color: var(--paligo-muted); cursor: pointer;
}
.site-search__clear svg { width: 13px; height: 13px; }
.site-search__clear:hover { background: var(--paligo-surface-2); color: var(--paligo-text); }
/* The "/" shortcut hint, and the one rule that used to be a bug: it shares the
   corner with the clear button, so it goes when there is a query to clear. Keyed
   off the input's own state rather than off the other control's presence. */
.site-search__hint {
  flex: none; font: inherit; font-size: 11px; line-height: 1;
  padding: 3px 5px; color: var(--paligo-faint);
  border: 1px solid var(--paligo-border); border-radius: 5px;
}
.site-search__form:focus-within .site-search__hint,
.site-search__input:not(:placeholder-shown) ~ .site-search__hint { display: none; }

/* -- the results panel --------------------------------------------------- */
/* Excluded from scroll anchoring. On the search page the panel is in normal
   flow, so a results list that grows and shrinks as you type keeps handing
   Chromium a different anchor node to hold stationary - and it holds it by
   adjusting the scroll offset, which walks the view a few pixels on every
   keystroke. Not the same thing as the caret scroll pinScrollWhileTyping()
   undoes: that one happens after the keypress and can be corrected, this one IS
   the layout and cannot. The dropdown mounts never needed it - their panel is
   absolute and so never in flow - but the rule costs nothing there. */
.site-search__panel { min-width: 0; overflow-anchor: none; }
.site-search[data-search-mode="dropdown"] .site-search__panel {
  position: absolute; z-index: 60; top: calc(100% + 6px); right: 0;
  /* At least as wide as the field, never narrower than 30rem, never wider than
     the viewport. The header's field is ~320px and wants the panel WIDER than
     itself, so this used to be a flat 30rem - which then cut the hero's 620px
     field down to 480 and hung the panel off its right edge. max(100%, 30rem)
     serves both: 30rem in the header, field-width in the hero. */
  width: min(max(100%, 30rem), calc(100vw - 2rem));
  max-height: min(60vh, 520px); overflow-y: auto;
  padding: .5rem;
  background: var(--paligo-bg);
  border: 1px solid var(--paligo-border); border-radius: var(--paligo-radius);
  box-shadow: var(--paligo-shadow-lg);
  text-align: left;
}
.site-search__status { margin: 0; padding: .35rem .5rem; font-size: 12.5px; color: var(--paligo-muted); }
.site-search__status:empty { display: none; }
.site-search__results { list-style: none; margin: 0; padding: 0; }

/* A result is one click target: the card IS the link, so there is no stretched
   pseudo-element and no containing-block puzzle. */
.site-search__result { margin: 0; border-radius: var(--paligo-radius-sm); }
.site-search__result-link {
  display: block; padding: .5rem; color: var(--paligo-text); text-decoration: none;
}
.site-search__result:hover, .site-search__result.is-active { background: var(--paligo-surface); }
.site-search__result.is-active { outline: 2px solid var(--paligo-focus); outline-offset: -2px; }
/* The card is ONE anchor around a title and an excerpt, so the generic
   `a:hover` rule - underline, link colour - claimed the whole of it: the excerpt
   underlined along with the title and both turned blue. Held on the anchor
   itself because that is the only place it can be held: text-decoration
   propagates from the element that sets it, and a descendant cannot take it
   back, so styling the excerpt could never have undone it. `a:hover` weighs
   (0,1,1) and the class alone weighed (0,1,0), which is why load order did not
   save it either. */
.site-search__result-link:hover { color: var(--paligo-text); text-decoration: none; }
.site-search__result-title { display: block; font-weight: 600; }
/* Only the title takes the hover underline - the affordance belongs to the thing
   you are clicking through to, not to its summary. */
.site-search__result-link:hover .site-search__result-title { text-decoration: underline; }
.site-search__result-excerpt {
  margin: .15rem 0 0; font-size: 13px; line-height: 1.5; color: var(--paligo-muted);
}
.site-search__result-excerpt mark {
  background: var(--paligo-brand-wash); color: inherit; border-radius: 3px; padding: 0 2px;
}

/* Sub-results are the matched headings inside a page: their own targets, so
   their own links. */
.site-search__subs { list-style: none; margin: 0 0 .35rem; padding: 0 0 0 1.25rem; }
.site-search__sub { margin: 0; }
.site-search__sub-link {
  display: block; padding: .25rem .5rem; border-radius: var(--paligo-radius-sm);
  font-size: 13px; color: var(--paligo-muted); text-decoration: none;
}
/* text-decoration: none repeated on hover, not redundantly: the generic
   `a:hover` underlines every anchor, and this control signals hover with a fill
   and a colour instead. Same reason on the "see all" link below. */
.site-search__sub-link:hover {
  background: var(--paligo-surface-2); color: var(--paligo-primary); text-decoration: none;
}

/* -- "see all" (dropdown) and "load more" (page) ------------------------- */
.site-search__all, .site-search__more {
  display: block; margin: .35rem 0 0; padding: .5rem;
  font: inherit; font-size: 13px; text-align: center;
  color: var(--paligo-primary); text-decoration: none;
  background: transparent; border: 0; border-radius: var(--paligo-radius-sm); cursor: pointer;
}
/* These two are styled as one pair above, and only one of them is an anchor -
   so the generic `a:hover` underline made the link and the button behave
   differently on hover while looking identical at rest. */
.site-search__all:hover, .site-search__more:hover {
  background: var(--paligo-surface); text-decoration: none;
}
.site-search__all.is-active { background: var(--paligo-surface); outline: 2px solid var(--paligo-focus); outline-offset: -2px; }

/* -- the header mount ---------------------------------------------------- */
.header-search { flex: 0 1 320px; }

/* -- the portal hero mount ----------------------------------------------- */
.portal-search .site-search__form { height: 52px; padding-inline: 1rem .75rem; font-size: 1rem; }
.portal-search .site-search__input { font-size: 1rem; }
.portal-search .site-search__icon { width: 18px; height: 18px; }
/* On a coloured or image hero the field cannot borrow the page tokens: the hero
   pair is ABSOLUTE while the page tokens follow the reader's scheme, so a
   --paligo-bg field became a dark hole punched into a light hero in dark mode.
   Derive it from the hero ink instead - a hint of the text colour mixed into the
   ground, which is contrast-safe because the pair is chosen to contrast. */
.portal-hero .site-search__form {
  color: var(--paligo-hero-text, var(--paligo-muted));
  background: color-mix(in srgb, var(--paligo-hero-text, var(--paligo-text)) 12%, transparent);
  border-color: color-mix(in srgb, var(--paligo-hero-text, var(--paligo-text)) 35%, transparent);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
}
.portal-hero .site-search__form:focus-within {
  background: color-mix(in srgb, var(--paligo-hero-text, var(--paligo-text)) 18%, transparent);
  border-color: var(--paligo-hero-text, var(--paligo-primary));
}
.portal-hero .site-search__input { color: var(--paligo-hero-text, var(--paligo-text)); }
.portal-hero .site-search__input::placeholder {
  color: color-mix(in srgb, var(--paligo-hero-text, var(--paligo-muted)) 65%, transparent);
}
/* The two marks in the corner have to come from the ink as well, and did not:
   the clear button kept --paligo-muted and the hint kept --paligo-faint, so both
   were painted in PAGE grey on a hero that is the same pale image in either
   scheme - present, addressable, and almost invisible. The hint's border was
   already derived here, which is what made the omission easy to miss: the box
   around the glyph tracked the hero while the glyph itself did not.
   :hover is the same point - --paligo-surface-2 is an opaque page colour, which
   would have put a near-white chip on the hero the moment you reached for it. */
.portal-hero .site-search__clear { color: color-mix(in srgb, var(--paligo-hero-text, var(--paligo-muted)) 80%, transparent); }
.portal-hero .site-search__clear:hover {
  color: var(--paligo-hero-text, var(--paligo-text));
  background: color-mix(in srgb, var(--paligo-hero-text, var(--paligo-surface-2)) 22%, transparent);
}
.portal-hero .site-search__hint {
  color: color-mix(in srgb, var(--paligo-hero-text, var(--paligo-faint)) 70%, transparent);
  border-color: color-mix(in srgb, var(--paligo-hero-text, var(--paligo-border)) 35%, transparent);
}
/* -- the search page mount ----------------------------------------------- */
/* grid-template-rows is DECLARED, and has to be. The facet column spans both
   rows, and with both rows left implicit (auto) a facet panel taller than the
   results column had its surplus height distributed across the two tracks -
   inflating row 1 past the field's own 52px and pushing the results heading
   down with it. Most visible on a query with no hits, where the results column
   is at its shortest: the field sat at the top and "No results for ..." floated
   ~29px below where it belonged, which reads as a broken layout.

   `1fr` on row 2 makes it the flexible track, so a spanning item's extra height
   lands there and row 1 stays the height of the field. min-content on row 1
   does NOT work: it is still an intrinsic sizing function, so it absorbs
   distributed space just as auto does (measured - same 29px). */
.searchpage .site-search {
  display: grid; grid-template-columns: var(--paligo-toc-w, 15rem) minmax(0, 1fr);
  grid-template-rows: auto 1fr;
  gap: 0 3rem; align-items: start;
}
.searchpage .site-search__form { grid-column: 2; height: 52px; padding-inline: 1rem .75rem; }
.searchpage .site-search__input { font-size: 1rem; }
.searchpage .site-search__facets { grid-column: 1; grid-row: 1 / span 2; }
.searchpage .site-search__panel { grid-column: 2; margin-top: 1.5rem; }

/* No facets, no facet column. The track and its gap are declared, so
   display:none on the facet panel still reserved 15rem + 3rem and pushed the
   field and the results off to the right - which is what an index carrying no
   filters shows, and what every preview shows. Same collapse .pagebody does
   when a page has no on-this-page nav. */
.searchpage .site-search:has(.site-search__facets[hidden]) { grid-template-columns: minmax(0, 1fr); }
.searchpage .site-search:has(.site-search__facets[hidden]) > .site-search__form,
.searchpage .site-search:has(.site-search__facets[hidden]) > .site-search__panel { grid-column: 1; }
/* Cards here too, not a ruled list. This was a separated list - square corners
   and a hairline under each row - which meant the hover fill painted a
   hard-edged rectangle while the same fill in the dropdown was rounded, so the
   two mounts disagreed about what a result looks like. Rounding cannot simply be
   added to the old treatment: border-radius clips a border to the curve, so a
   box with only a bottom border shows a bent line rather than a separator.
   Rounded fill and a small gap do the separating instead - the same way the
   dropdown has always done it.

   The larger radius, not the dropdown's: these rows carry a 1rem padding and a
   6px corner reads as almost square at that size. Horizontal padding grows with
   it, so the text is not sitting in the curve. */
.searchpage .site-search__result { border-radius: var(--paligo-radius); }
.searchpage .site-search__result + .site-search__result { margin-top: .25rem; }
/* One step up from the dropdown's .5rem, not two. The vertical padding was
   1rem, which doubled the dropdown's while the title grew by a fifth - so it
   read as loose rather than as a larger scale. Uniform, so the card is the
   same card with more air rather than a differently proportioned one. */
.searchpage .site-search__result-link { padding: .75rem; }
.searchpage .site-search__result-title { font-size: 1.05rem; }
.searchpage .site-search__more {
  margin: 1.5rem auto 0; max-width: 16rem;
  border: 1px solid var(--paligo-border-strong); border-radius: var(--paligo-radius-pill);
  color: var(--paligo-text);
}

/* -- facets -------------------------------------------------------------- */
.site-search__facets { min-width: 0; }
/* The panel is a <details>, but on the WIDE layout it is not a disclosure: it
   is a column beside the results and stays open, so the summary is styled back
   into the plain heading it used to be and made inert. Narrow screens turn it
   into a real control (see the breakpoint below). */
.site-search__facets-panel > summary {
  list-style: none; display: flex; align-items: center; gap: .5rem;
  cursor: default; pointer-events: none;
  /* Undoing `details > summary` and `main details[open] > summary` in the
     accordion block above, which this panel is a <details> inside <main> and so
     matches. Without the reset it arrived with the accordion's 12px/16px
     padding and, once open, a border-bottom under FILTER RESULTS - a divider
     the wide layout never had. Kept as explicit resets rather than by
     narrowing the accordion selectors: those are load-bearing for content
     accordions, and this panel is the one thing that has to opt out. */
  padding: 0; border-bottom: 0;
}
.site-search__facets-panel[open] > summary { border-bottom: 0; }
.site-search__facets-panel > summary::-webkit-details-marker { display: none; }
/* The summary is inert to the POINTER above, but a <summary> stays in the tab
   order and stays keyboard-operable, and pointer-events does not reach that.
   So Enter on the focused heading closed the panel - taking the checkboxes and
   Clear filters with it - on a layout that hides the caret and blocks clicking
   it back open. Recoverable with a second Enter, but nothing on screen says so.

   Keeping the content rendered regardless of [open] is the fix that belongs in
   the stylesheet: on this layout the panel is a column, so the open state has
   no business deciding whether its contents exist. ::details-content is the
   only selector that reaches them - `display` on the children does not, the UA
   hides them below CSS.

   It needs Chrome 131 / Safari 18.4 / Firefox 139, so it is the modern half of
   a pair: search-ui.js also refuses the toggle on this layout, which is what
   covers older browsers. Either alone leaves a gap. */
.site-search__facets-panel::details-content { content-visibility: visible; block-size: auto; }
.site-search__facets-caret { display: none; }
/* Hidden on the wide layout with the caret, and for the same reason: nothing
   there is collapsed, so the checkboxes speak for themselves. */
.site-search__facets-active { display: none; }
.site-search__facets-title {
  font-size: 12px; font-weight: 500; letter-spacing: .07em; text-transform: uppercase;
  color: var(--paligo-faint); margin: 0 0 .75rem;
}
.site-search__facet { border: 0; margin: 0 0 1.25rem; padding: 0; }
/* The group name is a HEADING for the checkboxes under it, and has to read as
   one. It used to be 13px/500 in --paligo-text with no margin, which is what an
   option row is - so, sitting flush against the first one and lacking only its
   checkbox indent, it read as a row whose checkbox had failed to render.

   Ranked between the panel title and the values instead: smaller and muted
   enough not to be mistaken for a value, heavier and darker than the uppercase
   title above it. Sentence case stays - the text is an author's taxonomy name
   ("0. Business line"), not our label, and uppercasing it would both mangle it
   and collide with the title's own treatment. */
.site-search__facet-name {
  padding: 0; margin: 0 0 .35rem;
  font-size: 12px; font-weight: 600; color: var(--paligo-muted);
}
.site-search__facet-option {
  display: flex; align-items: center; gap: .5rem;
  padding: .3rem 0; font-size: 13px; color: var(--paligo-text); cursor: pointer;
}
.site-search__facet-option input { accent-color: var(--paligo-primary); flex: none; }
.site-search__facet-label { flex: 1 1 auto; min-width: 0; }
.site-search__facet-count { flex: none; font-size: 12px; color: var(--paligo-faint); }
/* Clears every ticked filter. A quiet text button, not a primary action: it
   undoes the reader's own narrowing, so it should be findable without competing
   with the results beside it. Rendered only while something is ticked. */
.site-search__facets-clear {
  border: 0; background: none; padding: 0; margin: 0;
  font: inherit; font-size: 12px; color: var(--paligo-muted);
  cursor: pointer; text-align: start;
}
.site-search__facets-clear:hover { color: var(--paligo-primary); text-decoration: underline; }
.site-search__facets-clear:focus-visible { outline: 2px solid var(--paligo-primary); outline-offset: 2px; }

@media (max-width: 64rem) {
  /* One column: field, then FACETS, then results - the facet panel moved above
     the results here because filters the reader cannot see are filters they do
     not use. Rows are explicit because document order puts the panel last; the
     desktop grid's two-track sizing does not apply, nothing spans. */
  .searchpage .site-search { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto auto; }
  .searchpage .site-search__form,
  .searchpage .site-search__panel,
  .searchpage .site-search__facets { grid-column: 1; }
  .searchpage .site-search__form { grid-row: 1; }
  .searchpage .site-search__facets { grid-row: 2; }
  .searchpage .site-search__panel { grid-row: 3; }
  .searchpage .site-search__facets { margin: 1.25rem 0; }

  /* ...and HERE it is a disclosure. Stacked, three taxonomies of checkboxes
     between the field and the first result push every result below the fold,
     so the panel starts closed (search-ui.js decides that from the same 64rem)
     and the summary becomes the control that opens it. */
  .searchpage .site-search__facets-panel {
    border: 1px solid var(--paligo-border); border-radius: var(--paligo-radius);
  }
  .searchpage .site-search__facets-panel > summary {
    cursor: pointer; pointer-events: auto; padding: .75rem 1rem;
  }
  /* Here it IS a disclosure, so hand the open state back its job - the wide
     layout's ::details-content override would otherwise keep the panel's
     contents on screen while it is closed, which is the whole thing this
     breakpoint exists to avoid. */
  .searchpage .site-search__facets-panel:not([open])::details-content {
    content-visibility: hidden;
  }
  .searchpage .site-search__facets-panel > summary:hover { background: var(--paligo-surface-2); }
  .searchpage .site-search__facets-panel > summary .site-search__facets-caret {
    display: block; width: 13px; height: 13px; max-width: none;
    margin-inline-start: auto; color: var(--paligo-faint);
    transition: transform .15s ease;
  }
  .searchpage .site-search__facets-panel[open] > summary .site-search__facets-caret {
    transform: rotate(90deg);
  }
  .searchpage .site-search__facets-active {
    display: grid; place-items: center; min-width: 1.25rem; height: 1.25rem;
    padding-inline: .35rem; border-radius: 999px;
    background: var(--paligo-primary); color: var(--paligo-bg);
    font-size: 11px; font-weight: 600; letter-spacing: 0;
  }
  /* The title carried the panel's only bottom margin; inside a bordered
     disclosure the padding does that job, and the groups need their own inset. */
  .searchpage .site-search__facets-panel > summary .site-search__facets-title,
  .searchpage .site-search__facets-panel > summary { margin: 0; }
  .searchpage .site-search__facets-panel > .site-search__facet {
    margin: 0 1rem 1rem; padding-top: .25rem;
  }
  .searchpage .site-search__facets-panel > .site-search__facet:first-of-type { padding-top: 0; }
  /* Inside the bordered disclosure the button needs the same inset the groups
     get; the fieldset rule above does not reach it. */
  .searchpage .site-search__facets-panel > .site-search__facets-clear { margin: 0 1rem 1rem; }
}
@media (max-width: 60rem) {
  /* The header field collapses to the icon that links to the search page. Both
     halves together, which they were not: the field was hidden here while the
     icon only appeared at 860px, so between 861px and 960px there was no way to
     search at all - a comment describing a swap that only half happened. */
  .header-search { display: none; }
  .search-toggle { display: grid; }
}

/* ===== Cookie consent ===================================================== */
.paligo-consent {
  position: fixed; inset-inline: 0; inset-block-end: 0; z-index: 1000;
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: var(--paligo-space);
  padding: .75em var(--paligo-space); background: var(--paligo-surface); color: var(--paligo-text);
  border-block-start: 1px solid var(--paligo-border); font-size: .95rem; box-shadow: var(--paligo-shadow-lg);
}
.paligo-consent__message { margin: 0; }
.paligo-consent__link { color: var(--paligo-primary); }
.paligo-consent__actions { display: flex; gap: .5em; }
.paligo-consent__button { font: inherit; padding: .4em 1em; border: 1px solid var(--paligo-border); border-radius: var(--paligo-radius-sm); background: var(--paligo-bg); color: var(--paligo-primary); cursor: pointer; }
.paligo-consent__button:hover { border-color: var(--paligo-primary); }
/* The primary action is INK, not the accent, and the reason is that its label
   has to invert with the scheme. A fixed white label on var(--paligo-primary)
   reads 6.5:1 in light and 2.7:1 in dark, because the dark accent is a LIGHT
   blue - it has to be, to carry contrast as text on a near-black page. So the
   one place the accent is used as a GROUND rather than as ink is the one place
   it cannot work, and it was the accept button on a consent banner. The ink pair
   inverts by construction: 18.1:1 light, 17.4:1 dark. */
.paligo-consent__button--primary { background: var(--paligo-text); color: var(--paligo-bg); border-color: var(--paligo-text); }

/* ===== Help Center portal (out/index.html) ===============================
 * White hero on top, a soft grey cards band below, white cards on the grey. */
body.portal { display: flex; flex-direction: column; min-height: 100vh; grid-template-columns: none; background: var(--paligo-surface-2); }
body.portal > main { flex: 1; padding: 0; max-width: none; margin: 0; background: var(--paligo-surface-2); }
.portal-hero {
  /* Top padding min is 4.25rem (> the fixed 60px header) so the hero content
     clears the overlaid transparent topbar, even when the heading shrinks on
     small screens. */
  position: relative; text-align: center; padding: clamp(4.25rem, 8vw, 5.5rem) 1.5rem 2.75rem;
  /* --paligo-hero-bg is the flat hero colour (the hero-background option, else
     the accent); --paligo-hero-text is the ink chosen to contrast it.
     --paligo-hero-text can appear WITHOUT a ground: an image hero has an ink
     (forced via hero-contrast) and no flat shade. The reverse never happens,
     which is the case that would be unreadable. So the ground's fallback is the
     plain page background, and the ink's is the page text. */
  background: var(--paligo-hero-bg, var(--paligo-bg));
  color: var(--paligo-hero-text, var(--paligo-text));
  border-bottom: 1px solid var(--paligo-border);
}
/* On a coloured hero the search field cannot borrow the page tokens. Two
   reasons: its focus ring is --paligo-primary, which IS the accent, so on an
   accent background it would be invisible; and the hero pair is ABSOLUTE while
   the page tokens are theme-aware, so a --paligo-bg field became a dark hole
   punched into a light hero whenever the reader was in dark mode.

   So the field is derived from the hero pair instead - a hint of the text
   colour mixed into the background. That keeps it in-family with the hero,
   contrast-safe (the pair is chosen to contrast), and identical in both colour
   schemes, which is what the rest of the hero already is. */

/* A hero background image (inline style from $portal-background) is shown AS IT
   IS. There used to be a translucent wash of the page background over it, to
   keep near-black text readable in either scheme; the cost was that no author
   could ever show their own image, and washing it out is not something they can
   undo from their side. Brightening the image is, so the choice belongs to
   them - and the hero-contrast option carries the text, which is what the wash
   was really compensating for.

   The stacking context stays: the hero's own children need to sit above the
   background image, and so does the search field. */
.portal-hero[style*="background-image"] { background-size: cover; background-position: center; }
.portal-hero[style*="background-image"] > * { position: relative; z-index: 1; }
/* Nothing is laid over the image — no wash, no scrim, no gradient — and the hero
   text carries no shadow or glow. An author who chose a picture gets that
   picture, at full strength, all the way to the top of the hero.

   This is a deliberate constraint rather than an oversight, so it is worth
   naming what it gives up. The topbar floats TRANSPARENT over the hero until it
   scrolls, so the brand, the subtitle and the top-nav links sit directly on the
   photograph, and no single ink suits every photograph. Readability there is the
   author's to solve, with the tools that do not touch the image: the
   hero-contrast option picks the ink (and with it the logo variant), and the
   image itself can be brightened or darkened before upload. Both are reversible
   from the author's side. An overlay is not — which is why previous attempts at
   one, first a full-surface wash and then a top-weighted scrim, were each
   removed in turn.

   The stacking rule above still earns its place: the hero's children have to sit
   above the background image regardless. */
/* Over an image the field cannot be derived from the hero pair: there IS no
   ground, only an ink (base.html.twig emits --paligo-hero-text alone for an
   image). Deriving from the page background instead is what the wash used to
   make safe, and without it a dark-mode reader got a near-black field punched
   into a bright photograph.

   So the field becomes a scrim of its own ink — translucent, so the image still
   reads through it, and guaranteed to contrast with the text sitting on it
   because it is made of that text's colour. Independent of the reader's scheme,
   like the rest of the hero. */


/* The portal reuses the topbar chrome (shared t:brand + theme toggle / injected
   language switcher) but overlays the hero: fixed to the top and transparent, so
   the hero background shows through, then its background, blur and border fade in
   once the page is scrolled (portal.js toggles .is-scrolled). The base .topbar
   rules still supply the flex layout, height and bleed padding. */
body.portal .topbar {
  position: fixed; top: 0; left: 0; right: 0; z-index: 40;
  background: transparent; border-bottom-color: transparent;
  -webkit-backdrop-filter: none; backdrop-filter: none;
  transition: background .25s ease, border-color .25s ease,
    -webkit-backdrop-filter .25s ease, backdrop-filter .25s ease;
}
/* Before it scrolls, the topbar is transparent and floats ON the hero, so its
   own tokens are the wrong ones: --paligo-text is chosen to contrast the PAGE
   background, and the hero is a flat colour that may be nothing like it (the
   shipped accent is a dark indigo, against which near-black brand text is
   unreadable). Borrow the hero's text colour for as long as the hero is what
   sits behind it; .is-scrolled below restores the page tokens along with the
   page background.

   --paligo-hero-text is set for a flat hero AND for an image one (base.html.twig
   answers `auto` light over an image). The var() fallbacks below are therefore
   only reached by a hero that is neither — a plain hero on the page background —
   which is the one case where the page tokens ARE the right ones. They used to
   be reached by an image hero too, on the grounds that a wash kept them correct;
   nothing is laid over the image any more, and that was this rule painting
   near-black brand text onto photographs. */
body.portal .topbar:not(.is-scrolled) .brand,
body.portal .topbar:not(.is-scrolled) .brand:hover,
body.portal .topbar:not(.is-scrolled) .top-nav a {
  color: var(--paligo-hero-text, var(--paligo-text));
}
body.portal .topbar:not(.is-scrolled) .brand-name .brand-sub {
  color: color-mix(in srgb, var(--paligo-hero-text, var(--paligo-muted)) 75%, transparent);
  border-left-color: color-mix(in srgb, var(--paligo-hero-text, var(--paligo-border)) 35%, transparent);
}
body.portal .topbar:not(.is-scrolled) .icon-btn {
  color: var(--paligo-hero-text, var(--paligo-muted));
  background: color-mix(in srgb, var(--paligo-hero-text, var(--paligo-surface)) 10%, transparent);
  border-color: color-mix(in srgb, var(--paligo-hero-text, var(--paligo-border)) 30%, transparent);
}
body.portal .topbar:not(.is-scrolled) .icon-btn:hover,
body.portal .topbar:not(.is-scrolled) .top-nav a:hover {
  background: color-mix(in srgb, var(--paligo-hero-text, var(--paligo-surface-2)) 20%, transparent);
  color: var(--paligo-hero-text, var(--paligo-text));
}
/* The injected language switcher is the one control up here the theme does not
   render itself, and it was the one left behind: a surface-filled, page-bordered
   button sitting on the hero. Give its SUMMARY the same treatment as .icon-btn,
   which is the pill it sits next to.

   Only the summary. The dropdown it opens is an opaque --paligo-bg panel that
   floats BELOW the bar, over the hero rather than on it, so it keeps the page
   tokens — hero text on a page-coloured menu is exactly the mismatch this block
   exists to fix. */
body.portal .topbar:not(.is-scrolled) .langswitch > summary {
  color: var(--paligo-hero-text, var(--paligo-text));
  background: color-mix(in srgb, var(--paligo-hero-text, var(--paligo-surface)) 10%, transparent);
  border-color: color-mix(in srgb, var(--paligo-hero-text, var(--paligo-border)) 30%, transparent);
}
body.portal .topbar:not(.is-scrolled) .langswitch > summary:hover {
  background: color-mix(in srgb, var(--paligo-hero-text, var(--paligo-surface-2)) 20%, transparent);
}
/* The globe and chevron are muted against the label, in both grounds. */
body.portal .topbar:not(.is-scrolled) .langswitch > summary .globe,
body.portal .topbar:not(.is-scrolled) .langswitch > summary .chevron {
  color: color-mix(in srgb, var(--paligo-hero-text, var(--paligo-muted)) 75%, transparent);
}
body.portal .topbar.is-scrolled {
  background: color-mix(in srgb, var(--paligo-bg) 85%, transparent);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  border-bottom-color: var(--paligo-border);
}
.portal-hero h1 { font-size: clamp(1.9rem, 4vw, 2.6rem); font-weight: 780; letter-spacing: -.02em; margin: 0; text-wrap: balance; }
/* Optional sub-line under the banner heading (theme option "hero-subtitle").
   Measure-capped so it wraps to a readable line rather than the hero's full
   width, and derived from the hero ink so it works on a flat hero, an image one
   and the plain one alike. */
.portal-hero .portal-sub {
  margin: .9rem auto 0;
  max-width: 46ch;
  font-size: clamp(1rem, 1.3vw, 1.15rem);
  line-height: 1.5;
  color: color-mix(in srgb, var(--paligo-hero-text, var(--paligo-muted)) 78%, transparent);
  text-wrap: pretty;
}
.portal-search { max-width: 620px; margin: 1.75rem auto 0; text-align: left; }


/* ===== Full search page (search.html) ==================================== */
/* Flex column so the footer is pushed to the bottom of the viewport when the
   results are short (the sticky bar and main keep working as flex items),
   mirroring body.portal / the topic-page grid. */
body.searchpage { display: flex; flex-direction: column; min-height: 100vh; }

/* Content spans the same app-shell width as the topic page, aligned via the same
   bleed padding (no narrow reading-column cap). */
.searchpage main { flex: 1 0 auto; max-width: none; margin: 0; padding: 2.5rem var(--paligo-bleed-pad) 5rem; }
.searchpage main h1 { font-size: 1.9rem; font-weight: 780; letter-spacing: -.02em; margin: 0 0 1.5rem; }

@media (max-width: 64rem) {
  .searchpage main { padding-inline: 1.5rem; }
}
.portal-cards { max-width: 60rem; margin: 0 auto; padding: 2.75rem 1.5rem 4rem; }
.portal-cards h2 { font-size: 12px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: var(--paligo-faint); margin: 0 0 1rem; }
/* Featured content (role="promoted-content"): prominent cards, 3 per row, with
   partial last rows centered (flex, not grid). Plain cards — the accent is the
   hover state, so featured reads as inviting rather than pre-selected. */
.portal-featured { max-width: 60rem; margin: 0 auto; padding: 2.75rem 1.5rem 0; }
.portal-featured h2 { font-size: 12px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: var(--paligo-muted); margin: 0 0 1rem; }
/* Flex so a partial last row centers (e.g. a 4th card sits centered, not
   bottom-left). The basis is the exact per-row fraction, so a FULL row fills the
   width (no clumping) while a partial row centers. The fraction is adjusted per
   breakpoint below (3 → 2 → 1 across the row). */
.portal-featured .portal-grid { display: flex; flex-wrap: wrap; justify-content: center; gap: 1rem; }
.portal-featured .portal-card { flex: 0 1 calc((100% - 2rem) / 3); min-width: 0; }

/* Browse-all: a quieter list (not cards) of every top-level topic — secondary to
   the featured cards above. Multi-column; tight, uniform single-line rows. */
.portal-categories { max-width: 60rem; margin: 0 auto; padding: 2.25rem 1.5rem 3.5rem; }
.portal-list { list-style: none; margin: 0; padding: 0; display: grid; gap: .1rem 1.5rem; grid-template-columns: repeat(3, minmax(0, 1fr)); }
.portal-list > li { min-width: 0; }
.portal-list-link { display: flex; align-items: center; gap: .6rem; padding: .45rem .5rem; border-radius: var(--paligo-radius-sm); color: var(--paligo-text); text-decoration: none; min-width: 0; }
.portal-list-link:hover { background: var(--paligo-surface); color: var(--paligo-primary); text-decoration: none; }
/* Clean, visible icon — a white chip with the brand tint, so it reads against
   the section's tinted background (the old brand-wash box blended in). */
.portal-list-link .portal-card__icon { flex: none; width: 26px; height: 26px; border-radius: 7px; background: var(--paligo-bg); color: var(--paligo-primary); }
.portal-list-link .portal-card__icon svg { width: 15px; height: 15px; }
/* One ellipsized line keeps every row the same height, so wrapping never breaks
   the grid alignment. */
.portal-list-title { min-width: 0; font-weight: 500; font-size: .95rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Extra items are hidden until "show more"; revealing animates them in. Uses a
   class (not the hidden attr) so display can flip and the animation can run. */
.portal-list > li.portal-extra { display: none; }
.portal-categories.is-expanded .portal-list > li.portal-extra { display: block; animation: portal-reveal .28s ease both; }
@keyframes portal-reveal { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: none; } }
/* Show more/less: a proper secondary button (design system), not a bare link. */
.portal-categories .show-more {
  display: block; margin: 1.5rem auto 0; padding: .55rem 1.4rem; cursor: pointer;
  font: inherit; font-weight: 600; font-size: .9rem; color: var(--paligo-text);
  background: var(--paligo-bg); border: 1px solid var(--paligo-border-strong);
  border-radius: var(--paligo-radius-pill); transition: background .15s ease, border-color .15s ease, color .15s ease;
}
.portal-categories .show-more:hover { background: var(--paligo-surface); border-color: var(--paligo-primary); color: var(--paligo-primary); }
.portal-grid { display: grid; gap: 1rem; grid-template-columns: repeat(3, minmax(0, 1fr)); }
.portal-card {
  display: flex; gap: 1rem; align-items: center; padding: 1.15rem 1.25rem;
  border: 1px solid var(--paligo-border); border-radius: var(--paligo-radius-lg);
  background: var(--paligo-bg); color: var(--paligo-text); text-decoration: none;
  transition: border-color .15s ease, box-shadow .15s ease;
}
.portal-card:hover { border-color: var(--paligo-primary); box-shadow: var(--paligo-shadow); text-decoration: none; }
.portal-card__icon { flex-shrink: 0; width: 42px; height: 42px; border-radius: 11px; background: var(--paligo-brand-wash); color: var(--paligo-primary); display: grid; place-items: center; }
.portal-card__icon svg { width: 22px; height: 22px; }
.portal-card__body { min-width: 0; }
.portal-card__title { display: block; font-weight: 650; font-size: 1.05rem; line-height: 1.3; }
.portal-card__count { color: var(--paligo-faint); font-size: .85rem; }
.portal-langbar { display: flex; flex-wrap: wrap; gap: 1rem; }
.portal-langbar a { color: var(--paligo-muted); text-transform: capitalize; }
.portal-cta { margin: 0; text-align: center; }
.portal-button {
  display: inline-block; padding: .8em 1.75em; border-radius: var(--paligo-radius-pill);
  background: var(--paligo-gradient); color: #fff; font-weight: 600; text-decoration: none;
  box-shadow: 0 8px 24px -8px rgba(98,108,255,.6); transition: box-shadow .15s ease, filter .15s ease;
}
.portal-button:hover, .portal-button:focus-visible { color: #fff; text-decoration: none; filter: brightness(1.06); box-shadow: 0 12px 30px -8px rgba(98,108,255,.7); }

/* ===== Responsive ========================================================= */
@media (max-width: 1080px) {
  .pagebody { grid-template-columns: minmax(0, 1fr); }
  nav.pagetoc { display: none; }
}
@media (max-width: 860px) {
  body {
    grid-template-columns: minmax(0, 1fr);
    /* Single column, but keep a row for the breadcrumb — without a "crumb" area
       the breadcrumb (grid-area:crumb) lands in an implicit track and distorts
       the whole layout. */
    grid-template-areas: "header" "crumb" "content" "footer";
  }
  /* Header must not overflow on narrow screens: let the brand truncate and the
     search icon, brand and tools keep their size. The search FIELD is not
     mentioned because it is already gone by here, hidden at 60rem. */
  .topbar { gap: .5rem; }
  .brand { min-width: 0; }
  .brand-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  body > aside.site-nav {
    position: fixed; top: var(--paligo-header-h); bottom: 0; left: 0; width: min(320px, 82vw);
    height: auto; z-index: 55; transform: translateX(-102%); transition: transform .22s ease; box-shadow: var(--paligo-shadow-lg);
  }
  body.nav-open > aside.site-nav { transform: translateX(0); }
  .nav-scrim { position: fixed; inset: var(--paligo-header-h) 0 0; background: rgba(10,10,20,.45); z-index: 50; opacity: 0; pointer-events: none; transition: opacity .2s; }
  body.nav-open .nav-scrim { opacity: 1; pointer-events: auto; }
  .menu-toggle { display: grid; }
  /* Drop the theme toggle from the bar (it moves to the sidebar footer below).
     The search swap is not here: it happens at 60rem, above. Language switcher
     stays. */
  .header-tools .theme-toggle { display: none; }
  /* The drawer becomes a flex column (it is position:fixed, so full viewport
     height): the tree scrolls, the footer theme toggle pins to the bottom. */
  aside.site-nav { display: flex; flex-direction: column; }
  /* Top padding replaces the spacing the (now-hidden) filter box used to give,
     so the tree isn't flush against the header. */
  aside.site-nav > .nav-inner { position: static; max-height: none; flex: 1 1 auto; padding-top: .75rem; }
  .nav-footer { display: flex; flex: none; align-items: center; gap: .5rem; padding: .75rem; border-top: 1px solid var(--paligo-border); }
  /* Bigger touch targets: more vertical padding on the tree rows (and nudge the
     collapse caret to stay aligned). */
  aside.site-nav a { padding-top: 10px; padding-bottom: 10px; }
  .nav-tree .nav-toggle { top: 8px; }
  /* Hide the sidebar tree filter in the mobile drawer — full-text find is in the
     header search; the filter is desktop-only clutter here. */
  .nav-filter-wrap { display: none; }
  /* Compact the language switcher: show just the code (EN) instead of the full
     name (English), on both content pages and the portal. */
  .langswitch-label { display: none; }
  .langswitch-label-short { display: inline; }
  .pagebody, body > main { padding: 1.5rem 1rem; }
  /* With no sidebar, the reading column should fill the width — the measure cap
     otherwise leaves a large gap on the right (nothing occupies where the
     sidebar was). */
  main { max-width: none; }
  /* Breadcrumb aligns to the content's mobile inset (1rem), not the desktop 3rem
     centered box. */
  .breadcrumb { max-width: none; margin-inline: 0; padding-inline: 1rem; }
  .portal-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .portal-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* Featured cards: two per row, filling the width (was clumping in the middle
     at this size because the basis stayed at the 3-up fraction). */
  .portal-featured .portal-card { flex-basis: calc((100% - 1rem) / 2); }
}
@media (min-width: 861px) { .nav-scrim { display: none; } }
@media (max-width: 560px) {
  .brand-name .brand-sub { display: none; }
  .portal-grid { grid-template-columns: 1fr; }
  .portal-list { grid-template-columns: 1fr; }
  /* Featured cards reflow to full width, one per row. */
  .portal-featured .portal-card { flex-basis: 100%; }
}
