/* ==========================================================================
   PUDL, the Pleasantly Usable Design Language (pronounced "puddle").
   Version 0.18.0.

   PUDL gives every affordance one visual representation, so that somebody
   who has learned it once can use any application built with it on sight.

   Elevation signals interactivity:
     - Raised, with a hairline border and an inset highlight, is pressable (.btn).
     - Sunken, with an inset shadow, is an input (.form-input).
     - Flat is display only (.kv-table cells, .form-input[readonly]).
   Colour is never the only signal: every meaningful state carries a glyph.
   One .btn-primary per visible context.

   State is marked with the ARIA attribute that states it, so the eye and
   assistive technology are told the same thing: aria-current for where the
   reader is, aria-pressed and aria-checked for toggles, aria-selected for
   the tab of a tab panel. The older .active class still works as an alias.

   A project themes PUDL with a stylesheet loaded after this one. It may
   change any colour token, including the background and the topbar, and
   the font tokens, within the restrictions in the README. It never changes
   what a raised, sunken or flat surface means.

   Extracted from the Andoneer Design Language v2 reference page
   (Andoneer repository, mockups/adl-v2-reference.html, commit 097a9c7).
   ========================================================================== */

/* Inter, by Rasmus Andersson, under the SIL Open Font License 1.1
   (fonts/Inter-LICENSE.txt). One variable file covers weights 400 to 700,
   and its optical-size axis tightens the face at heading sizes. */
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("fonts/InterVariable.woff2") format("woff2");
}
@font-face {
  font-family: "Inter";
  font-style: italic;
  font-weight: 100 900;
  font-display: swap;
  src: url("fonts/InterVariable-Italic.woff2") format("woff2");
}

:root {
  /* === The palette ==========================================================
     These are the values a project theme sets. Everything under "Derived"
     further down is computed from them, so a new palette is this block and
     its dark counterpart, and nothing else. Set them on :root and on
     [data-theme="dark"], because the derived tokens are computed there. */

  /* Page and text. */
  --bg: #f3f4f6;
  --surface: #fbfbfc;
  --surface-alt: #e4e7eb;
  --text: #1b1f24;
  --text-muted: #5f6873;
  --border: #cfd4da;

  /* Accent and status. --pr is the old name for --positive; a theme that
     sets --pr still reaches --positive, and new themes set --positive. */
  --accent: #2c5282;
  --accent-hover: #1f3d63;
  --on-accent: #ffffff;
  --warn: #b86e1c;
  --danger: #b42318;
  --danger-hover: #8f1c13;
  --pr: #2e7d4f;
  --positive: var(--pr);          /* done, shipped, healthy */

  /* Lighting. --light is the colour of the light falling on a raised control
     and --shade the colour of its shadow. --lit sets how strongly the light
     shows and --depth how strongly the shadow does. A light theme wants a
     strong highlight and a soft shadow; a dark theme wants the reverse. */
  --light: #ffffff;
  --shade: #0e1621;
  --lit: 90%;
  --depth: 18%;

  /* Topbar. It keeps its own colours so that it can stay dark on a light
     page, as the default palette does. */
  --tb-bg: #262b31;
  --tb-fg: #f3f4f6;

  /* Fonts. A theme may change these within the roles the README sets out.
     Inter ships with PUDL in fonts/, so it does not depend on what the
     reader has installed; the system face stands in until it loads. */
  --font: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-display: var(--font);
  --mono: ui-monospace, "Cascadia Code", "SF Mono", Consolas, monospace;

  /* === Fixed by the language ================================================
     A theme does not change these. */

  /* The type scale. Every size PUDL uses is one of these. */
  --text-2xs: 11px;      /* section labels, meta lines, badges */
  --text-xs: 12px;       /* chips, small buttons, dock tabs */
  --text-sm: 13px;       /* labels, tabs, list rows, secondary text */
  --text-md: 14px;       /* controls, table values, dialog text */
  --text-base: 15px;     /* body text */
  --text-lg: 17px;       /* card and dialog titles, the brand, h4 */
  --text-xl: 20px;       /* h3 */
  --text-2xl: 24px;      /* h2 */
  --text-3xl: 30px;      /* h1 */

  /* The spacing grid, on a 4px base. Layout spacing, between and around
     components, comes from these. A control's own padding is part of the
     control's metrics and is fixed with the control. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;

  --radius: 12px;        /* cards, panels, windows */
  --radius-sm: 7px;      /* buttons, entries, tabs */
  --radius-pill: 999px;

  /* Glyphs. Every glyph PUDL draws in its chrome is one of these SVG masks,
     filled with the element's own colour, so it looks the same on every
     platform, can never turn into a colour emoji, and follows the theme. */
  --glyph-search: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='6.5' cy='6.5' r='4.75' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='M10.2 10.2L14.5 14.5' stroke='black' stroke-width='2.2' stroke-linecap='round'/%3E%3C/svg%3E");
  --glyph-caret: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 5.5h9L8 11z'/%3E%3C/svg%3E");
  --glyph-back: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M10 3L5 8l5 5' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --glyph-triangle: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 2l7 12H1z'/%3E%3C/svg%3E");
  --glyph-square: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect x='3' y='3' width='10' height='10' rx='1'/%3E%3C/svg%3E");
  --glyph-circle: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='5'/%3E%3C/svg%3E");
  --glyph-ring: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='4.2' fill='none' stroke='black' stroke-width='1.6'/%3E%3C/svg%3E");
  --glyph-diamond: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 2l6 6-6 6-6-6z'/%3E%3C/svg%3E");
  --glyph-warning: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.8l6.6 12H1.4z' fill='none' stroke='black' stroke-width='1.6' stroke-linejoin='round'/%3E%3Cpath d='M8 6.2v3.6' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3Ccircle cx='8' cy='11.9' r='1'/%3E%3C/svg%3E");
  --glyph-open: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6 3.5h6.5V10M12.5 3.5L4 12' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --glyph-minimize: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.5h9' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
  --glyph-maximize: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect x='3.2' y='3.2' width='9.6' height='9.6' rx='1.5' fill='none' stroke='black' stroke-width='1.8'/%3E%3C/svg%3E");
  --glyph-restore: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect x='2.5' y='5.5' width='8' height='8' rx='1.2' fill='none' stroke='black' stroke-width='1.6'/%3E%3Cpath d='M5.5 5.5V3.6a1.1 1.1 0 0 1 1.1-1.1h5.8a1.1 1.1 0 0 1 1.1 1.1v5.8a1.1 1.1 0 0 1-1.1 1.1h-1.9' fill='none' stroke='black' stroke-width='1.6'/%3E%3C/svg%3E");
  --glyph-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.2 4.2l7.6 7.6M11.8 4.2l-7.6 7.6' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
  --glyph-branch: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M5 1.5V9h8' fill='none' stroke='black' stroke-width='1.4'/%3E%3C/svg%3E");
  --glyph-sort: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.5 6.5L8 3l3.5 3.5zM4.5 9.5L8 13l3.5-3.5z'/%3E%3C/svg%3E");
  --glyph-sort-up: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 10L8 4.5l4.5 5.5z'/%3E%3C/svg%3E");
  --glyph-sort-down: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 6L8 11.5 12.5 6z'/%3E%3C/svg%3E");
  --glyph-info: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='6.2' fill='none' stroke='black' stroke-width='1.6'/%3E%3Ccircle cx='8' cy='5' r='1'/%3E%3Cpath d='M8 7.4v4.2' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
  --glyph-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='6.2' fill='none' stroke='black' stroke-width='1.6'/%3E%3Cpath d='M5.2 8.2l2 2 3.8-4' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --glyph-empty: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M1.8 9.2l2-5.4h8.4l2 5.4v3.6a.8.8 0 0 1-.8.8H2.6a.8.8 0 0 1-.8-.8z' fill='none' stroke='black' stroke-width='1.3' stroke-linejoin='round'/%3E%3Cpath d='M1.8 9.2h3.6l.9 1.6h3.4l.9-1.6h3.6' fill='none' stroke='black' stroke-width='1.3' stroke-linejoin='round'/%3E%3C/svg%3E");
  --glyph-stop: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M5.4 1.8h5.2l3.6 3.6v5.2l-3.6 3.6H5.4l-3.6-3.6V5.4z' fill='none' stroke='black' stroke-width='1.6' stroke-linejoin='round'/%3E%3Cpath d='M8 4.8v4' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3Ccircle cx='8' cy='11' r='1'/%3E%3C/svg%3E");

  /* === Derived ==============================================================
     Computed from the palette. A theme should not need to set any of these. */

  /* Inputs are sunken, so they take the surface with only a touch of light. */
  --input-bg: color-mix(in srgb, var(--light) var(--lit), var(--surface));

  /* Raised controls: lit from the top, shading to the recessed tint. */
  --raise-top: color-mix(in srgb, var(--light) var(--lit), var(--surface-alt));
  --raise-grad: linear-gradient(var(--raise-top), var(--surface-alt));
  --raise-grad-hover: linear-gradient(
      color-mix(in srgb, var(--text) 3%, var(--raise-top)),
      color-mix(in srgb, var(--text) 6%, var(--surface-alt)));
  --raise-active-bg: color-mix(in srgb, var(--shade) calc(var(--depth) * 0.45), var(--surface-alt));
  --raise-border: color-mix(in srgb, var(--shade) calc(var(--depth) * 0.9), var(--surface-alt));
  --raise-border-hover: color-mix(in srgb, var(--shade) calc(var(--depth) * 1.6), var(--surface-alt));
  --raise-shadow:
      inset 0 1px color-mix(in srgb, var(--light) var(--lit), transparent),
      0 1px 2px color-mix(in srgb, var(--shade) var(--depth), transparent);
  --raise-active-shadow: inset 0 1px 3px color-mix(in srgb, var(--shade) calc(var(--depth) * 1.2), transparent);
  --entry-shadow: inset 0 1px 2px color-mix(in srgb, var(--shade) calc(var(--depth) * 0.7), transparent);

  /* A recessed band, such as the tab bar or a switch's track: darker than
     the surface around it and shaded from above. */
  --recess-bg: color-mix(in srgb, var(--shade) calc(var(--depth) * 0.35), var(--surface-alt));
  --recess-shadow: inset 0 2px 3px color-mix(in srgb, var(--shade) calc(var(--depth) * 0.6), transparent);

  --shadow-btn-primary:
      inset 0 1px rgb(255 255 255 / 0.25),
      0 1px 2px color-mix(in srgb, var(--shade) calc(var(--depth) * 1.2), transparent);
  --shadow-card:
      0 1px 3px color-mix(in srgb, var(--shade) calc(var(--depth) * 0.6), transparent),
      0 0 0 1px color-mix(in srgb, var(--shade) calc(var(--depth) * 0.25), transparent);

  /* Both rings derive from their colour tokens, so a theme's accent reaches
     the focus ring with no further work. */
  --focus-ring: color-mix(in srgb, var(--accent) 26%, transparent);
  --focus-ring-danger: color-mix(in srgb, var(--danger) 26%, transparent);

  /* The dialog panel is always one step brighter than the page, so it reads
     as a card lifted off it. On a light page that is --surface. */
  --dialog-bg: var(--surface);

  /* The current section tab opens into what lies below the tab bar, so it
     takes that colour. Under a master-detail layout's toolbar that is
     --surface, the default; a page whose content sits on --bg sets this to
     var(--bg) on the bar or above it. */
  --section-current-bg: var(--surface);

  /* Topbar chrome. */
  --tb-grad: linear-gradient(color-mix(in srgb, var(--tb-fg) 8%, var(--tb-bg)), var(--tb-bg));
  --tb-border: color-mix(in srgb, #000 40%, var(--tb-bg));
  --tb-hi: color-mix(in srgb, var(--tb-fg) 12%, transparent);
  --tb-brand: var(--tb-fg);
  --tb-chrome-fg: color-mix(in srgb, var(--tb-fg) 80%, var(--tb-bg));
  --tb-chrome-hover-fg: var(--tb-fg);
  /* Links on the topbar, the brand among them, hover in the accent mixed
     into the topbar's own foreground, which reads on a dark bar and on a
     light one alike. A theme may set it. */
  --tb-link-hover: color-mix(in srgb, var(--accent) 45%, var(--tb-fg));
  --tb-chip: linear-gradient(
      color-mix(in srgb, var(--tb-fg) 19%, transparent),
      color-mix(in srgb, var(--tb-fg) 10%, transparent));
  --tb-chip-shadow:
      inset 0 1px color-mix(in srgb, var(--tb-fg) 22%, transparent),
      0 1px 2px color-mix(in srgb, #000 35%, transparent);
}

[data-theme="dark"] {
  /* === The dark palette ==================================================== */
  --bg: #121417;
  --surface: #1a1d21;
  --surface-alt: #23272d;
  --text: #e6e8eb;
  --text-muted: #9aa3ad;
  --border: #353b43;

  --accent: #6b9bd1;
  --accent-hover: #8db3e0;
  --on-accent: #121417;
  --warn: #d9954a;
  --danger: #e06156;
  --danger-hover: #f0857b;
  --pr: #5fb37f;
  --positive: var(--pr);

  --light: #e8eef6;
  --shade: #000000;
  --lit: 7%;
  --depth: 48%;

  --tb-bg: #262b31;
  --tb-fg: #e6e8eb;

  /* On a dark page --surface is barely lighter than --bg, so the dialog
     takes the next step up to keep reading as lifted. */
  --dialog-bg: var(--surface-alt);
}

:root[data-theme="dark"] { color-scheme: dark; }
:root[data-theme="light"] { color-scheme: light; }

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }

/* The hidden attribute always hides, even on a component that sets its own
   display, which would otherwise beat the browser's rule. hidden="until-found"
   is left to the browser, which hides it by other means so that in-page
   search can still reveal it. */
[hidden]:not([hidden="until-found"]) { display: none !important; }
body {
  font-family: var(--font);
  background: var(--bg);
  color: var(--text);
  font-size: var(--text-base);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

/* Headings, from the type scale. :where() gives these no weight at all, so
   any class a component or project puts on a heading wins over them. */
:where(h1, h2, h3, h4) {
  font-family: var(--font-display); font-weight: 700;
  line-height: 1.25; letter-spacing: -0.01em;
  margin: 0 0 var(--space-3);
}
:where(h1) { font-size: var(--text-3xl); }
:where(h2) { font-size: var(--text-2xl); }
:where(h3) { font-size: var(--text-xl); }
:where(h4) { font-size: var(--text-lg); }

/* Tabular numerals wherever numbers are data, so identifiers, counts, money
   and timestamps line up. Running prose keeps proportional figures, because
   Inter's tabular feature also widens the hyphen, which spaces out every
   hyphenated word. <time>, <data> and .num mark data inside prose. */
table, input, select, textarea, time, data, output, code, kbd, samp, .num,
.kv-table, .form-input, .badge, .chip, .filter-chip, .seg, .topbar-pill,
.md-item, .md-row {
  font-variant-numeric: tabular-nums;
}

/* === Glyphs ==============================================================
   Every chrome glyph is a pseudo-element drawn by this one rule: a square
   the size of the text, filled with the element's colour through an SVG
   mask named by --glyph. Each component below sets which glyph, and its
   size where the text's own size is not right. */
.badge::before, .form-error::before, .menu-btn::after, .md-back::before,
.notice::before, .toast::before, .data-table th[aria-sort] > :is(a, button)::after,
.notice-close::before, .toast-close::before, .empty-state::before,
.page-link[rel="prev"]::before, .page-link[rel="next"]::after {
  content: ""; flex: none;
  width: var(--glyph-size, 1em); height: var(--glyph-size, 1em);
  background: currentColor;
  -webkit-mask: var(--glyph) center / contain no-repeat;
  mask: var(--glyph) center / contain no-repeat;
}

/* === Topbar / brand chrome ============================================== */
.topbar {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 20px;
  background: var(--tb-grad);
  border-bottom: 1px solid var(--tb-border);
  box-shadow: inset 0 1px var(--tb-hi);
}
.brand {
  display: inline-flex; align-items: center; gap: var(--space-2);
  font-family: var(--font-display);
  font-size: var(--text-lg); font-weight: 700; letter-spacing: -0.01em;
  color: var(--tb-brand); text-decoration: none;
}
/* The page's link colours are made for the page, not for the topbar, so a
   plain link on the topbar takes the topbar's colours instead, and the
   brand, like it, hovers in --tb-link-hover. */
.topbar a:not([class]) { color: var(--tb-chrome-fg); }
.brand:hover, .topbar a:not([class]):hover { color: var(--tb-link-hover); }
.topbar-chrome {
  display: inline-flex; align-items: center; gap: var(--space-2);
}
/* A topbar pill is raised, so it is always a link or a button, never a label. */
.topbar-pill {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 12px; border-radius: var(--radius-sm);
  font-size: var(--text-sm); font-weight: 600;
  color: var(--tb-chrome-fg);
  background: var(--tb-chip); box-shadow: var(--tb-chip-shadow);
  text-decoration: none;
}
.topbar-pill:hover { color: var(--tb-chrome-hover-fg); }
/* The theme toggle shares the chip styling on the topbar chrome. */
.theme-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px;
  padding: 0; border-radius: var(--radius-sm);
  font-size: var(--text-md); line-height: 1; font-family: var(--font);
  color: var(--tb-chrome-fg);
  background: var(--tb-chip); box-shadow: var(--tb-chip-shadow);
  border: 0; cursor: pointer;
}
.theme-toggle:hover { color: var(--tb-chrome-hover-fg); }

/* === Section tabs: a notebook ============================================
   After the notebook widget of desktop toolkits. The bar is a recessed
   band. Each tab the reader can go to is a raised tab standing on the
   band's baseline, because it can be pressed. The tab for where the reader
   is, marked aria-current, is flat and lit, a little taller, and open at
   the bottom into the content below, so the page visibly hangs from it.

   The bar's baseline is drawn as part of the bar's own background, and the
   current tab paints over it from inside the bar. Nothing reaches past the
   bar's edge, which matters because the bar scrolls sideways on a narrow
   screen, and a box that scrolls clips whatever reaches past its edges. */
.app-section-bar, .tablist {
  display: flex; flex: none; align-items: flex-end; gap: var(--space-1);
  padding: 6px var(--space-4) 0;
  overflow-x: auto;
  background: var(--recess-bg);
  box-shadow: inset 0 -1px var(--border), var(--recess-shadow);
}
.section-tab, .tablist > [role="tab"] {
  padding: 5px 14px 4px;
  font: 600 var(--text-sm) var(--font);
  color: var(--text); text-decoration: none;
  white-space: nowrap; cursor: pointer;
  background: var(--raise-grad);
  border: 1px solid var(--raise-border); border-bottom: 0;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  box-shadow: inset 0 1px color-mix(in srgb, var(--light) var(--lit), transparent);
}
.section-tab:hover, .tablist > [role="tab"]:hover { background: var(--raise-grad-hover); border-color: var(--raise-border-hover); }
.section-tab:active, .tablist > [role="tab"]:active { background: var(--raise-active-bg); }
.section-tab:focus-visible, .tablist > [role="tab"]:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--focus-ring); }
.section-tab[aria-current]:not([aria-current="false"]),
.section-tab.active,
.tablist > [role="tab"][aria-selected="true"] {
  padding: 7px 14px 6px;
  color: var(--text);
  background: var(--section-current-bg);
  border-color: var(--border);
  box-shadow: none;
}
.section-tab[aria-current]:not([aria-current="false"]):focus-visible,
.section-tab.active:focus-visible,
.tablist > [role="tab"][aria-selected="true"]:focus-visible { box-shadow: 0 0 0 3px var(--focus-ring); }

/* === Tabs within a page ==================================================
   The ARIA tab pattern: a .tablist of role="tab" buttons over role="tabpanel"
   panels, inside a .tabs wrapper. They look like section tabs, since to the
   reader both are tabs; the difference is that section tabs go to other
   addresses and these switch panels in place. pudl-tabs.js shows the chosen
   panel, moves between tabs with the arrow keys, and keeps the chosen
   panel in the address's fragment, so it can be linked to and survives a
   reload. Without the script the tab list is hidden and every panel shows,
   one after another. */
.tabs { --section-current-bg: var(--surface); }
.tabs:not(.tabs-ready) > .tablist { display: none; }
.tabs > [role="tabpanel"] {
  padding: var(--space-4);
  background: var(--section-current-bg);
  border: 1px solid var(--border); border-top: 0;
}
.tabs:not(.tabs-ready) > [role="tabpanel"] { border-top: 1px solid var(--border); margin-bottom: var(--space-3); }
.tabs > [role="tabpanel"]:focus-visible { outline: none; box-shadow: inset 0 0 0 3px var(--focus-ring); }

/* A region that pudl-regions.js is replacing dims a little while the new
   content is on its way, after a moment, so a fast answer never flickers. */
[data-region][aria-busy="true"] { opacity: .6; transition: opacity .15s .12s; cursor: progress; }

/* === Buttons (raised; one look) ========================================= */
/* Every button is raised. There is no flat. Disabled is dimmed.
   At most one .btn-primary per visible context. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  padding: 7px 14px; border-radius: var(--radius-sm);
  font-size: var(--text-md); font-weight: 600; font-family: var(--font);
  color: var(--text); cursor: pointer; text-decoration: none;
  white-space: nowrap;
  border: 1px solid var(--raise-border);
  background: var(--raise-grad);
  box-shadow: var(--raise-shadow);
  transition: background .1s, border-color .1s, box-shadow .1s;
}
.btn:hover {
  background: var(--raise-grad-hover);
  border-color: var(--raise-border-hover);
  text-decoration: none;
}
.btn:active {
  background: var(--raise-active-bg);
  box-shadow: var(--raise-active-shadow);
}
.btn:focus-visible {
  outline: none;
  box-shadow: var(--raise-shadow), 0 0 0 3px var(--focus-ring);
}
/* A toggle button latched on stays pressed in. aria-pressed carries the
   state to assistive technology as well as to the eye, and the state shows
   through elevation, not colour. A project flips the attribute. */
.btn[aria-pressed="true"],
.btn[aria-pressed="true"]:hover {
  background: var(--raise-active-bg);
  border-color: var(--raise-border-hover);
  box-shadow: var(--raise-active-shadow);
}
.btn[aria-pressed="true"]:focus-visible {
  box-shadow: var(--raise-active-shadow), 0 0 0 3px var(--focus-ring);
}
.btn[disabled], .btn.is-disabled {
  opacity: .45; cursor: not-allowed;
  box-shadow: var(--raise-shadow);
  background: var(--raise-grad);
}
.btn-sm { padding: 4px 10px; font-size: var(--text-xs); }
.btn-primary {
  color: var(--on-accent);
  border-color: color-mix(in srgb, var(--accent) 62%, #000);
  background: linear-gradient(color-mix(in srgb, var(--accent) 78%, #fff), var(--accent));
  box-shadow: var(--shadow-btn-primary);
}
.btn-primary:hover {
  color: var(--on-accent);
  background: linear-gradient(color-mix(in srgb, var(--accent) 88%, #fff), color-mix(in srgb, var(--accent) 94%, #000));
}
.btn-primary:active {
  background: color-mix(in srgb, var(--accent) 86%, #000);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.32);
}
.btn-danger {
  color: var(--danger); border-color: color-mix(in srgb, var(--danger) 35%, var(--raise-border));
}
.btn-danger:hover {
  color: var(--on-accent);
  background: linear-gradient(color-mix(in srgb, var(--danger) 78%, #fff), var(--danger));
  border-color: color-mix(in srgb, var(--danger) 62%, #000);
}

/* Icon button: raised square for row actions (drag, remove, archive). */
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border-radius: var(--radius-sm);
  background: var(--raise-grad); border: 1px solid var(--raise-border);
  box-shadow: var(--raise-shadow);
  color: var(--text-muted); cursor: pointer;
}
.icon-btn:hover { color: var(--text); background: var(--raise-grad-hover); border-color: var(--raise-border-hover); }
.icon-btn:focus-visible { outline: none; box-shadow: var(--raise-shadow), 0 0 0 3px var(--focus-ring); }
.icon-btn.danger:hover { color: var(--danger); }

/* === Links ============================================================== */
a, .link {
  color: var(--accent); text-decoration: underline;
  text-decoration-thickness: 1px; text-underline-offset: 2px;
}
a:hover, .link:hover {
  color: var(--accent-hover);
  text-decoration-thickness: 2px;
}
.link-muted {
  color: var(--text-muted); text-decoration: underline;
  text-decoration-thickness: 1px; text-underline-offset: 2px;
}
.link-muted:hover { color: var(--text); }
button.link {
  border: 0; background: none; padding: 0; font: inherit; cursor: pointer;
  color: var(--accent); text-decoration: underline;
}

/* === Forms ============================================================== */
.form-group { display: flex; flex-direction: column; gap: 6px; margin-bottom: var(--space-4); }
.form-label { font-size: var(--text-sm); font-weight: 600; color: var(--text); }
.form-input, .form-select, .form-textarea {
  padding: 8px 11px; font-size: var(--text-md); font-family: var(--font);
  background: var(--input-bg); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  width: 100%;
  box-shadow: var(--entry-shadow);
}
.form-textarea { min-height: 5em; resize: vertical; line-height: 1.5; }
.form-input:focus, .form-select:focus, .form-textarea:focus {
  border-color: var(--accent); outline: none;
  box-shadow: var(--entry-shadow), 0 0 0 3px var(--focus-ring);
}
/* Read-only fields render as flat display, not input chrome. */
.form-input[readonly], .form-select[disabled] {
  background: transparent; border-color: transparent; box-shadow: none;
  padding-inline: 0; cursor: default;
}
::placeholder { color: var(--text-muted); opacity: .5; }

/* Checkbox + label as one inline control. */
.check {
  display: inline-flex; align-items: center; gap: var(--space-2);
  font-size: var(--text-md); cursor: pointer;
}
.check input { width: 16px; height: 16px; margin: 0; accent-color: var(--accent); }

/* Toggle switch (a boolean preference). The track is sunken, since it is
   the slot the thumb runs in, and the thumb is raised, since it is what the
   reader presses. When the switch is on the track fills with the accent
   and the thumb slides to the far end, so position says it as well as
   colour. */
.switch {
  display: inline-flex; align-items: center; gap: 10px;
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: var(--text); cursor: pointer;
}
.switch-track {
  position: relative; flex: none;
  width: 40px; height: 22px; border-radius: var(--radius-pill);
  background: var(--recess-bg); border: 1px solid var(--border);
  box-shadow: var(--entry-shadow);
  transition: background .15s, border-color .15s;
}
.switch-thumb {
  position: absolute; top: 2px; inset-inline-start: 2px;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--raise-grad);
  border: 1px solid var(--raise-border);
  box-shadow: var(--raise-shadow);
  transition: transform .15s;
}
.switch[aria-checked="true"] .switch-track {
  background: var(--accent); border-color: var(--accent-hover);
}
.switch[aria-checked="true"] .switch-thumb { transform: translateX(18px); }
.switch:dir(rtl)[aria-checked="true"] .switch-thumb { transform: translateX(-18px); }
.switch:hover .switch-thumb { border-color: var(--raise-border-hover); }
.switch:focus-visible { outline: none; }
.switch:focus-visible .switch-track {
  box-shadow: var(--entry-shadow), 0 0 0 3px var(--focus-ring);
}

.form-error {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--text-sm); color: var(--danger); margin: var(--space-1) 0 var(--space-3);
}
.form-error::before { --glyph: var(--glyph-warning); }

/* A field whose value the server refused is marked aria-invalid="true",
   with its .form-error tied to it by aria-describedby. It takes a danger
   border and a danger focus ring, and the message beside it carries the
   glyph, so the state never rests on the border's colour alone. */
.form-input[aria-invalid="true"], .form-select[aria-invalid="true"], .form-textarea[aria-invalid="true"] {
  border-color: var(--danger);
}
.form-input[aria-invalid="true"]:focus, .form-select[aria-invalid="true"]:focus, .form-textarea[aria-invalid="true"]:focus {
  border-color: var(--danger);
  box-shadow: var(--entry-shadow), 0 0 0 3px var(--focus-ring-danger);
}
.form-input:disabled, .form-select:disabled, .form-textarea:disabled { opacity: .55; cursor: not-allowed; }

/* A required field's label carries an asterisk, drawn by the stylesheet
   from the field's own required attribute and hidden from assistive
   technology, which hears "required" from the field itself. */
.form-group:has(:required) > .form-label::after {
  content: "*" / "";
  margin-inline-start: 3px; color: var(--danger);
}
.form-help { margin: 0; font-size: var(--text-sm); color: var(--text-muted); }
.form-group > .form-error { margin: 0; }

.form-fieldset {
  margin: 0 0 var(--space-4); padding: var(--space-3) var(--space-4) var(--space-4);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.form-fieldset > legend { padding: 0 var(--space-1); font-size: var(--text-sm); font-weight: 600; }
.form-options { display: flex; flex-direction: column; gap: var(--space-2); }
.form-options.inline { flex-direction: row; flex-wrap: wrap; gap: var(--space-2) var(--space-5); }

/* A file field: its button is a raised button, the file name flat text. */
.form-file { font: inherit; font-size: var(--text-sm); color: var(--text-muted); max-width: 100%; }
.form-file::file-selector-button {
  margin-inline-end: var(--space-3);
  padding: 6px 12px; border-radius: var(--radius-sm);
  font: 600 var(--text-sm) var(--font); color: var(--text);
  border: 1px solid var(--raise-border);
  background: var(--raise-grad); box-shadow: var(--raise-shadow);
  cursor: pointer;
}
.form-file::file-selector-button:hover { background: var(--raise-grad-hover); border-color: var(--raise-border-hover); }

/* === Segmented control / view switcher ================================== */
/* An inset trough of choices, the chosen one raised in it. Use it for two
   to four options: a small scale, kg|lbs, Read|Edit. Its items are buttons,
   marked aria-pressed or aria-checked, or links to views, marked
   aria-current. */
.seg {
  display: inline-flex; padding: 3px; gap: 2px;
  background: var(--recess-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--entry-shadow);
}
.seg button, .seg a {
  display: inline-flex; align-items: center;
  padding: 4px 12px; border-radius: 4px;
  background: transparent; border: 1px solid transparent; cursor: pointer;
  font: 600 var(--text-sm) var(--font); color: var(--text-muted);
  text-decoration: none;
}
.seg button:hover, .seg a:hover { color: var(--text); }
.seg button:focus-visible, .seg a:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--focus-ring); }
.seg button[aria-pressed="true"], .seg button[aria-checked="true"],
.seg [aria-current]:not([aria-current="false"]),
.seg button.active {
  color: var(--text);
  background: var(--raise-grad);
  border-color: var(--raise-border);
  box-shadow: var(--raise-shadow);
}
.seg.pill { border-radius: var(--radius-pill); }
.seg.pill button, .seg.pill a { border-radius: var(--radius-pill); }
.seg.sm button, .seg.sm a { padding: 2px 9px; font-size: var(--text-xs); }

/* === Badges, chips and filter chips =====================================
   Three small labels, each looking like what it is:
     - a badge states an entity's state, and always leads with its glyph;
     - a chip names an attribute the thing carries, a tag or a category,
       and is a plain neutral label;
     - a filter chip is a filter in force, outlined, with a raised × that
       removes it. */
.badge {
  display: inline-flex; align-items: center; gap: var(--space-1);
  padding: 1px 8px; border-radius: var(--radius-pill);
  font-size: var(--text-2xs); font-weight: 700; line-height: 1.55;
  background: color-mix(in srgb, var(--text-muted) 18%, transparent);
  color: var(--text);
  --glyph-size: 9px;
}
.badge.warn { background: color-mix(in srgb, var(--warn) 22%, transparent); color: var(--warn); }
.badge.danger { background: color-mix(in srgb, var(--danger) 18%, transparent); color: var(--danger); }
.badge.positive, .badge.pr { background: color-mix(in srgb, var(--positive) 22%, transparent); color: var(--positive); }
.badge.accent { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); }
.badge::before { content: none; }
.badge.warn::before { content: ""; --glyph: var(--glyph-triangle); }
.badge.danger::before { content: ""; --glyph: var(--glyph-square); }
.badge.positive::before, .badge.pr::before { content: ""; --glyph: var(--glyph-circle); }
.badge.accent::before { content: ""; --glyph: var(--glyph-diamond); }

.chip {
  display: inline-flex; align-items: center; gap: var(--space-1);
  padding: 1px 8px; border-radius: var(--radius-sm);
  font-size: var(--text-xs); font-weight: 600;
  background: color-mix(in srgb, var(--text) 8%, transparent);
  color: var(--text);
}

/* === Cards ============================================================== */
.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: var(--space-4);
  box-shadow: var(--shadow-card);
}
.card-title { margin: 0 0 6px; font-size: var(--text-lg); font-weight: 700; }
.card-subtitle { margin: 0 0 6px; font-size: var(--text-sm); font-weight: 600; color: var(--text-muted); }
.card-desc { margin: 0 0 var(--space-3); font-size: var(--text-sm); color: var(--text-muted); }

/* === Key/value table for read-only record fields ======================== */
/* Detail panes use this for displayed-only fields, not fake input boxes
   (elevation = interactivity). */
.kv-table { width: 100%; border-collapse: collapse; font-size: var(--text-md); }
.kv-table th {
  text-align: start; font-weight: 600; color: var(--text-muted); font-size: var(--text-sm);
  padding-block: var(--space-2); padding-inline: 0 var(--space-4); white-space: nowrap; vertical-align: top; width: 1%;
}
.kv-table td { padding: var(--space-2) 0; color: var(--text); vertical-align: top; overflow-wrap: anywhere; }
.kv-table tr + tr th, .kv-table tr + tr td { border-top: 1px solid var(--border); }

/* === Dialogs ==============================================================
   A dialog is a native <dialog class="dialog">, rendered by the server and
   shown with showModal(), or with a button carrying command="show-modal"
   and commandfor naming it. The browser then keeps focus inside it, closes
   it on Escape and draws its backdrop. The backdrop dims gently, to 40%,
   because the panel's shadow does the elevation work, and the panel is
   always one step brighter than the page. pudl-dialog.js adds the command
   buttons in browsers that lack them.

   The older markup, a .dialog panel inside a .dialog-backdrop shown by a
   project's own script, still works. */
.dialog {
  width: 100%; max-width: 480px;
  background: var(--dialog-bg, var(--surface));
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow:
      var(--shadow-card),
      0 24px 64px rgba(0, 0, 0, 0.32);
  padding: var(--space-5);
}
dialog.dialog { margin: auto; max-height: calc(100vh - 2 * var(--space-5)); overflow: auto; }
dialog.dialog:not([open]) { display: none; }
dialog.dialog::backdrop { background: color-mix(in srgb, #000 40%, transparent); }
.dialog-backdrop {
  position: fixed; inset: 0; z-index: 100;
  display: flex; align-items: center; justify-content: center; padding: var(--space-5);
  background: color-mix(in srgb, #000 40%, transparent);
}
.dialog-title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-lg); font-weight: 700;
  color: var(--text);
}
.dialog-body {
  margin: 0 0 var(--space-5);
  color: var(--text-muted);
  font-size: var(--text-md); line-height: 1.55;
}
.dialog-actions {
  display: flex; justify-content: flex-end; gap: var(--space-2);
}

/* === Master-detail layout ================================================ */
/* body:has(> .md-layout) is full-viewport flex; the toolbar + chip row +
   body fill the page. Scoped to a DIRECT child so a reference demo or any
   other embedded .md-layout doesn't accidentally kill page scrolling. */
body:has(> .md-layout) {
  height: 100vh; display: flex; flex-direction: column; overflow: hidden;
}
.md-layout {
  display: flex; flex-direction: column; flex: 1; min-height: 0;
}
.md-toolbar {
  display: flex; align-items: center; gap: 10px;
  padding: var(--space-2) var(--space-4); background: var(--surface);
  border-bottom: 1px solid var(--border); flex: none;
  --ctl-h: 30px;
}
.md-toolbar .btn-sm,
.md-toolbar .seg,
.md-toolbar .md-filter {
  box-sizing: border-box; height: var(--ctl-h);
  display: inline-flex; align-items: center;
}
.md-toolbar .md-filter { padding: 4px 12px; }
.md-filter {
  box-sizing: border-box; flex: none; width: 220px;
  padding: 6px 11px; font-size: var(--text-sm);
  background: var(--input-bg); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  box-shadow: var(--entry-shadow);
}
.md-filter:focus {
  border-color: var(--accent); outline: none;
  box-shadow: var(--entry-shadow), 0 0 0 3px var(--focus-ring);
}

/* A filter that applies on submission pairs the input with a raised apply
   button, so the action that can be pressed looks pressable. The two are
   drawn joined, the sunken input meeting the raised button. */
.md-filter-group { display: inline-flex; align-items: stretch; margin: 0; }
.md-filter-group .md-filter {
  flex: 1; min-width: 0;
  border-start-end-radius: 0; border-end-end-radius: 0;
}
.md-filter-go {
  flex: none; width: 32px; height: auto;
  margin-inline-start: -1px;
  border-start-start-radius: 0; border-end-start-radius: 0;
  color: var(--text-muted);
}
.md-filter-go::before {
  content: ""; width: 14px; height: 14px;
  background: currentColor;
  -webkit-mask: var(--glyph-search) center / contain no-repeat;
  mask: var(--glyph-search) center / contain no-repeat;
}
.md-filter-go:focus-visible { outline: none; box-shadow: var(--raise-shadow), 0 0 0 3px var(--focus-ring); position: relative; }
.md-toolbar .md-filter-group { height: var(--ctl-h); }
.md-toolbar-spacer { flex: 1; }
.md-toolbar-cmds {
  flex: none; display: flex; align-items: center; gap: 6px;
}
.md-toolbar-sep {
  width: 1px; height: var(--ctl-h);
  background: var(--border); align-self: center; margin: 0 var(--space-1);
}

/* Active-filter chips, below the toolbar. */
.md-chips {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  padding: var(--space-2) var(--space-4); background: var(--surface);
  border-bottom: 1px solid var(--border); flex: none;
}
/* A chips row with no chips is hidden. It is tested by what it holds, not
   by :empty, because a server's template leaves whitespace in an empty row
   and :empty counts that as content. The row stays in the page, so it can
   still be a region that a swap replaces. */
.md-chips:not(:has(.filter-chip)) { display: none; }
.filter-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 2px 3px 2px 10px;
  border-radius: var(--radius-pill);
  font-size: var(--text-xs); font-weight: 600;
  background: color-mix(in srgb, var(--accent) 8%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 55%, transparent);
  color: var(--accent);
}
/* .fc-kind is the old name for .filter-chip-kind. */
.filter-chip-kind, .fc-kind {
  opacity: .7; font-weight: 500; font-size: var(--text-2xs);
  text-transform: uppercase; letter-spacing: .04em;
}
/* The × removes the filter, so it is a small raised button. */
.filter-chip-x {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--raise-grad); border: 1px solid var(--raise-border);
  box-shadow: var(--raise-shadow);
  color: var(--text-muted); text-decoration: none;
  font-size: var(--text-sm); line-height: 1;
}
.filter-chip-x:hover { color: var(--text); background: var(--raise-grad-hover); border-color: var(--raise-border-hover); }
.filter-chip-x:focus-visible { outline: none; box-shadow: var(--raise-shadow), 0 0 0 3px var(--focus-ring); }
.md-chips-clear {
  font-size: var(--text-xs); color: var(--accent); text-decoration: underline; cursor: pointer;
}

/* Body row: sidebar + resize + detail. */
.md-body { display: flex; flex: 1; min-height: 0; }
/* The sidebar's width is --md-sidebar-w, which pudl-md.js sets as the
   divider is dragged and a project may set from a stored preference. The
   stylesheet holds it between --md-sidebar-min and half the layout, so the
   limits apply however the width was set, and still apply when the layout
   narrows after a drag. */
.md-sidebar {
  width: clamp(var(--md-sidebar-min, 180px), var(--md-sidebar-w, 260px), 50%);
  flex: none; min-height: 0; overflow-y: auto;
  background: var(--surface); border-inline-end: 1px solid var(--border);
}
.md-resize {
  flex: none; width: 6px; margin: 0 -3px; z-index: 3; cursor: col-resize;
  background: transparent; position: relative;
  touch-action: none;
}
.md-resize::before {
  content: ""; position: absolute; inset: 0 2px; border-radius: 2px;
  background: transparent; transition: background .12s;
}
.md-resize:hover::before,
.md-resize.dragging::before { background: var(--accent); }
.md-resize:focus-visible { outline: none; }
.md-resize:focus-visible::before { background: var(--accent); box-shadow: 0 0 0 3px var(--focus-ring); }
.md-detail {
  flex: 1; overflow-y: auto; padding: var(--space-5); min-width: 0;
  background: var(--bg);
}

/* The back link returns from a record to its list. It shows only when the
   layout is narrow enough to show one pane at a time. */
.md-back { display: none; }

/* === One pane at a time =================================================
   When the layout itself is 640px wide or less (a phone, or a narrow window
   or panel on a larger screen), it shows either the list or the record,
   never both. The server says which with data-md-pane on .md-layout:
   "detail" when the URL names a record, and "list", or nothing, otherwise.
   Each pane is its own URL, so Back, reload and sharing all work, and the
   detail's .md-back link returns to the list. */
.md-layout { container: md / inline-size; }

@container md (max-width: 640px) {
  .md-toolbar { flex-wrap: wrap; padding: var(--space-2) var(--space-3); }
  .md-toolbar .md-filter { flex: 1 1 100%; width: auto; }
  .md-toolbar .md-filter-group { flex: 1 1 100%; }
  .md-toolbar-spacer { display: none; }
  .md-toolbar-cmds { flex-wrap: wrap; }
  .md-resize { display: none; }

  /* The list pane fills the layout, and its rows are touch targets. */
  .md-sidebar { flex: 1; width: auto; border-inline-end: 0; }
  .md-sidebar .md-item { padding-top: 10px; padding-bottom: 10px; }
  .md-detail { display: none; padding: var(--space-4); }

  /* The detail pane: the list, its toolbar and its filters step aside. A
     toolbar of site-wide tools, such as a launcher or a window dock, is
     marked .md-site-tools and stays in both panes. */
  .md-layout[data-md-pane="detail"] .md-toolbar:not(.md-site-tools),
  .md-layout[data-md-pane="detail"] .md-chips,
  .md-layout[data-md-pane="detail"] .md-sidebar { display: none; }
  .md-layout[data-md-pane="detail"] .md-detail { display: block; }

  .md-back {
    display: inline-flex; align-items: center; gap: var(--space-1);
    margin-bottom: var(--space-3);
    font-size: var(--text-md); font-weight: 600;
  }
}
.md-back::before { --glyph: var(--glyph-back); --glyph-size: 14px; }
.md-back:dir(rtl)::before { transform: scaleX(-1); }

/* Sidebar rows: a compact list, not big touch targets except on a narrow
   layout. The current row, marked aria-current on its link, carries a 3px
   accent edge on the side facing the detail pane and a bold title. */
.md-section-label {
  padding: 14px 14px var(--space-1);
  font-size: var(--text-2xs); font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-muted);
}
.md-row {
  display: flex; align-items: center;
  border-inline-end: 3px solid transparent;
  border-bottom: 1px solid var(--surface-alt);
}
.md-row:hover { background: var(--surface-alt); }
.md-row:has(> [aria-current]:not([aria-current="false"])),
.md-row.active {
  background: var(--surface-alt);
  border-inline-end-color: var(--accent);
}
.md-row:has(> [aria-current]:not([aria-current="false"])) .md-item,
.md-row.active .md-item { font-weight: 700; color: var(--text); }
.md-item {
  flex: 1; min-width: 0;
  padding: 5px 14px;
  font-size: var(--text-sm); line-height: 1.5;
  color: var(--text); text-decoration: none;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  cursor: pointer;
}
.md-item .md-meta {
  display: block; font-size: var(--text-2xs); font-weight: 500; color: var(--text-muted);
}
.md-grip {
  flex: none; width: 22px; text-align: center; cursor: grab;
  color: var(--text-muted); font-size: var(--text-md); line-height: 1;
}

/* === Menus ===============================================================
   A menu button opens a panel of choices: places to go, and actions. The
   button is raised and carries a caret, so it reads as opening something
   rather than doing something, and it shows pressed while its panel is
   open. The panel is lifted like a dialog, without a backdrop, because it
   is not modal. It is an HTML popover, so it opens, closes on Escape or an
   outside click, and sits above everything with no script; pudl-menu.js
   places it against its button and adds the arrow keys and filtering.

   Places in a panel are list rows, the .md-row and .md-item of the
   master-detail sidebar, grouped under .md-section-label headings. Actions
   are .menu-action buttons, set apart by an .menu-sep rule. */
.menu { display: inline-flex; position: relative; }
.menu-btn::after { --glyph: var(--glyph-caret); --glyph-size: 12px; opacity: .75; }
.menu:has(> .menu-panel:popover-open) > .menu-btn {
  background: var(--raise-active-bg);
  box-shadow: var(--raise-active-shadow);
}

.menu-panel {
  margin: auto; padding: 6px 0;
  min-width: 14rem; max-width: min(22rem, calc(100vw - 16px));
  max-height: min(32rem, calc(100vh - 16px));
  overflow: auto;
  background: var(--dialog-bg, var(--surface)); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow:
      var(--shadow-card),
      0 16px 40px color-mix(in srgb, var(--shade) calc(var(--depth) * 1.6), transparent);
  font-family: var(--font);
}
/* The script hides a panel for the moment between opening and placing it,
   so it never flashes in the middle of the screen. */
.menu-panel.placing { visibility: hidden; }
/* On a narrow screen the script opens the panel as a sheet across the full
   width, so its rows stay large enough to tap. */
.menu-panel.sheet {
  width: 100%; max-width: none;
  border-width: 0 0 1px; border-radius: 0 0 var(--radius) var(--radius);
}
.menu-panel.sheet .md-item { padding-top: 10px; padding-bottom: 10px; }

.menu-panel .md-section-label { padding: 10px 14px var(--space-1); }
.menu-panel .md-section-label:first-child { padding-top: 6px; }
.menu-panel .md-row { border-bottom: 0; }
.menu-panel .md-item:focus-visible,
.menu-action:focus-visible {
  outline: none; background: var(--surface-alt);
  box-shadow: inset 3px 0 var(--accent);
}
.menu-panel .md-item:dir(rtl):focus-visible,
.menu-action:dir(rtl):focus-visible { box-shadow: inset -3px 0 var(--accent); }
.menu-panel .md-filter { display: block; width: calc(100% - 20px); margin: var(--space-1) 10px 6px; }
.menu-panel form { margin: 0; }
.menu-empty { margin: 0; padding: 6px 14px; font-size: var(--text-sm); color: var(--text-muted); }

.menu-sep { border: 0; border-top: 1px solid var(--border); margin: 6px 0; }
.menu-action {
  display: flex; align-items: center; gap: var(--space-2); width: 100%;
  padding: 6px 14px;
  background: none; border: 0; border-radius: 0;
  font: var(--text-sm)/1.5 var(--font); color: var(--text); text-align: start;
  cursor: pointer;
}
.menu-action:hover { background: var(--surface-alt); }
.menu-action.danger { color: var(--danger); }

/* === Data tables =========================================================
   A table of many records, one per row. It sits in a .data-table-wrap,
   which scrolls sideways when the columns outrun the space and keeps the
   header in view as the rows scroll under it.

   A column the reader can sort by has a header whose link, or button, is
   raised, because pressing it re-sorts, and it carries a glyph: two small
   arrows while the column is not the sort, one arrow for the direction it
   is. The header's aria-sort says which, and the link is an ordinary
   address such as ?sort=amount&dir=desc, so sorting works without script
   and a sorted list can be shared. Columns of numbers carry .num and align
   to the end. A selected row is marked aria-selected="true", or holds a
   checked checkbox, and carries an accent edge at its start as well as a
   tint. A .data-table-empty row says there is nothing to show.

   With .stack on the table, a wrap 560px wide or less shows each row as a
   small card of label and value pairs, the labels coming from each cell's
   data-label. */
.data-table-wrap {
  overflow: auto; max-width: 100%;
  background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius);
  container: dt / inline-size;
}
.data-table {
  width: 100%; border-collapse: separate; border-spacing: 0;
  font-size: var(--text-md); font-variant-numeric: tabular-nums;
}
.data-table caption {
  caption-side: top; text-align: start;
  padding: var(--space-3) var(--space-4) var(--space-2);
  font-size: var(--text-sm); font-weight: 600; color: var(--text-muted);
}
.data-table th, .data-table td {
  padding: var(--space-2) var(--space-3);
  text-align: start; vertical-align: top;
  border-bottom: 1px solid var(--border);
}
.data-table thead th {
  position: sticky; top: 0; z-index: 1;
  background: var(--surface-alt);
  font-size: var(--text-sm); font-weight: 600; color: var(--text-muted);
  white-space: nowrap;
}
.data-table tbody tr:last-child > * { border-bottom: 0; }
.data-table tbody tr:hover { background: color-mix(in srgb, var(--text) 4%, transparent); }
.data-table .num { text-align: end; }
.data-table .data-table-check { width: 1%; }
.data-table .data-table-check input { width: 16px; height: 16px; margin: 0; accent-color: var(--accent); }
.data-table tr[aria-selected="true"],
.data-table tr:has(> .data-table-check input:checked) {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  box-shadow: inset 3px 0 var(--accent);
}
.data-table tr[aria-selected="true"]:dir(rtl),
.data-table tr:has(> .data-table-check input:checked):dir(rtl) { box-shadow: inset -3px 0 var(--accent); }

/* A sortable column's header is a raised control filling its cell. */
.data-table th[aria-sort] { padding: 0; }
.data-table th[aria-sort] > :is(a, button) {
  display: flex; align-items: center; gap: 6px; width: 100%;
  padding: var(--space-2) var(--space-3);
  background: var(--raise-grad);
  border: 0; border-inline-end: 1px solid var(--raise-border);
  box-shadow: inset 0 1px color-mix(in srgb, var(--light) var(--lit), transparent);
  font: inherit; color: var(--text); text-decoration: none; cursor: pointer;
  --glyph: var(--glyph-sort); --glyph-size: 12px;
}
.data-table th[aria-sort].num > :is(a, button) { justify-content: flex-end; }
.data-table th[aria-sort] > :is(a, button):hover { background: var(--raise-grad-hover); }
.data-table th[aria-sort] > :is(a, button):active { background: var(--raise-active-bg); }
.data-table th[aria-sort] > :is(a, button):focus-visible { outline: none; box-shadow: inset 0 0 0 3px var(--focus-ring); }
.data-table th[aria-sort] > :is(a, button)::after { opacity: .55; }
.data-table th[aria-sort="ascending"] > :is(a, button)::after { --glyph: var(--glyph-sort-up); opacity: 1; }
.data-table th[aria-sort="descending"] > :is(a, button)::after { --glyph: var(--glyph-sort-down); opacity: 1; }
.data-table th[aria-sort="ascending"] > :is(a, button),
.data-table th[aria-sort="descending"] > :is(a, button) { font-weight: 700; }

.data-table .data-table-empty td {
  padding: var(--space-6) var(--space-4);
  text-align: center; color: var(--text-muted);
}

@container dt (max-width: 560px) {
  .data-table.stack thead {
    position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%);
  }
  .data-table.stack, .data-table.stack tbody, .data-table.stack tr, .data-table.stack td { display: block; }
  .data-table.stack tr { padding: var(--space-2) 0; border-bottom: 1px solid var(--border); }
  .data-table.stack tbody tr:last-child { border-bottom: 0; }
  .data-table.stack td {
    display: flex; justify-content: space-between; gap: var(--space-4);
    padding: var(--space-1) var(--space-4); border-bottom: 0; text-align: end;
  }
  .data-table.stack td[data-label]::before {
    content: attr(data-label);
    font-size: var(--text-sm); font-weight: 600; color: var(--text-muted); text-align: start;
  }
}

/* === Notices and toasts ==================================================
   A notice is a message that stays on the page until it is dealt with: an
   error on submission, a warning about the record in view, a note of what
   has changed. A toast is a passing confirmation, "Saved", that leaves by
   itself. Both lead with the glyph of their kind (information, positive,
   warning, danger) and a coloured edge, so neither rests on colour alone.
   An error the reader must act on is a notice, never a toast, because a
   toast goes away. */
.notice {
  display: flex; align-items: flex-start; gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px solid color-mix(in srgb, var(--notice-color) 45%, var(--border));
  border-inline-start: 4px solid var(--notice-color);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--notice-color) 9%, var(--surface));
  color: var(--text);
  font-size: var(--text-md);
  --notice-color: var(--accent);
  --glyph: var(--glyph-info); --glyph-size: 18px;
}
.notice::before { margin-top: 1px; color: var(--notice-color); background: var(--notice-color); }
.notice.positive { --notice-color: var(--positive); --glyph: var(--glyph-check); }
.notice.warn { --notice-color: var(--warn); --glyph: var(--glyph-warning); }
.notice.danger { --notice-color: var(--danger); --glyph: var(--glyph-stop); }
.notice-content { flex: 1; min-width: 0; }
.notice-title { margin: 0 0 2px; font-size: var(--text-md); font-weight: 700; }
.notice-body { margin: 0; }
.notice-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-2); }

/* The small raised button that dismisses a notice or a toast. */
.notice-close, .toast-close {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; padding: 0; margin-inline-start: auto;
  border-radius: 50%;
  background: var(--raise-grad); border: 1px solid var(--raise-border);
  box-shadow: var(--raise-shadow);
  color: var(--text-muted); cursor: pointer;
  --glyph: var(--glyph-close); --glyph-size: 12px;
}
.notice-close:hover, .toast-close:hover { color: var(--text); background: var(--raise-grad-hover); }
.notice-close:focus-visible, .toast-close:focus-visible { outline: none; box-shadow: var(--raise-shadow), 0 0 0 3px var(--focus-ring); }

/* Toasts gather in a live region at the foot of the window, which
   pudl-toast.js creates if the page has not rendered one. */
.toast-region {
  position: fixed; z-index: 1000;
  inset-block-end: var(--space-4); inset-inline-end: var(--space-4);
  display: flex; flex-direction: column; align-items: flex-end; gap: var(--space-2);
  width: min(24rem, calc(100vw - 2 * var(--space-4)));
  pointer-events: none;
}
.toast {
  pointer-events: auto;
  display: flex; align-items: flex-start; gap: var(--space-3);
  width: 100%;
  padding: var(--space-3) var(--space-3) var(--space-3) var(--space-4);
  background: var(--dialog-bg, var(--surface)); color: var(--text);
  border: 1px solid var(--border);
  border-inline-start: 4px solid var(--notice-color);
  border-radius: var(--radius-sm);
  box-shadow:
      var(--shadow-card),
      0 10px 28px color-mix(in srgb, var(--shade) calc(var(--depth) * 1.4), transparent);
  font-size: var(--text-md);
  transition: opacity .2s, transform .2s;
  --notice-color: var(--accent);
  --glyph: var(--glyph-info); --glyph-size: 18px;
}
.toast::before { margin-top: 1px; background: var(--notice-color); }
.toast.positive { --notice-color: var(--positive); --glyph: var(--glyph-check); }
.toast.warn { --notice-color: var(--warn); --glyph: var(--glyph-warning); }
.toast.danger { --notice-color: var(--danger); --glyph: var(--glyph-stop); }
.toast-text { flex: 1; min-width: 0; margin: 2px 0 0; }
.toast.leaving { opacity: 0; transform: translateY(8px); }

/* === Empty and loading states ============================================
   An empty state says there is nothing here yet, and what to do about it:
   a drawn glyph, a title, a line of text and perhaps an action. A loading
   state is a spinner with words beside it, in a role="status" element, so
   it is announced and never rests on the animation alone. A region being
   refreshed by pudl-regions.js dims as well. */
.empty-state {
  display: grid; justify-items: center; gap: var(--space-2);
  padding: var(--space-6) var(--space-4);
  text-align: center; color: var(--text-muted);
  --glyph: var(--glyph-empty); --glyph-size: 36px;
}
.empty-state::before { opacity: .7; margin-bottom: var(--space-1); }
.empty-state-title { margin: 0; font-size: var(--text-lg); font-weight: 700; color: var(--text); }
.empty-state-body { margin: 0; max-width: 42ch; font-size: var(--text-md); }
.empty-state-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-2); margin-top: var(--space-2); }

.spinner {
  display: inline-block; flex: none; vertical-align: middle;
  width: 1.2em; height: 1.2em; border-radius: 50%;
  border: 2px solid color-mix(in srgb, currentColor 22%, transparent);
  border-top-color: currentColor;
  animation: pudl-spin .8s linear infinite;
}
@keyframes pudl-spin { to { transform: rotate(360deg); } }
.loading {
  display: flex; align-items: center; justify-content: center; gap: var(--space-2);
  padding: var(--space-5) var(--space-4);
  font-size: var(--text-sm); color: var(--text-muted);
}

/* === Tooltips ============================================================
   A tooltip names a control that shows only a glyph, such as an icon
   button, a window button or a filter's apply button. pudl-tooltip.js
   shows one for any element with data-tooltip, or with an aria-label on a
   glyph-only control, after a moment of hover or at once on keyboard
   focus. It stays while the pointer moves onto it, and Escape dismisses
   it, as WCAG 1.4.13 asks. */
.tooltip {
  margin: 0; padding: 4px 8px;
  max-width: 18rem;
  border: 0; border-radius: 5px;
  background: var(--text); color: var(--bg);
  font: 600 var(--text-xs)/1.4 var(--font);
  box-shadow: 0 4px 12px color-mix(in srgb, var(--shade) calc(var(--depth) * 1.4), transparent);
  pointer-events: auto;
}
.tooltip.placing { visibility: hidden; }

/* === Pagination ==========================================================
   Pages of a long list, each an address such as ?page=3, so paging works
   without script and a page can be shared. The page links are raised
   small buttons; the current page, marked aria-current="page", is pressed
   in, because the reader is already there. Previous and next carry rel
   and a glyph, and at either end are marked aria-disabled with no href. */
.pagination { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1); }
.pagination-summary { margin-inline-end: var(--space-3); font-size: var(--text-sm); color: var(--text-muted); }
.page-link {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-1);
  min-width: 32px; height: 32px; padding: 0 10px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--raise-border);
  background: var(--raise-grad); box-shadow: var(--raise-shadow);
  font: 600 var(--text-sm) var(--font); color: var(--text);
  text-decoration: none; font-variant-numeric: tabular-nums;
  --glyph: var(--glyph-back); --glyph-size: 12px;
}
.page-link:hover { background: var(--raise-grad-hover); border-color: var(--raise-border-hover); }
.page-link:focus-visible { outline: none; box-shadow: var(--raise-shadow), 0 0 0 3px var(--focus-ring); }
.page-link[aria-current]:not([aria-current="false"]) {
  background: var(--raise-active-bg); box-shadow: var(--raise-active-shadow);
  border-color: var(--raise-border-hover);
}
.page-link[aria-disabled="true"] { opacity: .45; pointer-events: none; }
.page-link[rel="next"]::after { transform: scaleX(-1); }
.page-link[rel="prev"]:dir(rtl)::before { transform: scaleX(-1); }
.page-link[rel="next"]:dir(rtl)::after { transform: none; }
.page-gap { padding: 0 var(--space-1); color: var(--text-muted); }

/* === Reduced motion ======================================================
   A reader who has asked their system for less motion gets none: every
   transition and animation completes at once. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    transition-delay: 0s !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
}

/* === Forced colours ======================================================
   In Windows' high-contrast mode the browser replaces the page's colours
   with the reader's and removes every box-shadow. PUDL draws its focus
   rings, and much of its raised and sunken treatment, with shadows, so
   here focus becomes a real outline, controls keep visible borders, and
   selected and pressed states take the system's highlight colours, since
   an elevation change alone would not show. */
@media (forced-colors: active) {
  :focus-visible { outline: 2px solid Highlight !important; outline-offset: 2px !important; }
  .btn, .icon-btn, .seg, .form-input, .form-select, .form-textarea, .md-filter,
  .switch-track, .switch-thumb, .menu-panel, .dialog, .card, .section-tab,
  .filter-chip, .filter-chip-x { border: 1px solid CanvasText; }
  .seg button[aria-pressed="true"], .seg button[aria-checked="true"],
  .seg [aria-current]:not([aria-current="false"]), .seg button.active,
  .section-tab[aria-current]:not([aria-current="false"]), .section-tab.active,
  .md-row:has(> [aria-current]:not([aria-current="false"])), .md-row.active,
  .btn[aria-pressed="true"], .menu:has(> .menu-panel:popover-open) > .menu-btn {
    forced-color-adjust: none;
    background: Highlight; color: HighlightText; border-color: Highlight;
  }
  .md-row:has(> [aria-current]:not([aria-current="false"])) .md-item,
  .md-row:has(> [aria-current]:not([aria-current="false"])) .md-meta,
  .md-row.active .md-item, .md-row.active .md-meta { color: HighlightText; }
  .md-row:not(.active):not(:has(> [aria-current]:not([aria-current="false"]))) { border-inline-end-color: Canvas; }
  .switch[aria-checked="true"] .switch-track { forced-color-adjust: none; background: Highlight; border-color: Highlight; }
  .switch-thumb { forced-color-adjust: none; background: Canvas; }
  /* Glyphs are masks, exempt from the system's recolouring so that they
     still paint, so they are painted in the system's own colours here. */
  .badge::before, .form-error::before, .menu-btn::after, .md-back::before,
  .md-filter-go::before { forced-color-adjust: none; background: CanvasText; }
  .md-filter-go::before, .menu-btn::after { background: ButtonText; }
  .notice::before, .toast::before { forced-color-adjust: none; background: CanvasText; }
  .notice-close::before, .toast-close::before, .data-table th[aria-sort] > :is(a, button)::after {
    forced-color-adjust: none; background: ButtonText;
  }
  .notice, .toast, .notice-close, .toast-close, .data-table-wrap { border: 1px solid CanvasText; }
  .tablist > [role="tab"], .page-link, .tabs > [role="tabpanel"], .tooltip { border: 1px solid CanvasText; }
  .tablist > [role="tab"][aria-selected="true"], .page-link[aria-current]:not([aria-current="false"]) {
    forced-color-adjust: none; background: Highlight; color: HighlightText; border-color: Highlight;
  }
  .empty-state::before { forced-color-adjust: none; background: CanvasText; }
  .page-link[rel="prev"]::before, .page-link[rel="next"]::after { forced-color-adjust: none; background: ButtonText; }
  .spinner { border-color: GrayText; border-top-color: CanvasText; }
  .notice, .toast { border-inline-start-width: 4px; }
  .data-table th[aria-sort] > :is(a, button) { border: 1px solid ButtonText; }
  .data-table tr[aria-selected="true"], .data-table tr:has(> .data-table-check input:checked) {
    forced-color-adjust: none; background: Highlight; color: HighlightText;
  }
  .menu:has(> .menu-panel:popover-open) > .menu-btn::after { background: HighlightText; }
  .md-resize::before { background: CanvasText; inset: 0 2.5px; }
  .md-resize:hover::before, .md-resize.dragging::before, .md-resize:focus-visible::before { background: Highlight; }
  .badge { border: 1px solid CanvasText; }
}

/* === Print ===============================================================
   A printed page carries the content, on white, without the chrome: the
   topbar, the tab bar, the toolbars, the filter chips and the divider.
   A master-detail layout showing a record prints the record alone. */
@media print {
  :root, [data-theme="dark"] {
    color-scheme: light;
    --bg: #ffffff; --surface: #ffffff; --surface-alt: #f2f2f2;
    --text: #000000; --text-muted: #444444; --border: #999999;
    --accent: #1a3d6b; --accent-hover: #1a3d6b;
  }
  .topbar, .app-section-bar, .md-toolbar, .md-chips, .md-resize, .md-back,
  .menu, .menu-panel, .dialog-backdrop, .theme-toggle,
  .toast-region, .notice-close, .tablist, .pagination, .tooltip { display: none !important; }
  .tabs > [role="tabpanel"] { display: block !important; border: 0; padding: 0 0 var(--space-4); }
  .data-table-wrap { overflow: visible; border: 0; }
  .data-table thead th { position: static; }
  .data-table th[aria-sort] > :is(a, button) { background: none; box-shadow: none; border: 0; }
  body:has(> .md-layout) { height: auto; overflow: visible; display: block; }
  .md-layout, .md-body { display: block; height: auto; }
  .md-sidebar, .md-detail { width: auto; overflow: visible; border: 0; }
  .md-layout[data-md-pane="detail"] .md-sidebar { display: none; }
  .btn, .icon-btn, .card, .dialog { box-shadow: none; }
  a { color: #000000; }
}
