/*
 * Shared components for Storyarr and Starboard (version 1, 2 November 2026).
 *
 * Uses only tokens.css. Everything is in the "shared" layer, so an app's own styles win without
 * !important. Class names are plain and match Starboard's; Storyarr maps its older classes to these
 * as each page is rebuilt (README.md has the map).
 *
 * Rules that hold everywhere: square corners; 44px controls; one typeface; a visible 3px focus ring;
 * status is never colour alone; nothing moves when reduced motion is on.
 */
@layer shared {
  /* 1. Base ------------------------------------------------------------------------------------ */
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }
  html {
    -webkit-text-size-adjust: 100%;
  }
  /* The hidden attribute always wins over a component's display. */
  [hidden] {
    display: none !important;
  }
  /* Headings may have a font of their own from a style (Starboard's styles, 11 November 2026); otherwise the body's. */
  :is(h1, h2, h3, h4) {
    font-family: var(--font-heading, var(--font));
  }
  body {
    margin: 0;
    font-family: var(--font);
    font-size: var(--text-md);
    line-height: var(--leading);
    letter-spacing: var(--letter-spacing);
    word-spacing: var(--word-spacing);
    background: var(--color-bg);
    color: var(--color-text);
  }
  h1,
  h2,
  h3,
  h4 {
    margin: 0;
    line-height: var(--leading-tight);
    font-weight: var(--weight-strong);
    text-wrap: balance;
  }
  h1 {
    font-size: var(--text-xl);
  }
  h2 {
    font-size: var(--text-lg);
  }
  h3 {
    font-size: var(--text-md);
  }
  p {
    margin: 0;
  }
  a {
    color: var(--color-accent);
    text-underline-offset: 0.15em;
  }
  :root.pref-underline a {
    text-decoration: underline;
  }
  button,
  input,
  select,
  textarea {
    font: inherit;
    color: inherit;
    letter-spacing: inherit;
    word-spacing: inherit;
  }
  button {
    cursor: pointer;
  }
  svg {
    flex: none;
  }
  /* Scroll bars: thin and square, in the border colour on a clear track, in every scrolling area (the
     page, long lists, slide-outs). They stay visible; only shelves hide theirs, behind arrows. */
  :root {
    scrollbar-color: var(--color-scrollbar) transparent;
  }
  :where(*) {
    scrollbar-width: thin;
  }
  @supports not (scrollbar-color: auto) {
    ::-webkit-scrollbar {
      width: 10px;
      height: 10px;
    }
    ::-webkit-scrollbar-track {
      background: transparent;
    }
    ::-webkit-scrollbar-thumb {
      border: 2px solid transparent;
      border-radius: 0;
      background: var(--color-scrollbar) padding-box;
    }
    ::-webkit-scrollbar-thumb:hover {
      background: var(--color-scrollbar-hover) padding-box;
    }
  }
  :focus-visible {
    outline: var(--focus-w) solid var(--color-focus);
    outline-offset: 2px;
  }
  /* Focus not obscured (WCAG 2.2, 2.4.11): the sticky header and save bar never cover what has focus or
     what a link scrolls to, because the page keeps their height clear when it scrolls. */
  :root:has(.site-header) {
    scroll-padding-top: calc(var(--header-h) + var(--space-4));
  }
  :root:has(.site-header__tabs) {
    scroll-padding-top: calc(var(--header-h) + var(--tabs-h) + var(--space-4));
  }
  :root:has(.save-bar:not([hidden])) {
    scroll-padding-bottom: calc(var(--control-h) + var(--space-8));
  }
  .visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .skip-link {
    position: absolute;
    left: var(--space-4);
    top: -100px;
    z-index: var(--layer-toast);
    padding: var(--space-3) var(--space-4);
    background: var(--color-accent);
    color: var(--color-accent-text);
    font-weight: var(--weight-strong);
  }
  .skip-link:focus {
    top: var(--space-2);
  }
  code,
  kbd {
    font-family: var(--font);
    background: var(--color-surface-sunken);
    padding: 0 var(--space-1);
    overflow-wrap: anywhere;
  }
  .code-block {
    display: block;
    padding: var(--space-3);
    white-space: pre-wrap;
    border: 1px solid var(--color-divider);
  }
  .muted {
    color: var(--color-text-muted);
  }
  .small {
    font-size: var(--text-sm);
  }
  .numeric {
    font-variant-numeric: tabular-nums;
  }

  /* 2. Shell: header, menus, rail, phone menu, page ---------------------------------------------- */

  /* Header row 1: the 1st-level tabs, then search, then Display at the far right. */
  .site-header {
    position: sticky;
    top: 0;
    z-index: var(--layer-header);
    background: var(--color-header);
    border-bottom: 1px solid var(--color-divider);
  }
  .site-header__bar {
    display: flex;
    align-items: stretch;
    gap: var(--space-4);
    height: var(--header-h);
    padding-right: var(--space-4);
  }
  .site-header__bar > :not(.menu-tabs) {
    align-self: center;
  }
  .site-header__brand {
    display: none;
    align-items: center;
    gap: var(--space-2);
    font-weight: var(--weight-heading);
    white-space: nowrap;
    color: var(--color-text);
    text-decoration: none;
  }
  .site-header__search {
    flex: 1 1 200px;
    max-width: 360px;
    margin-left: auto;
  }
  .site-header__actions {
    display: flex;
    gap: var(--space-2);
    flex: none;
  }
  /* Header row 2: the page's 2nd-level tabs. Only there when the page has tabs. */
  .site-header__tabs {
    height: var(--tabs-h);
    border-top: 1px solid var(--color-divider);
  }

  /* Menu tabs: 1st level (row 1) and 2nd level (row 2) share one look. Icons on both. */
  .menu-tabs {
    display: flex;
    align-items: stretch;
    min-width: 0;
    height: 100%;
    margin: 0;
    padding: 0;
    list-style: none;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .menu-tabs::-webkit-scrollbar {
    display: none;
  }
  .menu-tabs a {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: 0 var(--space-4);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    font-weight: var(--weight-strong);
    white-space: nowrap;
    text-decoration: none;
  }
  .menu-tabs a:hover,
  .menu-tabs a[aria-current="page"] {
    background: var(--color-control-active);
    color: var(--color-text);
  }
  /* A bar under the current page, so it does not rely on colour alone. */
  .menu-tabs a[aria-current="page"]::after {
    content: "";
    position: absolute;
    inset: auto 0 0 0;
    height: var(--current-bar-w);
    background: var(--color-accent);
  }
  .menu-tabs a:focus-visible {
    outline-offset: calc(-1 * var(--focus-w));
  }
  /* When the names do not fit, icons only; the names stay as tooltips and for screen readers. */
  .menu-tabs.is-compact a {
    padding: 0 var(--space-3);
  }
  .menu-tabs.is-compact .menu-tabs__label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
  }

  /* Main menu: the icon rail on the left, on computers. */
  .rail {
    position: fixed;
    inset: 0 auto 0 0;
    z-index: var(--layer-header);
    width: var(--rail-w);
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-1);
    padding: var(--space-3) var(--space-2);
    background: var(--color-rail, var(--color-header)); /* a style may give the side menu its own colour */
    border-right: 1px solid var(--color-divider);
  }
  .rail__logo {
    display: grid;
    place-items: center;
    height: 48px;
    margin-bottom: var(--space-3);
  }
  /* The app's logo with its name underneath, at the top of the side menu. It links to the home page. */
  .rail__brand {
    display: grid;
    justify-items: center;
    gap: 2px;
    padding: var(--space-2) 0 var(--space-3);
    color: var(--color-text);
    text-decoration: none;
  }
  .rail .rail__brand {
    min-height: 0;
    font-size: var(--text-xs);
    font-weight: var(--weight-heading);
  }
  .rail .rail__brand:hover {
    background: none;
  }
  .rail__name {
    max-width: 100%;
    overflow-wrap: anywhere;
    text-align: center;
  }
  .rail a,
  .rail__search,
  .rail__feedback {
    position: relative;
    display: grid;
    justify-items: center;
    gap: var(--space-1);
    padding: var(--space-2) 0;
    min-height: 56px;
    color: var(--color-text-muted);
    font-size: var(--text-xs);
    font-weight: var(--weight-strong);
    text-decoration: none;
  }
  /* Search is always one button away: first in the rail, under the logo. */
  .rail__search {
    width: 100%;
    margin-bottom: var(--space-2);
    border: 0;
    border-bottom: 1px solid var(--color-divider);
    background: none;
    font-family: inherit;
    cursor: pointer;
  }
  .rail a:hover,
  .rail__search:hover,
  .rail__feedback:hover,
  .rail a[aria-current="page"] {
    background: var(--color-control-active);
    color: var(--color-text);
  }
  .rail a[aria-current="page"]::before {
    content: "";
    position: absolute;
    inset: 0 auto 0 calc(-1 * var(--space-2));
    width: 3px;
    background: var(--color-accent);
  }
  /* Send feedback at the foot of the side menu while Send feedback is activated (Settings, General): a menu item
     under a line, opening Snaglist. Pages with no side menu, such as the set-up, keep Snaglist's corner button. */
  .rail__feedback {
    width: 100%;
    border: 0;
    border-top: 1px solid var(--color-divider);
    background: none;
    font-family: inherit;
    cursor: pointer;
  }
  .rail__feedback[hidden] {
    display: none;
  }
  .rail__end {
    margin-top: auto;
    display: grid;
    gap: var(--space-2);
    justify-items: center;
  }
  /* "Update" above the version when a newer release is out: a dot and the word, a 44px target, to About and updates. */
  .rail__update {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    min-height: 44px;
    padding-inline: var(--space-2);
    color: var(--color-text);
    font-size: var(--text-xs);
    font-weight: var(--weight-strong);
    text-decoration: none;
  }
  .rail__update[hidden] {
    display: none;
  }
  /* The app's version at the bottom of the side menu: plain text, not a link (Release notes is in the footer). */
  .rail__version {
    padding-bottom: var(--space-2);
    color: var(--color-text-muted);
    font-size: var(--text-xs);
    text-align: center;
  }
  .with-rail {
    padding-left: var(--rail-w);
  }
  /* The server's own name, up to 10 characters, at the start of the level 1 menu, as plain text with no
     icon (TheMightyTRexUK, 4 November 2026). A label, not a link: it names the server the page is about. */
  .site-header__server {
    display: inline-flex;
    flex: none;
    align-items: center;
    gap: var(--space-2);
    align-self: stretch;
    padding: 0 var(--space-4) 0 var(--space-5);
    border-right: 1px solid var(--color-divider);
    color: var(--color-text);
    /* Bold, and a size up from the tabs (TheMightyTRexUK, 4 November 2026: "Make the text bold"). */
    font-size: var(--text-md);
    font-weight: var(--weight-heading);
    white-space: nowrap;
  }
  /* The header's search button: shown where the search box is not (phones). It opens the same box as a
     full-width row under the header bar. */
  .site-header .site-header__search-btn {
    display: none;
  }
  .site-header.is-search-open .site-header__search {
    display: block;
    position: absolute;
    inset: 100% 0 auto 0;
    max-width: none;
    margin: 0;
    padding: var(--space-3) var(--space-4);
    background: var(--color-header);
    border-bottom: 1px solid var(--color-divider);
    box-shadow: var(--shadow-raised);
  }

  /* Update dot: a small red dot on the version or the menu button. Its name says it in words. */
  .update-dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: var(--radius-round);
    background: var(--status-danger-fg);
    vertical-align: super;
  }

  /* Phone menu: a drawer from the left with search, the main menu and the 1st level. */
  .site-header .phone-menu-button {
    display: none;
  }
  .phone-menu {
    position: fixed;
    inset: 0 auto 0 0;
    z-index: var(--layer-panel);
    width: min(340px, 86vw);
    display: flex;
    flex-direction: column;
    background: var(--color-surface-raised);
    border-right: 1px solid var(--color-divider);
    box-shadow: var(--shadow-raised);
    animation: slide-in-left var(--motion-panel);
  }
  .phone-menu__search {
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--color-divider);
  }
  .phone-menu__sections {
    flex: 1;
    overflow-y: auto;
  }
  .phone-menu__section {
    border-bottom: 1px solid var(--color-divider);
  }
  /* Send feedback, its own row above the menu's foot while Send feedback is activated, like the section rows. */
  .phone-menu__feedback {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    width: 100%;
    min-height: 52px;
    margin-top: auto;
    padding: 0 var(--space-4);
    border: 0;
    border-top: 1px solid var(--color-divider);
    background: transparent;
    color: var(--color-text);
    font: inherit;
    font-size: var(--text-md);
    font-weight: var(--weight-strong);
    text-align: left;
    cursor: pointer;
  }
  .phone-menu__feedback[hidden] {
    display: none;
  }
  .phone-menu__section > button {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    width: 100%;
    min-height: 52px;
    padding: 0 var(--space-4);
    border: 0;
    background: transparent;
    font-size: var(--text-md);
    font-weight: var(--weight-strong);
    text-align: left;
  }
  .phone-menu__section[data-current] > button {
    color: var(--color-accent);
  }
  .phone-menu__chevron {
    margin-left: auto;
    color: var(--color-text-muted);
    transition: transform var(--motion-panel);
  }
  .phone-menu__section > button[aria-expanded="true"] .phone-menu__chevron {
    transform: rotate(90deg);
  }
  .phone-menu__section ul {
    margin: 0;
    padding: 0 0 var(--space-2);
    list-style: none;
  }
  .phone-menu__section ul a {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-height: var(--control-h);
    padding: 0 var(--space-4) 0 48px;
    color: var(--color-text);
    text-decoration: none;
  }
  .phone-menu__section ul a[aria-current="page"] {
    background: var(--color-control-active);
    box-shadow: inset 3px 0 0 var(--color-accent);
    color: var(--color-accent);
    font-weight: var(--weight-strong);
  }
  .phone-menu__foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border-top: 1px solid var(--color-divider);
  }

  /* Page: content width, the page head (h1, summary, page actions) and the footer. */
  .page {
    max-width: var(--content-max);
    margin: 0 auto;
    padding: var(--space-8) var(--space-8) var(--space-12);
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-6);
  }
  .page-head {
    display: flex;
    flex-wrap: wrap;
    align-items: end;
    justify-content: space-between;
    gap: var(--space-3) var(--space-6);
  }
  .page-head__text {
    display: grid;
    gap: var(--space-1);
  }
  .page-head__summary {
    color: var(--color-text-muted);
  }
  .page-head .actions {
    margin-left: auto;
  }

  /* 3. Actions -------------------------------------------------------------------------------- */
  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-height: var(--control-h);
    padding: 0 var(--space-4);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    background: var(--color-surface);
    color: var(--color-text);
    font-size: var(--text-sm);
    font-weight: var(--weight-strong);
    white-space: nowrap;
    text-decoration: none;
  }
  .btn:hover {
    background: var(--color-surface-sunken);
  }
  .btn--primary {
    background: var(--color-accent);
    border-color: var(--color-accent);
    color: var(--color-accent-text);
  }
  .btn--primary:hover {
    background: var(--color-accent);
    filter: brightness(1.1);
  }
  .btn--quiet {
    border-color: transparent;
    background: transparent;
    color: var(--color-accent);
  }
  .btn--quiet:hover {
    background: var(--color-control-active);
  }
  .btn--danger {
    border-color: var(--status-danger-fg);
    color: var(--status-danger-fg);
  }
  .btn--danger.btn--primary {
    background: var(--status-danger-fg);
    color: var(--color-surface-raised);
  }
  .btn--compact {
    min-height: var(--control-h-compact);
    padding: 0 var(--space-3);
  }
  .btn[disabled],
  .btn[aria-disabled="true"] {
    opacity: 0.6;
    cursor: not-allowed;
    filter: none;
  }
  .btn svg {
    width: var(--icon-md);
    height: var(--icon-md);
  }
  /* Icon button: square, 44px. Always has an accessible name. */
  .icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-width: var(--control-h);
    min-height: var(--control-h);
    padding: 0 var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    background: transparent;
    color: var(--color-text);
    font-size: var(--text-sm);
    font-weight: var(--weight-strong);
  }
  .icon-btn:hover {
    background: var(--color-control-active);
  }
  .icon-btn--bare {
    border-color: transparent;
    color: var(--color-text-muted);
  }
  .icon-btn--bare:hover {
    color: var(--color-text);
    border-color: var(--color-border);
  }
  /* Split button: the main action and a menu of related ones, in one frame. */
  .split-btn {
    display: inline-flex;
    align-items: stretch;
    height: var(--control-h);
    border: 1px solid var(--color-border);
  }
  .split-btn > button {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: 0 var(--space-3);
    border: 0;
    background: transparent;
    font-size: var(--text-sm);
    font-weight: var(--weight-strong);
  }
  .split-btn > button + button {
    width: var(--control-h);
    justify-content: center;
    padding: 0;
    border-left: 1px solid var(--color-border);
  }
  .split-btn > button:hover {
    background: var(--color-control-active);
  }
  .actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
  }

  /* Menu: a button that opens a list of actions (role="menu"). Arrow keys move; Escape closes. */
  .menu-wrap {
    position: relative;
    display: inline-block;
  }
  .menu {
    position: absolute;
    top: calc(100% + var(--space-1));
    right: 0;
    z-index: var(--layer-menu);
    min-width: 15rem;
    padding: var(--space-1);
    border: 1px solid var(--color-border);
    background: var(--color-surface-raised);
    box-shadow: var(--shadow-raised);
  }
  .menu[data-align="start"] {
    left: 0;
    right: auto;
  }
  .menu [role="menuitem"],
  .menu [role="menuitemradio"],
  .menu [role="menuitemcheckbox"] {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    width: 100%;
    min-height: var(--control-h);
    padding: 0 var(--space-3);
    border: 0;
    background: transparent;
    color: var(--color-text);
    font-size: var(--text-sm);
    text-align: left;
    text-decoration: none;
  }
  .menu [role^="menuitem"]:hover,
  .menu [role^="menuitem"]:focus-visible {
    background: var(--color-control-active);
    outline-offset: calc(-1 * var(--focus-w));
  }
  .menu [aria-checked="true"] {
    font-weight: var(--weight-strong);
  }
  .menu [role^="menuitem"][disabled] {
    color: var(--color-text-muted);
    cursor: not-allowed;
  }
  .menu hr {
    margin: var(--space-1) 0;
    border: 0;
    border-top: 1px solid var(--color-divider);
  }
  .menu__label {
    padding: var(--space-2) var(--space-3) var(--space-1);
    color: var(--color-text-muted);
    font-size: var(--text-xs);
    font-weight: var(--weight-strong);
  }

  /* Copy button: copies a value and says so in a status message. */
  .copy-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--control-h);
    height: var(--control-h);
    border: 1px solid transparent;
    background: transparent;
    color: var(--color-text-muted);
  }
  .copy-btn:hover {
    border-color: var(--color-border);
    color: var(--color-text);
  }

  /* 4. Inputs --------------------------------------------------------------------------------- */
  .field {
    display: grid;
    gap: var(--space-1);
    min-width: 0;
  }
  .field > label,
  .field > legend,
  .field__label {
    font-size: var(--text-sm);
    font-weight: var(--weight-strong);
  }
  .hint {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }
  /* A link that opens a new tab: its words, then the external-link icon (LIBRARY.md, Links). */
  a[target="_blank"] > .i:last-child {
    margin-left: 0.25em;
    vertical-align: -0.15em;
  }
  /* Required fields: a * after the label (hidden from screen readers, which hear the control's required state),
     and "* = Required" at the bottom of the help slide-out, not in the form (LIBRARY.md, Fields). */
  .field__required {
    margin-left: 0.2em;
    color: var(--status-danger-fg);
  }
  .required-key {
    margin: 0;
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }
  .required-key .field__required {
    margin: 0 0.2em 0 0;
  }
  .field-error {
    display: flex;
    gap: var(--space-1);
    color: var(--status-danger-fg);
    font-size: var(--text-sm);
    font-weight: var(--weight-strong);
  }
  .input,
  .select,
  .textarea {
    width: 100%;
    min-height: var(--control-h);
    padding: 0 var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    background: var(--color-surface-raised);
    font-size: var(--text-md);
  }
  .textarea {
    min-height: 96px;
    padding: var(--space-2) var(--space-3);
  }
  .select {
    appearance: none;
    padding-right: var(--space-8);
    background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
      linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-position:
      calc(100% - 18px) 50%,
      calc(100% - 13px) 50%;
    background-size: 5px 5px;
    background-repeat: no-repeat;
  }
  .input:focus-visible,
  .select:focus-visible,
  .textarea:focus-visible {
    outline-offset: 0;
  }
  .input[aria-invalid="true"],
  .select[aria-invalid="true"],
  .textarea[aria-invalid="true"] {
    border-color: var(--status-danger-fg);
    border-width: 2px;
  }
  /* Read only (TheMightyTRexUK, 10 November 2026): a value the app filled in and uses itself, such as the setup code
     from the installer's link. Sunken, so it does not look like a box to type in; the text keeps full contrast and
     can still be selected and copied. Its hint says why it cannot be changed. */
  .input[readonly],
  .textarea[readonly] {
    background: var(--color-surface-sunken);
    border-style: dashed;
    cursor: default;
  }
  .input[disabled],
  .select[disabled] {
    background: var(--color-surface-sunken);
    color: var(--color-text-muted);
  }
  /* Search field: a plain box. Enter searches; the button is for screen readers and voice control. */
  .search-field {
    position: relative;
    display: flex;
  }
  .search-field .input {
    flex: 1;
  }
  /* Input with a unit or a button joined to it. */
  .input-group {
    display: flex;
  }
  .input-group > .input {
    flex: 1;
    min-width: 0;
  }
  .input-group > :not(:first-child) {
    margin-left: -1px;
  }
  .input-group__unit {
    display: inline-flex;
    align-items: center;
    padding: 0 var(--space-3);
    border: 1px solid var(--color-border);
    background: var(--color-surface-sunken);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }
  /* Checkbox and radio: the native control at 22px, with a 44px row as the target. */
  .check {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    min-height: var(--control-h);
    padding-block: 11px;
    cursor: pointer;
  }
  .check input {
    width: 22px;
    height: 22px;
    margin: 0;
    flex: none;
    accent-color: var(--color-accent);
  }
  .check__text {
    display: grid;
    gap: 2px;
  }
  .check__hint {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }
  /* Switch: a checkbox with role="switch", for settings that apply at once. */
  .switch {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    min-height: var(--control-h);
    cursor: pointer;
  }
  .switch input {
    appearance: none;
    position: relative;
    flex: none;
    width: 48px;
    height: 28px;
    margin: 0;
    border: 2px solid var(--color-border);
    background: var(--color-surface-sunken);
    transition: background var(--motion-fast);
  }
  .switch input::after {
    content: "";
    position: absolute;
    top: 3px;
    left: 3px;
    width: 18px;
    height: 18px;
    background: var(--color-text-muted);
    transition: transform var(--motion-fast);
  }
  .switch input:checked {
    border-color: var(--color-accent);
    background: var(--color-accent);
  }
  .switch input:checked::after {
    transform: translateX(20px);
    background: var(--color-accent-text);
  }
  /* Segmented control: radios that look like joined buttons (theme, text size, view). */
  .segmented {
    display: inline-flex;
    flex-wrap: wrap;
    gap: 0;              /* the buttons touch, with a line between them, even when it is a fieldset */
    width: fit-content;
    max-width: 100%;
    border: 1px solid var(--color-border);
  }
  .segmented label {
    position: relative;
  }
  .segmented input {
    position: absolute;
    inset: 0;
    opacity: 0;
    margin: 0;
    cursor: pointer;
  }
  .segmented span {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-height: calc(var(--control-h) - 2px);
    padding: 0 var(--space-3);
    font-size: var(--text-sm);
    font-weight: var(--weight-strong);
  }
  .segmented label + label span {
    border-left: 1px solid var(--color-divider);
  }
  .segmented input:checked + span {
    background: var(--color-accent);
    color: var(--color-accent-text);
  }
  /* Every button keeps room for the tick, so nothing moves when the choice changes. */
  .segmented span::before {
    content: "✓";
    visibility: hidden;
  }
  .segmented input:checked + span::before {
    visibility: visible;
  }
  .segmented input:focus-visible + span {
    outline: var(--focus-w) solid var(--color-focus);
    outline-offset: calc(-1 * var(--focus-w));
  }
  /* Option card: a radio or checkbox with a title and a line of explanation. */
  .option-cards {
    display: grid;
    gap: var(--space-2);
  }
  /* Option cards side by side: as many as fit, each at least 200px. */
  .option-cards--row {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
  }
  .option-card {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--space-1) var(--space-3);
    align-items: start;
    align-content: start; /* cards in one row share a height; their text stays at the top */
    padding: var(--space-3);
    border: 1px solid var(--color-divider);
    cursor: pointer;
  }
  .option-card input {
    width: 22px;
    height: 22px;
    margin: 0;
    accent-color: var(--color-accent);
  }
  .option-card strong {
    font-weight: var(--weight-strong);
  }
  .option-card small {
    grid-column: 2;
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    overflow-wrap: anywhere;   /* a long address wraps inside the card */
  }
  .option-card:has(input:checked) {
    border-color: var(--color-accent);
    background: var(--color-accent-soft);
  }
  .option-card:has(input:focus-visible) {
    outline: var(--focus-w) solid var(--color-focus);
    outline-offset: 2px;
  }
  /* Filter chip: a toggle button (aria-pressed) in a toolbar. */
  .filter-chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
  }
  .filter-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: var(--control-h);
    padding: 0 var(--space-3);
    border: 1px solid var(--color-border);
    background: var(--color-surface-raised);
    font-size: var(--text-sm);
    font-weight: var(--weight-strong);
  }
  .filter-chip[aria-pressed="true"] {
    border-color: var(--color-accent);
    background: var(--color-accent);
    color: var(--color-accent-text);
  }
  .filter-chip[aria-pressed="true"]::before {
    content: "✓";
  }
  /* Applied filter: shows a filter in use, with a button to remove it. */
  .applied {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-sm);
  }
  .applied__item {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    min-height: var(--control-h-compact);
    padding: 0 0 0 var(--space-2);
    border: 1px solid var(--color-divider);
    background: var(--color-accent-soft);
  }
  .applied__item button {
    display: inline-grid;
    place-items: center;
    width: var(--control-h-compact);
    height: var(--control-h-compact);
    border: 0;
    background: transparent;
  }

  /* 5. Content -------------------------------------------------------------------------------- */

  /* Status chip: words, an icon and a colour together (chosen style B, UX-04: outline with icon). A
     surface background, a border and text in the tone colour, so it stays clear on busy walls and
     covers. Put the state's icon first (svg, aria-hidden); a chip with no icon shows the tone's shape
     instead: dot, diamond (warning), square (danger), ring (muted). */
  .chip {
    --chip-tone: var(--status-neutral-fg);
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    min-height: 24px;
    padding: 1px var(--space-2);
    border: 1px solid var(--chip-tone);
    background: var(--color-surface);
    color: var(--chip-tone);
    font-size: var(--text-xs);
    font-weight: var(--weight-strong);
    line-height: 1.4;
    white-space: nowrap;
  }
  .chip > svg {
    flex: none;
    width: 14px;
    height: 14px;
  }
  .chip::before {
    content: "";
    flex: none;
    width: 8px;
    height: 8px;
    border-radius: var(--radius-round);
    background: currentColor;
  }
  .chip:has(> svg)::before {
    display: none;
  }
  .chip[data-tone="success"] {
    --chip-tone: var(--status-success-fg);
  }
  .chip[data-tone="info"] {
    --chip-tone: var(--status-info-fg);
  }
  .chip[data-tone="warning"] {
    --chip-tone: var(--status-warning-fg);
  }
  .chip[data-tone="warning"]::before {
    border-radius: 0;
    transform: rotate(45deg) scale(0.85);
  }
  .chip[data-tone="danger"] {
    --chip-tone: var(--status-danger-fg);
  }
  .chip[data-tone="danger"]::before {
    border-radius: 0;
  }
  .chip[data-tone="muted"] {
    --chip-tone: var(--status-muted-fg);
  }
  .chip[data-tone="muted"]::before {
    background: transparent;
    border: 2px solid currentColor;
  }
  /* On a cover or a coloured edge the chip keeps its own surface, so it reads on any picture. */
  .stat .chip,
  .cover .chip {
    background: var(--color-surface-raised);
  }
  /* A heading that a page moves focus to (a wizard step, a slide-out title) is not a control: no focus box.
     Screen readers still announce it. */
  :is(h1, h2, h3, h4)[tabindex="-1"]:focus {
    outline: none;
  }
  /* Stacks: children one under another with a fixed gap (1: 4px, 2: 8px, 4: 16px). */
  .stack-1,
  .stack-2,
  .stack-4 {
    display: grid;
    align-content: start;
  }
  .stack-1 {
    gap: var(--space-1);
  }
  .stack-2 {
    gap: var(--space-2);
  }
  .stack-4 {
    gap: var(--space-4);
  }
  /* Settings helpers. Inline save: a field and its Save button on one line. Form stack: fields one under
     another, at a readable width. Setting cost: the muted line under a setting that says what it costs. */
  .inline-save {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    align-items: end;
  }
  .inline-save > .field {
    flex: 0 1 420px;
  }
  .inline-save > .btn {
    flex: none;
  }
  .setting__control .inline-save {
    justify-content: end;
    flex-wrap: nowrap;
  }
  .form-stack {
    display: grid;
    gap: var(--space-4);
    max-width: 420px;
  }
  .setting__cost {
    display: block;
    margin-top: var(--space-1);
    color: var(--color-text-muted);
    font-size: var(--text-xs);
  }
  /* Title with status (2 November 2026): the status of the thing a heading names goes on the heading's
     line, straight after the name, centred on it; never on a line of its own under the heading. Details
     (a version, a time) go on the right or in the muted line, not in the chip. At most two chips; more
     belongs in a facts list.
     Markup: .title-status > h2 (or h1 to h4) + .title-status__chips > .chip... */
  .title-status {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-1) var(--space-3);
    min-width: 0;
  }
  .title-status > :is(h1, h2, h3, h4) {
    margin: 0;
  }
  .title-status__chips {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
  }
  /* Status icon: in a list of things that each have a state (Manage apps, what an app can access), the state
     is an icon straight after the name, with the word in hidden text and a tooltip, instead of a chip.
     Markup: <span class="status-icon" data-tone="success" title="Connected"><svg aria-hidden>…</svg>
     <span class="visually-hidden">Connected</span></span> */
  .status-icon {
    display: inline-flex;
    vertical-align: -3px;
    margin-left: var(--space-2);
    color: var(--status-neutral-fg);
  }
  .status-icon[data-tone="success"] {
    color: var(--status-success-fg);
  }
  .status-icon[data-tone="info"] {
    color: var(--status-info-fg);
  }
  .status-icon[data-tone="warning"] {
    color: var(--status-warning-fg);
  }
  .status-icon[data-tone="danger"] {
    color: var(--status-danger-fg);
  }
  .status-icon[data-tone="muted"] {
    color: var(--color-text-muted);
  }
  /* Count: a number on a tab or button, with its meaning in a visually hidden label. */
  .count {
    display: inline-block;
    min-width: 1.4em;
    padding: 1px 6px;
    background: var(--color-count);
    color: var(--color-count-text);
    font-size: var(--text-xs);
    font-weight: var(--weight-heading);
    font-variant-numeric: tabular-nums;
    text-align: center;
  }
  /* Tag: a neutral label for a format, genre or kind (not a status). */
  .tag {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: 1px var(--space-2);
    border: 1px solid var(--color-divider);
    color: var(--color-text-muted);
    font-size: var(--text-xs);
    font-weight: var(--weight-strong);
  }
  /* Avatar: initials in a circle (the only round shape apart from dots). */
  .avatar {
    display: inline-grid;
    place-items: center;
    width: 32px;
    height: 32px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-round);
    background: var(--color-accent-soft);
    font-size: var(--text-sm);
    font-weight: var(--weight-heading);
  }
  /* App icon: an image, or the app's initial on a tile. */
  .app-icon {
    display: grid;
    place-items: center;
    width: 40px;
    height: 40px;
    flex: none;
    border: 1px solid var(--color-divider);
    background: var(--color-accent-soft);
    font-weight: var(--weight-heading);
  }
  .app-icon img {
    width: 100%;
    height: 100%;
    object-fit: contain;
  }
  /* With its logo, the icon is just the image: no tile and no border. Initials keep the tile. */
  .app-icon.has-image {
    border-color: transparent;
    background: none;
  }
  /* Cover: a book cover at 2:3; audiobook squares are letterboxed, never cropped. */
  .cover {
    position: relative;
    display: grid;
    place-items: center;
    aspect-ratio: 2 / 3;
    width: 100%;
    overflow: hidden;
    background: var(--color-surface-sunken);
    color: var(--color-text-muted);
    font-size: var(--text-xs);
    text-align: center;
  }
  .cover img {
    width: 100%;
    height: 100%;
    object-fit: contain;
  }
  .cover__badge {
    position: absolute;
    left: var(--space-1);
    bottom: var(--space-1);
  }
  /* Facts: label and value pairs (book details, disk details, connection details). */
  .facts {
    display: grid;
    grid-template-columns: max-content minmax(0, 1fr);
    gap: var(--space-2) var(--space-6);
    margin: 0;
  }
  .facts dt {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }
  .facts dd {
    margin: 0;
    overflow-wrap: anywhere;
  }
  /* Stat: one number with its label; an edge colour plus words for status. */
  .stats {
    display: grid;
    gap: var(--space-3);
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
  }
  .stat {
    display: grid;
    gap: var(--space-1);
    padding: var(--space-4);
    border: 1px solid var(--color-divider);
    background: var(--color-surface);
  }
  .stat__value {
    font-size: var(--text-xl);
    font-weight: var(--weight-strong);
    font-variant-numeric: tabular-nums;
  }
  .stat__label {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }
  .stat[data-tone] {
    border-left-width: var(--status-edge-w);
  }
  .stat[data-tone="success"] {
    border-left-color: var(--status-success-fg);
  }
  .stat[data-tone="warning"] {
    border-left-color: var(--status-warning-fg);
  }
  .stat[data-tone="danger"] {
    border-left-color: var(--status-danger-fg);
  }
  /* Progress and meter: a bar with its value in words beside it. */
  .progress {
    height: 8px;
    border: 1px solid var(--color-divider);
    background: var(--color-surface-sunken);
  }
  .progress > span {
    display: block;
    height: 100%;
    background: var(--color-accent);
    transition: width var(--motion-panel);
  }
  .progress[data-tone="warning"] > span {
    background: var(--status-warning-fg);
  }
  .progress[data-tone="danger"] > span {
    background: var(--status-danger-fg);
  }
  /* Busy: a job whose end is not known (WIZARD.md section 5, A job's progress). A band moves across; with less motion
     asked for, the bar is shown half filled and still, and the words beside it say it is working. */
  .progress--busy {
    overflow: hidden;
  }
  .progress--busy > span {
    width: 30%;
    animation: progress-busy 1.4s ease-in-out infinite;
  }
  @keyframes progress-busy {
    from { margin-left: -30%; }
    to { margin-left: 100%; }
  }
  :root.pref-motion .progress--busy > span {
    width: 50%;
    animation: none;
  }
  @media (prefers-reduced-motion: reduce) {
    .progress--busy > span {
      width: 50%;
      animation: none;
    }
  }
  .meter {
    display: grid;
    gap: var(--space-1);
  }
  .meter__label {
    display: flex;
    justify-content: space-between;
    gap: var(--space-3);
    font-size: var(--text-sm);
  }
  /* The right-hand words (the timer and the count) stay on one line; the words on the left wrap instead. */
  .meter__label > :last-child:not(:first-child) {
    flex: none;
    white-space: nowrap;
  }
  /* Rows: a list where each row has a title, a meta line and actions at the end. */
  .rows {
    display: grid;
    margin: 0;
    padding: 0;
    list-style: none;
    border-top: 1px solid var(--color-divider);
  }
  .row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--space-1) var(--space-4);
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--color-divider);
  }
  .row--no-lead {
    grid-template-columns: minmax(0, 1fr) auto;
  }
  .row__title {
    font-weight: var(--weight-strong);
    overflow-wrap: anywhere;
  }
  .row__meta {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }
  .row__side {
    display: flex;
    flex-wrap: wrap;
    justify-content: end;
    align-items: center;
    gap: var(--space-2);
  }
  .row .progress {
    grid-column: 1 / -1;
  }
  /* A row that opens a slide-out: the title is the link, the whole row is the target. */
  .row--opens {
    position: relative;
    cursor: pointer;
  }
  .row--opens:hover {
    background: var(--color-control-active);
  }
  .row--opens .row__title button,
  .row--opens .row__title a {
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    text-align: left;
    text-decoration: none;
  }
  .row--opens .row__title :is(a, button)::after {
    content: "";
    position: absolute;
    inset: 0;
  }
  .row--opens .row__side {
    position: relative;
    z-index: 1;
  }
  .group-heading {
    padding-top: var(--space-4);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    font-weight: var(--weight-strong);
  }
  /* Table: for real tables of data (logs, history). Rows become cards on phones. */
  .table-wrap {
    overflow-x: auto;
    border: 1px solid var(--color-divider);
  }
  .table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--text-sm);
  }
  .table th {
    position: sticky;
    top: 0;
    padding: var(--space-2) var(--space-3);
    background: var(--color-surface-sunken);
    font-weight: var(--weight-strong);
    text-align: left;
    white-space: nowrap;
  }
  .table td {
    padding: var(--space-2) var(--space-3);
    border-top: 1px solid var(--color-divider);
    vertical-align: top;
  }
  /* A row's own heading (th scope="row") reads like its cells, in bold: no header shading, lined up with them. */
  .table tbody th {
    position: static;
    background: none;
    border-top: 1px solid var(--color-divider);
    vertical-align: top;
    white-space: normal;
  }
  /* A summary with a button on each row (such as Change): every cell on the row's middle line. */
  .table--middle td,
  .table--middle tbody th {
    vertical-align: middle;
  }
  .table th[aria-sort] button {
    display: inline-flex;
    gap: var(--space-1);
    padding: 0;
    border: 0;
    background: none;
    font-weight: inherit;
  }
  .table tbody tr:hover {
    background: var(--color-control-active);
  }
  .table .numeric {
    text-align: right;
  }
  /* Tables that line up: several tables one after another (tasks by category, a menu by section) keep the
     same columns, whatever their content. Give each table the same <colgroup>: the named widths below for
     short columns, and no width for the column that takes the rest. */
  .table--fixed {
    table-layout: fixed;
  }
  .table--fixed th {
    white-space: normal;
  }
  .table--fixed td {
    overflow-wrap: anywhere;
  }
  .table--fixed .col-id {
    width: 8rem;
  }
  .table--fixed .col-date {
    width: 11rem;
  }
  .table--fixed .col-state {
    width: 13rem;
  }
  .table--fixed .col-number {
    width: 7rem;
  }
  .table--fixed .col-quarter {
    width: 25%;
  }
  .table--fixed .col-third {
    width: 33%;
  }
  /* Notice: a message in the page. Icon and words, not colour alone. */
  .notice {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    gap: var(--space-1) var(--space-3);
    align-items: start;
    padding: var(--space-3) var(--space-4);
    border-left: 4px solid var(--status-info-fg);
    background: var(--status-info-bg);
    color: var(--status-info-fg);
  }
  .notice__body {
    display: grid;
    gap: var(--space-1);
  }
  .notice__title {
    font-weight: var(--weight-heading);
  }
  .notice p {
    color: var(--color-text);
  }
  .notice[data-tone="success"] {
    border-color: var(--status-success-fg);
    background: var(--status-success-bg);
    color: var(--status-success-fg);
  }
  .notice[data-tone="warning"] {
    border-color: var(--status-warning-fg);
    background: var(--status-warning-bg);
    color: var(--status-warning-fg);
  }
  .notice[data-tone="danger"] {
    border-color: var(--status-danger-fg);
    background: var(--status-danger-bg);
    color: var(--status-danger-fg);
  }
  /* Muted: an aside the reader may skip (the "Example:" callout). */
  .notice[data-tone="muted"] {
    border-color: var(--status-muted-fg);
    background: var(--status-muted-bg);
    color: var(--status-muted-fg);
  }
  :root[data-theme="dark"] .notice p {
    color: inherit;
  }
  /* Empty state: what is missing, why, and the one thing to do next. */
  .empty {
    display: grid;
    justify-items: center;
    gap: var(--space-3);
    padding: var(--space-8) var(--space-4);
    border: 1px dashed var(--color-border);
    text-align: center;
  }
  .empty__icon {
    color: var(--color-text-muted);
  }
  .empty p {
    max-width: 48ch;
    color: var(--color-text-muted);
  }
  /* Loading: skeleton blocks in the shape of the content, and a bar along the header. */
  .skeleton {
    display: block;
    min-height: 1em;
    background: linear-gradient(
      90deg,
      var(--color-surface-sunken) 25%,
      var(--color-divider) 50%,
      var(--color-surface-sunken) 75%
    );
    background-size: 200% 100%;
    animation: shimmer 1.4s ease-in-out infinite;
  }
  .loading-bar {
    position: relative;
    height: 3px;
    overflow: hidden;
  }
  .loading-bar[data-active]::before {
    content: "";
    position: absolute;
    inset: 0 auto 0 0;
    width: 35%;
    background: var(--color-accent);
    animation: loading 1.1s ease-in-out infinite;
  }
  /* Toast (option C, TheMightyTRexUK, 3 November 2026): a card in the corner with a status edge, an icon, a title,
     an optional line of detail, one optional action and Close. Tones: success, info, warning and danger
     (an error, on its tinted background so it stands apart). A lone .toast still works; components.js
     builds the .toasts stack. */
  .toast {
    position: fixed;
    z-index: var(--layer-toast);
    right: var(--space-4);
    bottom: max(var(--space-4), env(safe-area-inset-bottom, 0px));
    width: min(360px, calc(100vw - 32px));
    display: flex;
    align-items: start;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-2) var(--space-3) var(--space-3);
    background: var(--color-surface-raised);
    color: var(--color-text);
    border: 1px solid var(--color-border);
    border-left: var(--status-edge-w) solid var(--status-success-fg);
    box-shadow: var(--shadow-raised);
    font-size: var(--text-sm);
    animation: toast-in var(--motion-panel);
  }
  .toast[data-tone="info"] {
    border-left-color: var(--status-info-fg);
  }
  .toast > svg.i {
    flex: none;
    margin-top: 2px;
    color: var(--status-success-fg);
  }
  .toast[data-tone="info"] > svg.i {
    color: var(--status-info-fg);
  }
  .toast[data-tone="warning"] {
    border-left-color: var(--status-warning-fg);
  }
  .toast[data-tone="warning"] > svg.i {
    color: var(--status-warning-fg);
  }
  .toast[data-tone="danger"] {
    border-left-color: var(--status-danger-fg);
    background: var(--status-danger-bg);
  }
  .toast[data-tone="danger"] > svg.i {
    color: var(--status-danger-fg);
  }
  .toast__text {
    flex: 1;
    display: grid;
    gap: 2px;
    align-self: center;
  }
  .toast__title {
    font-weight: var(--weight-heading);
  }
  .toast__detail {
    color: var(--color-text-muted);
  }
  .toast__action {
    flex: none;
    align-self: center;
  }
  .toast__close {
    flex: none;
    margin: calc(var(--space-2) * -1) 0;
  }
  /* The stack: up to three, newest at the bottom, in the corner; full width on phones. */
  .toasts {
    position: fixed;
    z-index: var(--layer-toast);
    right: var(--space-4);
    bottom: max(var(--space-4), env(safe-area-inset-bottom, 0px));
    width: min(360px, calc(100vw - 32px));
    display: grid;
    gap: var(--space-2);
  }
  .toasts .toast {
    position: static;
    width: auto;
  }
  @keyframes toast-in {
    from {
      opacity: 0;
      transform: translateY(8px);
    }
  }
  :root.pref-motion .toast {
    animation: none;
  }
  @media (prefers-reduced-motion: reduce) {
    .toast {
      animation: none;
    }
  }
  @media (max-width: 720px) {
    .toast,
    .toasts {
      left: var(--space-2);
      right: var(--space-2);
      width: auto;
    }
  }
  /* Saved note: on settings pages the confirmation sits beside the button that saved, instead of a toast,
     and stays until the next change. */
  .saved-note {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    color: var(--status-success-fg);
    font-size: var(--text-sm);
    font-weight: var(--weight-strong);
  }
  .saved-note[hidden] {
    display: none;
  }
  /* Help: a small question button that opens a short explanation (a disclosure, not a tooltip). */
  .help {
    position: relative;
    display: inline-block;
  }
  .help > button {
    display: inline-grid;
    place-items: center;
    width: var(--control-h);
    height: var(--control-h);
    margin: calc(-1 * var(--space-2)) 0;
    border: 0;
    background: transparent;
    color: var(--color-text-muted);
  }
  .help__text {
    position: absolute;
    left: 0;
    top: calc(100% + var(--space-1));
    z-index: var(--layer-menu);
    width: min(320px, 80vw);
    padding: var(--space-3);
    border: 1px solid var(--color-border);
    background: var(--color-surface-raised);
    box-shadow: var(--shadow-raised);
    font-size: var(--text-sm);
  }
  /* Disclosure (accordion): a <details> with a 44px summary and a chevron that turns. */
  .disclosure {
    border: 1px solid var(--color-divider);
    background: var(--color-surface);
  }
  .disclosure + .disclosure {
    border-top: 0;
  }
  .disclosure > summary {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-height: var(--control-h);
    padding: var(--space-2) var(--space-4);
    font-weight: var(--weight-strong);
    cursor: pointer;
    list-style: none;
  }
  .disclosure > summary::-webkit-details-marker {
    display: none;
  }
  .disclosure > summary::after {
    content: "";
    width: 8px;
    height: 8px;
    margin-left: auto;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(45deg);
    transition: transform var(--motion-fast);
  }
  .disclosure[open] > summary::after {
    transform: rotate(225deg);
  }
  .disclosure__body {
    display: grid;
    gap: var(--space-3);
    padding: 0 var(--space-4) var(--space-4);
  }
  .disclosure__meta {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    font-weight: var(--weight-regular);
  }
  /* Releases: one .disclosure.release for each release, newest first, the installed one open. The summary has
     the version, an "Installed" chip, the date, and a count for each type of change in that type's chip tone; the
     body has one .release__type for each type, a coloured bar beside it, then Your settings and the way back. */
  .release > summary {
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-3);
    padding-block: var(--space-3);
  }
  .release > summary h3 {
    margin: 0;
    font-size: var(--text-lg);
  }
  .release[open] > summary {
    margin-bottom: var(--space-3);
    border-bottom: 1px solid var(--color-divider);
  }
  .release .disclosure__body {
    gap: var(--space-4);
  }
  .release__date {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    font-weight: var(--weight-regular);
  }
  .release__counts {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-left: auto;
    font-weight: var(--weight-regular);
  }
  .release__type {
    display: grid;
    gap: var(--space-2);
    padding: var(--space-1) 0 var(--space-1) var(--space-4);
    border-left: 3px solid var(--type-colour, var(--color-divider));
  }
  .release__type > h4 {
    margin: 0;
    color: var(--type-colour, var(--color-text));
    font-size: var(--text-base);
  }
  .release__type > ul {
    display: grid;
    gap: var(--space-2);
    margin: 0;
    padding-left: var(--space-4);
  }
  .release__type > p {
    margin: 0;
  }
  .release__type[data-type="added"] {
    --type-colour: var(--status-success-fg);
  }
  .release__type[data-type="changed"] {
    --type-colour: var(--status-info-fg);
  }
  .release__type[data-type="fixed"] {
    --type-colour: var(--status-warning-fg);
  }
  .release__type[data-type="security"] {
    --type-colour: var(--status-danger-fg);
  }
  .release__type[data-type="deprecated"],
  .release__type[data-type="removed"] {
    --type-colour: var(--status-muted-fg);
  }
  .release__type[data-type="settings"] {
    padding-block: var(--space-3);
    background: var(--color-surface-sunken);
  }
  .releases-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    margin-bottom: var(--space-3);
  }
  .releases-head > p {
    margin: 0;
  }
  /* Steps: where you are in a wizard (onboarding, upgrade). */
  .steps {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin: 0;
    padding: 0;
    list-style: none;
    counter-reset: step;
  }
  .steps li {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--color-divider);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    counter-increment: step;
  }
  .steps li::before {
    content: counter(step);
    display: inline-grid;
    flex: none;
    place-items: center;
    width: max(24px, 1.7em); /* grows with Text size, so the number stays inside */
    height: max(24px, 1.7em);
    border: 1px solid currentColor;
    font-weight: var(--weight-heading);
  }
  .steps li[aria-current="step"] {
    border-color: var(--color-accent);
    color: var(--color-text);
    font-weight: var(--weight-strong);
  }
  .steps li[data-done]::before {
    content: "✓";
    background: var(--color-accent);
    border-color: var(--color-accent);
    color: var(--color-accent-text);
  }
  /* Load more: infinite lists end with a button and a count, never only scrolling. */
  .load-more {
    display: grid;
    justify-items: center;
    gap: var(--space-2);
    padding: var(--space-6) 0;
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }

  /* A to Z jump list (8 November 2026): a row of letters over a long wall, # then A to Z. Letters with
     nothing are shown but cannot be chosen (aria-disabled, and "Q, no titles" for screen readers). It
     sticks under the header; the letter in view has aria-current="location" and a bar, so it is not shown
     by colour alone. Targets are 44px. On phones the row scrolls sideways, as .menu-tabs does.
     Markup: nav.az-jump[aria-label="Jump to letter"] > ol > li > a.az-jump__letter[href="#letter-a"];
     a letter with nothing: a.az-jump__letter[aria-disabled="true"][aria-label="Q, no titles"], no href.
     The wall: h2.wall-letter#letter-a[tabindex="-1"] before each letter's covers. components.js
     (setupAzJump) adds arrow keys, Home and End, one Tab stop, and focus on the heading after a jump. */
  .az-jump {
    --az-top: var(--header-h);
    position: sticky;
    top: var(--az-top);
    z-index: calc(var(--layer-header) - 1);
    min-width: 0;
    max-width: 100%;
    contain: inline-size;
    background: var(--color-bg);
    border-bottom: 1px solid var(--color-divider);
  }
  :root:has(.site-header__tabs) .az-jump {
    --az-top: calc(var(--header-h) + var(--tabs-h));
  }
  .az-jump ol {
    display: flex;
    gap: 2px;
    margin: 0;
    padding: 0;
    list-style: none;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .az-jump ol::-webkit-scrollbar {
    display: none;
  }
  .az-jump li {
    flex: 1 0 auto;
  }
  .az-jump__letter {
    position: relative;
    display: grid;
    place-items: center;
    min-width: var(--control-h);
    min-height: var(--control-h);
    padding: 0 var(--space-1);
    color: var(--color-text);
    font-size: var(--text-sm);
    font-weight: var(--weight-strong);
    text-decoration: none;
  }
  /* A letter with titles is a filled button and one with none is plain grey text, so the two differ by more than
     weight (Starboard issue #6, 10 November 2026). */
  .az-jump__letter[href] {
    background: var(--color-accent-soft);
    color: var(--color-accent);
    border-radius: var(--radius);
  }
  .az-jump__letter:hover {
    background: var(--color-control-active);
  }
  .az-jump__letter:focus-visible {
    outline-offset: calc(-1 * var(--focus-w));
  }
  .az-jump__letter[aria-current="location"] {
    background: var(--color-control-active);
  }
  .az-jump__letter[aria-current="location"]::after {
    content: "";
    position: absolute;
    inset: auto 0 0 0;
    height: var(--current-bar-w);
    background: var(--color-accent);
  }
  .az-jump__letter[aria-disabled="true"] {
    background: none;
    color: var(--color-text-muted);
    font-weight: var(--weight-regular);
    cursor: default;
  }
  @media (forced-colors: active) {
    .az-jump__letter[href] { border: 1px solid LinkText; color: LinkText; }
    .az-jump__letter[aria-disabled="true"] { color: GrayText; }
  }
  .az-jump__letter[aria-disabled="true"]:hover {
    background: none;
  }
  /* A jump lands clear of the header, the tabs and the letter row. */
  :root:has(.az-jump) {
    scroll-padding-top: calc(var(--header-h) + var(--control-h) + var(--space-4));
  }
  :root:has(.site-header__tabs):has(.az-jump) {
    scroll-padding-top: calc(var(--header-h) + var(--tabs-h) + var(--control-h) + var(--space-4));
  }
  /* A letter heading on a wall: full width, before that letter's covers. */
  .wall-letter {
    grid-column: 1 / -1;
    margin: var(--space-4) 0 0;
    padding-bottom: var(--space-1);
    border-bottom: 1px solid var(--color-divider);
    font-size: var(--text-lg);
    line-height: var(--leading-tight);
  }
  .wall-letter:focus {
    outline: none;
  }
  .wall-letter:focus-visible {
    outline: var(--focus-w) solid var(--color-focus);
  }

  /* 6. Containers ----------------------------------------------------------------------------- */

  /* Panel: a bordered box that groups related content. */
  .panel {
    display: grid;
    gap: var(--space-4);
    padding: var(--space-6);
    border: 1px solid var(--color-divider);
    background: var(--color-surface);
  }
  .panel__head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2) var(--space-4);
  }
  .panel[data-tone] {
    border-left-width: var(--status-edge-w);
  }
  .panel[data-tone="success"] {
    border-left-color: var(--status-success-fg);
  }
  .panel[data-tone="warning"] {
    border-left-color: var(--status-warning-fg);
  }
  .panel[data-tone="danger"] {
    border-left-color: var(--status-danger-fg);
  }
  .pref-contrast .panel {
    border-color: var(--color-border);
  }
  /* Card: one thing (an app, a disk, a connection) that opens its slide-out or its page. */
  .cards {
    display: grid;
    gap: var(--space-3);
    margin: 0;
    padding: 0;
    list-style: none;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
  }
  .card {
    position: relative;
    display: grid;
    gap: var(--space-2);
    align-content: start;
    padding: var(--space-4);
    border: 1px solid var(--color-divider);
    background: var(--color-surface);
  }
  .card:hover {
    border-color: var(--color-border);
  }
  .card:focus-within {
    outline: var(--focus-w) solid var(--color-focus);
    outline-offset: 2px;
  }
  .card__top {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-width: 0;
  }
  .card__title {
    min-width: 0;
    font-size: var(--text-md);
    overflow-wrap: anywhere;
  }
  .card__title :is(a, button) {
    padding: 0;
    border: 0;
    background: none;
    color: var(--color-text);
    font: inherit;
    font-weight: var(--weight-strong);
    text-align: left;
    text-decoration: none;
  }
  /* The whole card is the target for its main link; other buttons sit above it. */
  .card__title :is(a, button)::after {
    content: "";
    position: absolute;
    inset: 0;
  }
  .card__title :is(a, button):focus-visible {
    outline: none;
  }
  .card__meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }
  .card__actions {
    position: relative;
    z-index: 1;
  }
  .card[data-tone] {
    border-left-width: var(--status-edge-w);
  }
  .card[data-tone="success"] {
    border-left-color: var(--status-success-fg);
  }
  .card[data-tone="warning"] {
    border-left-color: var(--status-warning-fg);
  }
  .card[data-tone="danger"] {
    border-left-color: var(--status-danger-fg);
  }
  /* Media card: a cover with a title and a meta line (books, films, search results). */
  .media-grid {
    display: grid;
    gap: var(--space-6) var(--space-4);
    margin: 0;
    padding: 0;
    list-style: none;
    grid-template-columns: repeat(auto-fill, minmax(min(var(--cover-w, 148px), 45%), 1fr));
  }
  .media-grid[data-size="small"] {
    --cover-w: 108px;
  }
  .media-grid[data-size="large"] {
    --cover-w: 196px;
  }
  .media-card {
    position: relative;
    display: grid;
    gap: var(--space-2);
    align-content: start;
  }
  /* Covers in a row or grid always line up: the cover keeps its size, the title always takes two lines
     of space (one-line titles leave the second empty) and longer titles stop at two lines with an
     ellipsis; the full title is in the Summary. */
  .media-card {
    grid-template-rows: auto auto auto;
  }
  .media-card > .cover {
    width: 100%;
  }
  .media-card__title {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    min-height: calc(2em * var(--leading-tight));
    font-size: var(--text-sm);
    font-weight: var(--weight-strong);
    line-height: var(--leading-tight);
    overflow-wrap: anywhere;
  }
  .media-card__meta {
    display: block;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  .media-card__title :is(a, button) {
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    text-align: left;
    text-decoration: none;
  }
  .media-card__title :is(a, button)::after {
    content: "";
    position: absolute;
    inset: 0;
  }
  .media-card__title :is(a, button):focus-visible {
    outline: none;
  }
  .media-card:focus-within .cover {
    outline: var(--focus-w) solid var(--color-focus);
    outline-offset: 2px;
  }
  .media-card__meta {
    color: var(--color-text-muted);
    font-size: var(--text-xs);
  }
  /* Cover card (UX-42, 2 November 2026): a media card whose cover and title open the book's Summary,
     with a column of 44px actions down the cover's right edge (Details, Ebook, Audiobook, Add to a
     list, More). The person chooses the layout in Settings, saved with their account:
       data-actions="on" (the default): the column covers the right edge of the art and shows on
         hover and keyboard focus, and always on touch screens;
       data-actions="beside": the column sits beside the art and is always shown.
     Markup: li.media-card.cover-card[data-actions]
               > button.cover-card__open   named "<title>, details"; it stretches over the whole card,
                                           so the cover and the title open the Summary
               > .cover-card__art          the .cover (or the app's own cover) and .cover-actions
               > .media-card__title, .media-card__meta, then .cover-card__status for progress.
     A format already in the library has .cover-actions__tick on its icon and "in library" in its
     name, so the status is never colour alone. */
  .cover-card {
    grid-template-columns: minmax(0, 1fr);
  }
  .cover-card > .cover-card__open,
  .cover-card > .cover-card__art {
    grid-area: 1 / 1;
  }
  .cover-card__open {
    align-self: stretch;
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    cursor: pointer;
  }
  .cover-card__open::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
  }
  .cover-card__open:focus-visible {
    outline: none;
  }
  .cover-card__art {
    position: relative;
    min-width: 0;
  }
  .cover-card__art > :first-child {
    width: 100%;
  }
  .media-card.cover-card:focus-within .cover {
    outline: none;
  }
  .cover-card:has(> .cover-card__open:focus-visible) .cover-card__art > :first-child {
    outline: var(--focus-w) solid var(--color-focus);
    outline-offset: 2px;
  }
  .cover-card:has(> .cover-card__open:hover) .media-card__title {
    text-decoration: underline;
  }
  /* The column is exactly the height of the cover, whatever its shape or size (TheMightyTRexUK, 2 November
     2026): top and bottom edges match the art. Its 44px targets spread down the cover; on a cover
     shorter than five targets (a small or square cover) they share the height evenly instead of
     running past the bottom edge. Keep covers at least 120px tall so each target stays 24px or more. */
  .cover-actions {
    position: absolute;
    z-index: 2;
    top: 0;
    right: 0;
    bottom: 0;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    background: var(--color-surface-raised);
    border-left: 1px solid var(--color-divider);
    box-shadow: var(--shadow-raised);
    opacity: 0;
    transform: translateX(8px);
    pointer-events: none;
    transition:
      opacity var(--motion-fast),
      transform var(--motion-fast);
  }
  .cover-card:hover .cover-actions,
  .cover-card:focus-within .cover-actions,
  .cover-actions:has([aria-expanded="true"]) {
    opacity: 1;
    transform: none;
    pointer-events: auto;
  }
  @media (pointer: coarse) {
    .cover-actions {
      opacity: 1;
      transform: none;
      pointer-events: auto;
    }
  }
  .cover-actions:not(.cover-actions--row) > .cover-actions__btn {
    flex: 0 1 var(--control-h);
    min-height: 0;
  }
  /* Beside the art: the column takes the cover's height (size containment stops its own five
     targets from making the row taller than the cover). */
  .cover-card[data-actions="beside"] .cover-card__art {
    display: grid;
    grid-template-columns: minmax(0, 1fr) var(--control-h);
    align-items: start;
  }
  .cover-card[data-actions="beside"] .cover-actions {
    position: relative;
    top: auto;
    right: auto;
    bottom: auto;
    align-self: stretch;
    contain: size;
    border: 1px solid var(--color-divider);
    border-left: 0;
    box-shadow: none;
    opacity: 1;
    transform: none;
    pointer-events: auto;
  }
  .cover-actions__btn {
    position: relative;
    display: grid;
    place-items: center;
    width: var(--control-h);
    min-height: var(--control-h);
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 0;
    background: transparent;
    color: var(--color-text);
    cursor: pointer;
  }
  .cover-actions__btn:hover,
  .cover-actions__btn[aria-expanded="true"] {
    background: var(--color-control-active);
  }
  .cover-actions__btn:focus-visible {
    outline: var(--focus-w) solid var(--color-focus);
    outline-offset: calc(-1 * var(--focus-w));
  }
  .cover-actions__btn:disabled,
  .cover-actions__btn[aria-disabled="true"] {
    color: var(--color-text-muted);
    cursor: default;
  }
  .cover-actions__btn > svg {
    width: 18px;
    height: 18px;
  }
  .cover-actions__tick {
    position: absolute;
    right: 4px;
    bottom: 4px;
    display: grid;
    place-items: center;
    width: 16px;
    height: 16px;
    background: var(--status-success-fg);
    color: var(--color-surface-raised);
  }
  .cover-actions__tick svg {
    width: 12px;
    height: 12px;
    stroke-width: 3;
  }
  /* The hover label: the same words as the name, shown on hover and on keyboard focus. */
  .cover-actions__tip {
    position: absolute;
    z-index: 3;
    top: 50%;
    right: calc(100% + 6px);
    padding: 4px 8px;
    transform: translateY(-50%);
    background: var(--color-text);
    color: var(--color-bg);
    font-size: var(--text-xs);
    font-weight: var(--weight-strong);
    line-height: var(--leading-tight);
    white-space: nowrap;
    pointer-events: none;
    opacity: 0;
  }
  .cover-actions__btn:hover .cover-actions__tip,
  .cover-actions__btn:focus-visible .cover-actions__tip {
    opacity: 1;
  }
  .cover-actions__btn[aria-expanded="true"] .cover-actions__tip {
    opacity: 0;
  }
  .cover-card__status {
    position: relative;
    z-index: 2;
    display: flex;
    align-items: center;
    gap: var(--space-1);
    color: var(--color-text-muted);
    font-size: var(--text-xs);
  }
  .cover-card__status svg {
    flex: none;
    width: 14px;
    height: 14px;
  }
  /* A menu opened from inside a shelf that scrolls sideways sits over the page instead (the app
     sets top and left from the button). */
  .menu[data-fixed] {
    position: fixed;
    right: auto;
  }

  /* Book row (UX-44 A, 2 November 2026): a book in a list row (a series' Books in order, search
     results, Following). Pressing anywhere on the row opens the book's Summary. The same five actions
     as the cover column (Details, Ebook, Audiobook, Add to a list, More) sit in a row at the end,
     always shown, with the same names, ticks and hover labels. On phones they go under the title.
     Markup: .book-row (.book-row--lead when a lead column, such as the series position, comes first)
               > button.book-row__open   named "<title>, details"; it lies under the whole row
               > [lead], .book-row__art (a small cover), .book-row__text (.book-row__title first),
                 .cover-actions.cover-actions--row
     Links, buttons and check boxes inside the text stay pressable; the rest of the row opens the
     Summary. Set --book-row-art for the cover width (56px by default). */
  .book-row {
    --book-row-art: 56px;
    position: relative;
    isolation: isolate;
    display: grid;
    grid-template-columns: var(--book-row-art) minmax(0, 1fr) auto;
    gap: var(--space-2) var(--space-4);
    align-items: center;
    padding: var(--space-3) 0;
  }
  .book-row--lead {
    grid-template-columns: auto var(--book-row-art) minmax(0, 1fr) auto;
  }
  .book-row__open {
    position: absolute;
    inset: 0;
    z-index: -1;
    margin: 0;
    padding: 0;
    border: 0;
    background: transparent;
    color: inherit;
    cursor: pointer;
  }
  .book-row__open:hover {
    background: var(--color-control-active);
  }
  .book-row__open:focus-visible {
    outline: var(--focus-w) solid var(--color-focus);
    outline-offset: calc(-1 * var(--focus-w));
  }
  .book-row > :not(.book-row__open, .cover-actions) {
    pointer-events: none;
  }
  .book-row :is(a, button, input, select, textarea, label, summary, [tabindex]) {
    pointer-events: auto;
  }
  .book-row__art {
    width: var(--book-row-art);
  }
  .book-row__art > * {
    width: 100%;
  }
  .book-row__text {
    display: grid;
    gap: var(--space-1);
    min-width: 0;
  }
  .book-row__title {
    margin: 0;
    font-size: var(--text-md);
    font-weight: var(--weight-strong);
    line-height: var(--leading-tight);
    overflow-wrap: anywhere;
  }
  .book-row:has(> .book-row__open:hover) .book-row__title {
    text-decoration: underline;
  }
  /* Progress from getting a format shows under the actions. */
  .book-row > .cover-card__status {
    grid-column: -2 / -1;
    justify-self: end;
  }
  .cover-actions--row {
    position: relative;
    top: auto;
    right: auto;
    bottom: auto;
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    min-height: 0;
    background: transparent;
    border: 0;
    box-shadow: none;
    opacity: 1;
    transform: none;
    pointer-events: auto;
  }
  .cover-actions--row .cover-actions__tip {
    top: auto;
    right: auto;
    bottom: calc(100% + 6px);
    left: 50%;
    transform: translateX(-50%);
  }
  @media (max-width: 720px) {
    .book-row {
      grid-template-columns: var(--book-row-art) minmax(0, 1fr);
    }
    .book-row--lead {
      grid-template-columns: auto var(--book-row-art) minmax(0, 1fr);
    }
    .book-row > .cover-actions--row {
      grid-column: -2 / -1;
    }
  }

  /* Small cover tile (UX-44 B, 2 November 2026): a cover too small for the action column (series
     gaps, calendar day cards) gets one 44px More button on its corner. Its menu says each action in
     words: View details, Get the ebook (or "Ebook in library"), Get the audiobook, Add to a list,
     Open the book page. The cover itself opens the Summary.
     Markup: .cover-card[data-actions="more"] > .cover-card__art > the cover, then
             button.cover-card__more (aria-haspopup="menu", named "More for <title>") and its .menu.
     .cover-card__more also works on its own in any positioned box, such as a calendar day card. */
  .cover-card__more {
    position: absolute;
    z-index: 2;
    top: 2px;
    right: 2px;
    display: grid;
    place-items: center;
    width: var(--control-h);
    height: var(--control-h);
    margin: 0;
    padding: 0;
    border: 1px solid var(--color-border);
    border-radius: 0;
    background: var(--color-surface-raised);
    color: var(--color-text);
    cursor: pointer;
  }
  .cover-card__more:hover,
  .cover-card__more[aria-expanded="true"] {
    background: var(--color-control-active);
  }
  .cover-card__more:focus-visible {
    outline: var(--focus-w) solid var(--color-focus);
    outline-offset: 2px;
  }
  .cover-card__more > svg {
    width: 18px;
    height: 18px;
  }

  /* Show art (FEAT-R decision 2, fill 4, 2 November 2026): a radio show's square artwork in the 2:3
     book shape, with the same 1px outline as book covers, so shows and books line up. The square
     image sits at the top and is never cropped; the bottom third is a label in the image's main colour
     with the station and the show's name as real text. The app sets --show-art-bg and --show-art-fg
     from the image (it serves its own copy, so the browser can read it), darkened until the white text
     passes 4.5:1; without them the label is a neutral surface. With no image the square shows the name.
     Markup: .show-art[role="img"][aria-label="<show>, <station>"]
               > img.show-art__img (or span.show-art__text), span.show-art__label
                 > span.show-art__station, span.show-art__name */
  .show-art {
    container-type: inline-size;
    display: grid;
    grid-template-rows: auto minmax(0, 1fr);
    width: 100%;
    aspect-ratio: 2 / 3;
    overflow: hidden;
    outline: 1px solid var(--color-border);
    outline-offset: -1px;
    background: var(--show-art-bg, var(--color-surface-sunken));
    color: var(--show-art-fg, var(--color-text));
  }
  .show-art__img,
  .show-art__text {
    display: block;
    width: 100%;
    aspect-ratio: 1;
  }
  .show-art__img {
    object-fit: contain;
  }
  .show-art__text {
    display: grid;
    place-items: center;
    padding: var(--space-3);
    overflow: hidden;
    background: var(--color-surface-raised);
    color: var(--color-text);
    font-size: var(--text-sm);
    font-weight: var(--weight-strong);
    line-height: var(--leading-tight);
    text-align: center;
    overflow-wrap: anywhere;
  }
  .show-art__label {
    display: grid;
    align-content: center;
    gap: 2px;
    min-width: 0;
    padding: var(--space-1) var(--space-3);
    overflow: hidden;
  }
  .show-art__station {
    overflow: hidden;
    font-size: var(--text-xs);
    font-weight: var(--weight-strong);
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  .show-art__name {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    font-size: var(--text-sm);
    font-weight: var(--weight-strong);
    line-height: var(--leading-tight);
    overflow-wrap: anywhere;
  }
  /* Small show art (under 120px wide): the name alone in the label. */
  @container (max-width: 120px) {
    .show-art__station {
      display: none;
    }
    .show-art__label {
      padding: var(--space-1) var(--space-2);
    }
    .show-art__name {
      font-size: var(--text-xs);
    }
  }
  /* Logo tile: a station's (or a channel's) logo on a plain tile, or its name as text when there is
     no logo. Markup: .logo-tile > img (alt="") or span.logo-tile__text. */
  .logo-tile {
    display: grid;
    place-items: center;
    width: 100%;
    aspect-ratio: 3 / 2;
    padding: var(--space-3);
    overflow: hidden;
    border: 1px solid var(--color-divider);
    background: var(--color-surface-sunken);
  }
  .logo-tile > img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
  }
  .logo-tile__text {
    font-weight: var(--weight-strong);
    line-height: var(--leading-tight);
    text-align: center;
    overflow-wrap: anywhere;
  }

  /* Shelf (UX-30, option A, 2 November 2026): one row of covers that scrolls sideways with no scroll
     bar. At each end the covers fade into the page, with a 44px arrow over the fade in the middle of
     the covers; the arrow and its fade show only when there are more books that way. Each press moves
     one screen less one cover; with reduced motion the shelf jumps. Tabbing to a book scrolls it fully
     into view (scroll-padding keeps it clear of the fade). On phones there are no arrows; the fade
     alone shows there is more.
     Markup: .shelf-scroller > ul.shelf-track, .shelf-edge.shelf-edge--start > button.shelf-arrow
     ("Earlier books in <shelf>"), .shelf-edge.shelf-edge--end > button.shelf-arrow ("More books in
     <shelf>"). Set --shelf-cover-w to the cover width and --shelf-fade-bg to the colour behind. */
  .shelf-scroller {
    --shelf-cover-w: var(--cover-w, 148px);
    --shelf-fade: 96px;
    --shelf-fade-bg: var(--color-bg);
    position: relative;
    min-width: 0;
  }
  .shelf-track {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: var(--shelf-cover-w);
    gap: var(--space-4);
    margin: 0;
    padding: var(--space-1) var(--space-1) var(--space-2);
    list-style: none;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    scroll-padding-inline: var(--shelf-fade);
    scroll-behavior: smooth;
    scrollbar-width: none;
  }
  .shelf-track::-webkit-scrollbar {
    display: none;
  }
  .shelf-track > li {
    min-width: 0;
    scroll-snap-align: start;
  }
  .shelf-edge {
    position: absolute;
    z-index: 3;
    top: 0;
    bottom: 0;
    width: var(--shelf-fade);
    pointer-events: none;
  }
  .shelf-edge--start {
    left: 0;
    background: linear-gradient(to right, var(--shelf-fade-bg) 20%, transparent);
  }
  .shelf-edge--end {
    right: 0;
    background: linear-gradient(to left, var(--shelf-fade-bg) 20%, transparent);
  }
  .shelf-arrow {
    position: absolute;
    /* The middle of the covers; an app can measure it and set --shelf-arrow-top. */
    top: var(--shelf-arrow-top, calc(var(--space-1) + var(--shelf-cover-w) * 0.75));
    display: grid;
    place-items: center;
    width: var(--control-h);
    height: var(--control-h);
    padding: 0;
    border: 1px solid var(--color-border);
    border-radius: 0;
    background: var(--color-surface-raised);
    color: var(--color-text);
    box-shadow: var(--shadow-raised);
    transform: translateY(-50%);
    cursor: pointer;
    pointer-events: auto;
  }
  .shelf-edge--start .shelf-arrow {
    left: 0;
  }
  .shelf-edge--end .shelf-arrow {
    right: 0;
  }
  .shelf-arrow:hover {
    background: var(--color-surface-sunken);
  }
  .shelf-arrow > svg {
    width: 20px;
    height: 20px;
  }
  @media (max-width: 720px) {
    .shelf-scroller {
      --shelf-fade: 48px;
    }
    .shelf-arrow {
      display: none;
    }
  }
  @media (prefers-reduced-motion: reduce) {
    .shelf-track {
      scroll-behavior: auto;
    }
    .cover-actions {
      transition: none;
    }
  }
  :root.pref-motion .shelf-track {
    scroll-behavior: auto;
  }
  :root.pref-motion .cover-actions {
    transition: none;
  }

  /* Media row: the list view of the same thing (cover on the left). */
  .media-row {
    display: grid;
    grid-template-columns: 72px minmax(0, 1fr) auto;
    gap: var(--space-4);
    align-items: start;
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--color-divider);
  }

  /* Section toolbar (design D): heading, view switch, search in the section, filter chips, menus,
     sort and end actions on one line; applied filters under it. */
  .toolbar {
    display: grid;
    gap: var(--space-3);
    padding-bottom: var(--space-3);
    border-bottom: 1px solid var(--color-divider);
  }
  .toolbar__row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;  /* may shrink below its chips, so a long row of chips scrolls inside it, not the page */
  }
  .toolbar__search {
    flex: 1 1 220px;
    max-width: 340px;
  }
  .toolbar__row > .filter-chips {
    min-width: 0;
  }
  .toolbar__end {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin-left: auto;
  }
  .toolbar__count {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }

  /* Settings: a section with a heading, then rows of label, explanation and control. */
  .settings-section {
    display: grid;
    gap: var(--space-4);
    max-width: 880px;
  }
  .settings-section__head {
    display: grid;
    gap: var(--space-1);
    padding-bottom: var(--space-3);
    border-bottom: 1px solid var(--color-divider);
  }
  .settings-rows {
    display: grid;
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .setting {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(200px, 320px);
    gap: var(--space-2) var(--space-6);
    align-items: center;
    padding: var(--space-4) 0;
    border-bottom: 1px solid var(--color-divider);
  }
  .setting__text {
    display: grid;
    gap: 2px;
  }
  .setting__label {
    font-weight: var(--weight-strong);
  }
  .setting__hint {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }
  .setting__control {
    justify-self: end;
    width: 100%;
    display: flex;
    justify-content: end;
  }
  /* A setting that opens a slide-out to edit (a connection, an account). */
  .setting--opens .setting__control {
    width: auto;
  }
  .form-grid {
    display: grid;
    gap: var(--space-4);
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  }
  .form-grid [data-span="all"] {
    grid-column: 1 / -1;
  }
  fieldset {
    display: grid;
    gap: var(--space-2);
    min-width: 0;
    margin: 0;
    padding: 0;
    border: 0;
  }

  /* Panel header: the same bar on every slide-out, dialog and the phone menu. Bold 1rem name, close
     button, white bar, grey line. It stays put while the content scrolls. */
  .panel-header {
    position: sticky;
    top: 0;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    min-height: 56px;
    padding: var(--space-1) var(--space-1) var(--space-1) var(--space-5);
    background: var(--color-panel-header);
    border-bottom: 1px solid var(--color-divider);
  }
  .panel-header__title {
    margin: 0;
    font-size: var(--text-md);
    font-weight: var(--weight-heading);
    line-height: var(--leading-tight);
  }
  .panel-header__brand {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-weight: var(--weight-heading);
  }
  .panel-header .icon-btn {
    border-color: transparent;
  }
  .panel-header .icon-btn:hover {
    border-color: var(--color-border);
  }
  .pref-contrast .panel-header {
    border-bottom-color: currentColor;
  }
  /* Slide-outs (from the right, and the phone menu from the left): the header wears the page background and
     the body is the raised surface (white, or the dark surface), so the header reads as part of the page and
     the body as the thing that slid out. Dialogs keep the plain header. */
  .side-panel .panel-header,
  .phone-menu .panel-header {
    background: var(--color-slide-header);
  }

  /* Slide-out (side panel): from the right, below nothing, over a dimmed page. For details of one
     thing and for editing it without leaving the list. Escape, the close button and a press outside
     close it; focus moves in, and back to what opened it. */
  .side-panel-layer {
    position: fixed;
    inset: 0;
    z-index: var(--layer-panel);
  }
  .side-panel-backdrop {
    position: absolute;
    inset: 0;
    background: var(--color-backdrop);
    animation: fade-in var(--motion-panel);
  }
  .side-panel {
    position: absolute;
    inset: 0 0 0 auto;
    display: flex;
    flex-direction: column;
    width: min(max(var(--panel-w), 33.333vw), 100vw);   /* about a third of the screen */
    background: var(--color-surface-raised);
    border-left: 1px solid var(--color-border);
    box-shadow: var(--shadow-raised);
    animation: slide-in-right var(--motion-panel);
  }
  /* Release notes and help: about half the screen (LIBRARY.md, Slide-out widths). */
  .side-panel--wide {
    width: min(max(640px, 50vw), 100vw);
  }
  .side-panel__body {
    flex: 1;
    overflow-y: auto;
    display: grid;
    align-content: start;
    gap: var(--space-5);
    padding: var(--space-5);
  }
  .side-panel__foot {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-5) calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
    border-top: 1px solid var(--color-divider);
    background: var(--color-surface-raised);
  }
  .side-panel__foot .btn--danger {
    margin-left: auto;
  }
  .side-panel__lead {
    display: grid;
    grid-template-columns: 96px minmax(0, 1fr);
    gap: var(--space-4);
    align-items: start;
  }

  /* Dialog: a modal for a decision or a short task that must finish before going on. */
  .dialog {
    width: min(var(--dialog-w), calc(100vw - 32px));
    max-height: calc(100dvh - 32px);
    margin: auto;
    padding: 0;
    border: 1px solid var(--color-border);
    background: var(--color-surface-raised);
    color: var(--color-text);
    box-shadow: var(--shadow-raised);
    overflow-y: auto;
  }
  .dialog::backdrop {
    background: var(--color-backdrop);
  }
  /* Long dialogs (help): the header and foot stay in view and only the body scrolls. */
  .dialog--scroll[open] {
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
  .dialog--scroll .panel-header,
  .dialog--scroll .dialog__foot {
    flex: none;
  }
  .dialog--scroll .dialog__body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-gutter: stable;
  }
  .dialog--scroll .dialog__foot {
    padding-top: var(--space-4);
    border-top: 1px solid var(--color-divider);
  }
  .dialog__body {
    display: grid;
    gap: var(--space-4);
    padding: var(--space-6);
  }
  .dialog__foot {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    padding: 0 var(--space-6) var(--space-6);
  }

  /* Save bar: one way to save a settings section. It sticks to the bottom of the window while there
     are unsaved changes, says how many, and offers Save and Discard. */
  .save-bar {
    position: sticky;
    bottom: 0;
    z-index: var(--layer-sticky);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2) var(--space-4);
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-border);
    border-left: var(--status-edge-w) solid var(--color-accent);
    background: var(--color-surface-raised);
    box-shadow: var(--shadow-raised);
  }
  .save-bar__text {
    flex: 1 1 200px;
    font-weight: var(--weight-strong);
  }
  .save-bar[hidden] {
    display: none;
  }

  /* Timeline: what happened to one thing, newest first (a request's searches, downloads, retries). */
  .timeline {
    display: grid;
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .timeline__item {
    position: relative;
    display: grid;
    gap: 2px;
    padding: 0 0 var(--space-4) var(--space-6);
  }
  .timeline__item::before {
    content: "";
    position: absolute;
    left: 5px;
    top: 6px;
    width: 12px;
    height: 12px;
    border: 2px solid var(--color-border);
    border-radius: var(--radius-round);
    background: var(--color-surface-raised);
  }
  .timeline__item:not(:last-child)::after {
    content: "";
    position: absolute;
    left: 10px;
    top: 20px;
    bottom: 0;
    width: 2px;
    background: var(--color-divider);
  }
  .timeline__item[data-tone="success"]::before {
    border-color: var(--status-success-fg);
    background: var(--status-success-fg);
  }
  .timeline__item[data-tone="warning"]::before {
    border-color: var(--status-warning-fg);
    border-radius: 0;
    transform: rotate(45deg) scale(0.85);
  }
  .timeline__item[data-tone="danger"]::before {
    border-color: var(--status-danger-fg);
    background: var(--status-danger-fg);
    border-radius: 0;
  }
  .timeline__title {
    font-weight: var(--weight-strong);
  }
  .timeline__meta {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }

  /* Icons from the sprite: three sizes. */
  svg.i {
    width: var(--icon-md);
    height: var(--icon-md);
  }
  svg.i-lg {
    width: var(--icon-lg);
    height: var(--icon-lg);
  }
  svg.i-xl {
    width: 32px;
    height: 32px;
  }
  /* Two or three lines, then an ellipsis (overviews in rows). */
  .nowrap {
    white-space: nowrap;
  }
  .clamp-2,
  .clamp-3 {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
  }
  .clamp-3 {
    -webkit-line-clamp: 3;
  }

  /* Apps with no rail (Starboard): the name and logo start header row 1. */
  .site-header--no-rail .site-header__bar {
    padding-left: var(--space-4);
  }
  .site-header--no-rail .site-header__brand {
    display: inline-flex;
  }
  /* Narrow page for Sign in and Set up: a slim header (name and Display) and one centred column. */
  .page--narrow {
    max-width: 480px;
  }

  /* Section head: a heading with its meta and controls on one line (a time range, a count). */
  .section-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2) var(--space-4);
  }
  .section-head__control {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-sm);
  }

  /* Phone menu: a main menu item with no 1st level is a link that looks like a section button. */
  .phone-menu__section > a {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-height: 52px;
    padding: 0 var(--space-4);
    color: var(--color-text);
    font-size: var(--text-md);
    font-weight: var(--weight-strong);
    text-decoration: none;
  }
  .phone-menu__section > a[aria-current="page"] {
    background: var(--color-control-active);
    box-shadow: inset 3px 0 0 var(--color-accent);
    color: var(--color-accent);
  }

  /* Status words in a stat tile sit at their own width. */
  .stat .chip {
    justify-self: start;
  }
  /* A small cover as a row's leading slot. */
  .cover--row {
    width: 48px;
  }
  /* A list straight under a settings section head shares its line. */
  .settings-section__head + .rows,
  .settings-section__head + .settings-rows {
    border-top: 0;
  }
  /* Command: a code block with a copy button beside it. */
  .command {
    display: flex;
    align-items: stretch;
    border: 1px solid var(--color-divider);
    background: var(--color-surface-sunken);
  }
  .command .code-block {
    flex: 1;
    min-width: 0;
    border: 0;
    background: transparent;
  }
  .command .copy-btn {
    height: auto;
    min-height: var(--control-h);
    border-left: 1px solid var(--color-divider);
  }
  /* A copy button with its word beside the icon, for commands newcomers copy. It grows to fit. */
  .copy-btn--label {
    flex: none;
    width: auto;
    gap: var(--space-2);
    padding: 0 var(--space-4);
    color: var(--color-text);
    white-space: nowrap;
  }

  /* Line chart: grid, axes, one or two series (the second dashed, so it is not colour alone), a
     legend, and always a "Show as a table" disclosure beside it. */
  .chart {
    display: grid;
    gap: var(--space-2);
    align-content: start;
  }
  .chart__svg {
    display: block;
    width: 100%;
    height: auto;
    overflow: visible;
  }
  .chart__svg:focus-visible {
    outline: var(--focus-w) solid var(--color-focus);
    outline-offset: 2px;
  }
  .chart__grid {
    stroke: var(--color-divider);
    stroke-width: 1;
  }
  .chart__axis {
    fill: var(--color-text-muted);
    font-family: var(--font);
    font-size: var(--text-xs); /* follows Text size; charts draw their viewBox at the real width */
    font-variant-numeric: tabular-nums;
  }
  .chart__line {
    fill: none;
    stroke-width: 2;
    stroke-linejoin: round;
    stroke-linecap: round;
    vector-effect: non-scaling-stroke;
  }
  .chart__line--1 {
    stroke: var(--chart-1);
  }
  .chart__line--2 {
    stroke: var(--chart-2);
    stroke-dasharray: 6 4;
  }
  .chart__legend {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-4);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }
  .chart__legend span {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
  }
  .chart__swatch {
    display: inline-block;
    width: 16px;
    height: 0;
    border-top: 3px solid var(--chart-1);
  }
  .chart__swatch--2 {
    border-top: 3px dashed var(--chart-2);
  }

  /* List and detail: a list on the left and the chosen item on the right; on phones, the list alone
     and the item opens as a page. */
  .list-detail {
    display: grid;
    grid-template-columns: minmax(240px, 320px) minmax(0, 1fr);
    gap: var(--space-6);
    align-items: start;
  }
  .list-detail__list {
    position: sticky;
    top: calc(var(--header-h) + var(--tabs-h) + var(--space-4));
    max-height: calc(100dvh - var(--header-h) - var(--tabs-h) - var(--space-8));
    overflow-y: auto;
  }
  /* The chosen row in a list. */
  .row[aria-current="true"],
  .row[aria-current="page"] {
    background: var(--color-control-active);
    box-shadow: inset var(--current-bar-w) 0 0 var(--color-accent);
  }
  /* A row with a checkbox, a cover, the text and the actions (bulk selection). */
  .row--select {
    grid-template-columns: auto auto minmax(0, 1fr) auto;
  }
  .row--select > input[type="checkbox"] {
    width: 22px;
    height: 22px;
    margin: 0 var(--space-2);
    accent-color: var(--color-accent);
  }
  .cover--thumb {
    width: 48px;
  }

  /* Item header: the top of a book, author, series or narrator page. */
  .item-header {
    display: grid;
    grid-template-columns: 176px minmax(0, 1fr);
    gap: var(--space-6);
    align-items: start;
  }
  .item-header__art {
    width: 176px;
  }
  .item-header__art--round .cover {
    aspect-ratio: 1;
    border-radius: var(--radius-round);
  }
  .item-header__text {
    display: grid;
    gap: var(--space-3);
    min-width: 0;
  }
  .item-header__byline {
    color: var(--color-text-muted);
  }
  .item-header__chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
  }
  /* Solid-colour logo (UX-43, 2 November 2026): any logo image with a transparent background, drawn
     as one solid colour through a CSS mask, so it follows the theme and High contrast. Set --logo to
     url("…") and --logo-ratio to the image's width / height. Text colour by default;
     data-colour="accent" for the accent colour. Markup: span.logo-mask[role="img"][aria-label]. */
  .logo-mask {
    --logo-colour: var(--color-text);
    display: block;
    width: min(100%, var(--logo-w, 520px));
    max-height: var(--logo-max-h, 160px);
    aspect-ratio: var(--logo-ratio, 4 / 1);
    background: var(--logo-colour);
    -webkit-mask: var(--logo) left center / contain no-repeat;
    mask: var(--logo) left center / contain no-repeat;
  }
  .logo-mask[data-colour="accent"] {
    --logo-colour: var(--color-accent);
  }
  .logo-mask[hidden] {
    display: none;
  }
  /* Wordmark: a name set large in place of a logo, when there is no logo yet. */
  .wordmark {
    font-size: clamp(2rem, 4vw, 3rem);
    font-weight: 700;
    line-height: 1.05;
    color: var(--color-text);
    overflow-wrap: anywhere;
  }

  /* Series header (UX-43): the logo (or the name as a wordmark) above the meta line, a short
     description, progress and the actions; a fan of the first covers on the right, hidden on
     phones. The name stays the page's h1 under the logo.
     Markup: header.series-head > .series-head__text (> .logo-mask, h1.series-head__name
     (.wordmark when there is no logo), .series-head__meta, .series-head__desc,
     .series-head__progress, .series-head__actions) + .cover-fan[aria-hidden]. */
  .series-head {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--space-8);
    align-items: center;
    padding: var(--space-8);
    border-bottom: 1px solid var(--color-divider);
    background: var(--color-surface);
  }
  .series-head__text {
    display: grid;
    gap: var(--space-3);
    min-width: 0;
  }
  .series-head__text > .logo-mask {
    --logo-w: 340px;
  }
  .series-head__name {
    margin: 0;
    color: var(--color-text-muted);
    font-size: var(--text-lg);
    font-weight: var(--weight-heading);
  }
  .series-head__name.wordmark {
    color: var(--color-text);
    font-size: clamp(2rem, 4vw, 3rem);
    font-weight: 700;
  }
  .series-head__meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-4);
    margin: 0;
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }
  .series-head__meta strong {
    color: var(--color-text);
  }
  .series-head__desc {
    max-width: 68ch;
    margin: 0;
  }
  .series-head__progress {
    display: grid;
    gap: var(--space-1);
    max-width: 420px;
    font-size: var(--text-sm);
  }
  .series-head__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
  }
  /* Fan of covers: the first four covers of a series, overlapping and turned a little. Decoration
     only (aria-hidden): the books themselves are listed below. */
  .cover-fan {
    position: relative;
    width: 300px;
    height: 240px;
  }
  .cover-fan > * {
    position: absolute;
    bottom: 0;
    width: 128px;
    aspect-ratio: 2 / 3;
    border: 2px solid var(--color-surface);
    background: var(--color-surface-sunken);
    box-shadow: var(--shadow-raised);
    object-fit: cover;
  }
  .cover-fan > :nth-child(1) {
    left: 0;
    transform: rotate(-8deg);
  }
  .cover-fan > :nth-child(2) {
    left: 56px;
    transform: rotate(-3deg);
  }
  .cover-fan > :nth-child(3) {
    left: 112px;
    transform: rotate(2deg);
  }
  .cover-fan > :nth-child(4) {
    left: 168px;
    transform: rotate(7deg);
  }

  /* Logo slide-out (UX-43): the saved copy, three ways to get a logo as tabs, the colour and a
     light and dark preview. Markup: .logo-saved (> h3, .logo-saved__row > .logo-mask +
     .logo-saved__text), .logo-tabs[role=tablist] > button[role=tab], .logo-pane[role=tabpanel],
     .logo-preview > .logo-preview__light + .logo-preview__dark. */
  .logo-saved {
    display: grid;
    gap: var(--space-3);
    padding: var(--space-4);
    border: 1px solid var(--color-divider);
    background: var(--color-surface);
  }
  .logo-saved h3 {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0;
    font-size: var(--text-md);
  }
  .logo-saved__row {
    display: grid;
    grid-template-columns: 140px minmax(0, 1fr);
    gap: var(--space-4);
    align-items: start;
  }
  .logo-saved__row > .logo-mask {
    --logo-w: 140px;
  }
  .logo-saved__text {
    display: grid;
    gap: var(--space-1);
    min-width: 0;
    font-size: var(--text-sm);
  }
  .logo-saved__text code {
    overflow-wrap: anywhere;
  }
  .logo-tabs {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    border-bottom: 1px solid var(--color-divider);
  }
  .logo-tabs > button {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-height: var(--control-h);
    padding: var(--space-1) var(--space-2);
    border: 0;
    background: none;
    color: var(--color-text-muted);
    font: inherit;
    font-size: var(--text-sm);
    font-weight: var(--weight-strong);
    text-align: center;
    cursor: pointer;
  }
  .logo-tabs > button:hover {
    background: var(--color-control-active);
    color: var(--color-text);
  }
  .logo-tabs > button[aria-selected="true"] {
    color: var(--color-text);
  }
  .logo-tabs > button[aria-selected="true"]::after {
    content: "";
    position: absolute;
    right: 0;
    bottom: -1px;
    left: 0;
    height: 3px;
    background: var(--color-accent);
  }
  .logo-tabs > button:focus-visible {
    outline: var(--focus-w) solid var(--color-focus);
    outline-offset: calc(-1 * var(--focus-w));
  }
  .logo-tabs svg {
    flex: none;
  }
  .logo-pane {
    display: grid;
    gap: var(--space-4);
    align-content: start;
    min-width: 0;
  }
  .logo-pane[hidden] {
    display: none;
  }
  .logo-pane:focus-visible {
    outline: var(--focus-w) solid var(--color-focus);
    outline-offset: 2px;
  }
  .logo-step {
    margin: 0;
    font-size: var(--text-md);
    font-weight: var(--weight-heading);
  }
  /* Find online: a form on one line, then the results with their licence and any warning. */
  .inline-form {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
  }
  .inline-form > .input {
    flex: 1 1 200px;
    min-width: 0;
  }
  .logo-candidates {
    display: grid;
    gap: var(--space-2);
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .logo-candidate {
    display: grid;
    grid-template-columns: 132px minmax(0, 1fr);
    gap: var(--space-2) var(--space-3);
    align-items: center;
    padding: var(--space-2);
    border: 1px solid var(--color-divider);
  }
  .logo-candidate[aria-current="true"] {
    border-color: var(--color-accent);
    box-shadow: inset 4px 0 0 var(--color-accent);
  }
  .logo-candidate__art {
    display: grid;
    place-items: center;
    height: 64px;
    padding: var(--space-2);
    background: var(--color-surface-sunken);
  }
  /* A found file's own picture (another site's image cannot be a mask). */
  .logo-candidate__art > img {
    max-width: 112px;
    max-height: 48px;
    object-fit: contain;
  }
  .logo-candidate__art > .logo-mask {
    width: 112px;
    height: 48px;
    aspect-ratio: auto;
    -webkit-mask-position: center;
    mask-position: center;
  }
  .logo-candidate__text {
    display: grid;
    justify-items: start;
    gap: 2px;
    min-width: 0;
    font-size: var(--text-sm);
  }
  .logo-candidate__text > * {
    max-width: 100%;
  }
  .logo-candidate__text b {
    overflow-wrap: anywhere;
  }
  .logo-candidate .chip {
    height: auto;
    min-height: 0;
    white-space: normal;
  }
  .logo-candidate > .btn {
    grid-column: 1 / -1;
    justify-self: start;
  }
  /* From a cover: pick a cover, draw a box, make it one colour. */
  .cover-pick {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
  }
  .cover-pick label {
    position: relative;
    display: grid;
    gap: var(--space-1);
    width: 76px;
    font-size: var(--text-xs);
    cursor: pointer;
  }
  .cover-pick input {
    position: absolute;
    inset: 0;
    margin: 0;
    opacity: 0;
    cursor: pointer;
  }
  .cover-pick img {
    width: 76px;
    aspect-ratio: 2 / 3;
    border: 3px solid transparent;
    background: var(--color-surface-sunken);
    object-fit: cover;
  }
  .cover-pick input:checked + img {
    border-color: var(--color-accent);
  }
  .cover-pick input:focus-visible + img {
    outline: var(--focus-w) solid var(--color-focus);
    outline-offset: 2px;
  }
  .logo-crop {
    display: grid;
    grid-template-columns: minmax(0, 280px) minmax(0, 1fr);
    gap: var(--space-4);
    align-items: start;
  }
  .logo-crop__canvas {
    display: block;
    width: 100%;
    max-width: 280px;
    height: auto;
    background: var(--color-surface-sunken);
    cursor: crosshair;
    touch-action: none;
  }
  .logo-crop__canvas:focus-visible {
    outline: var(--focus-w) solid var(--color-focus);
    outline-offset: 2px;
  }
  .logo-crop__controls {
    display: grid;
    gap: var(--space-3);
    min-width: 0;
  }
  .logo-crop__readout {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    font-variant-numeric: tabular-nums;
  }
  /* A slider (input type=range), 44px tall. */
  .range {
    width: 100%;
    height: var(--control-h);
    margin: 0;
    accent-color: var(--color-accent);
    cursor: pointer;
  }
  .range:focus-visible {
    outline: var(--focus-w) solid var(--color-focus);
    outline-offset: 2px;
  }
  /* Checks behind a cut-out show what is transparent. */
  .checker {
    padding: var(--space-2);
    border: 1px solid var(--color-divider);
    background-color: #fff;
    background-image:
      linear-gradient(45deg, #d6d9df 25%, transparent 25%, transparent 75%, #d6d9df 75%),
      linear-gradient(45deg, #d6d9df 25%, transparent 25%, transparent 75%, #d6d9df 75%);
    background-position:
      0 0,
      8px 8px;
    background-size: 16px 16px;
  }
  .checker canvas,
  .checker img {
    display: block;
    max-width: 100%;
    height: auto;
  }
  /* The logo editor's eraser and paintbrush draw on the cut-out (UX-48). */
  .checker[data-tool="erase"] canvas,
  .checker[data-tool="paint"] canvas {
    cursor: crosshair;
    touch-action: none;
  }
  /* Upload your own. */
  .logo-drop {
    display: grid;
    justify-items: start;
    gap: var(--space-2);
    min-width: 0;
    padding: var(--space-4);
    border: 2px dashed var(--color-border);
  }
  .logo-drop input {
    width: 100%;
    max-width: 100%;
    font-size: var(--text-sm);
  }
  .logo-drop input::file-selector-button {
    min-height: var(--control-h);
    margin-right: var(--space-3);
    padding: 0 var(--space-4);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    background: var(--color-surface);
    color: var(--color-text);
    font: inherit;
    font-weight: var(--weight-strong);
    cursor: pointer;
  }
  .logo-drop input::file-selector-button:hover {
    background: var(--color-surface-sunken);
  }
  /* The logo on the Light and Dark themes' surfaces, whatever theme is on. */
  .logo-preview {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-2);
  }
  .logo-preview > * {
    display: grid;
    gap: var(--space-2);
    padding: var(--space-4);
    border: 1px solid var(--color-divider);
  }
  .logo-preview__light {
    --color-text: #141416;
    --color-accent: #0b5f5a;
    background: #f7faff;
    color: #141416;
  }
  .logo-preview__dark {
    --color-text: #f3f4f6;
    --color-accent: #5ed3c6;
    background: #1c2738;
    color: #f3f4f6;
  }
  .logo-preview .logo-mask {
    --logo-w: 220px;
    --logo-max-h: 96px;
  }
  .logo-preview small {
    font-size: var(--text-xs);
  }
  @media (max-width: 720px) {
    .series-head {
      grid-template-columns: minmax(0, 1fr);
      padding: var(--space-5) var(--space-4);
    }
    .cover-fan {
      display: none;
    }
    .logo-crop,
    .logo-saved__row,
    .logo-candidate {
      grid-template-columns: minmax(0, 1fr);
    }
    .logo-candidate__art {
      justify-items: start;
    }
    .logo-tabs > button {
      flex-direction: column;
      gap: 2px;
    }
  }

  /* Back link: returns to where you came from, named for it ("Back to My library"). */
  .back-link {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: var(--control-h);
    font-size: var(--text-sm);
    font-weight: var(--weight-strong);
    text-decoration: none;
  }
  .back-link:hover {
    text-decoration: underline;
  }
  /* Breadcrumbs: a path, such as folders. */
  .breadcrumbs ol {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: var(--text-sm);
  }
  .breadcrumbs li + li::before {
    content: "/";
    margin-right: var(--space-1);
    color: var(--color-text-muted);
  }
  .breadcrumbs [aria-current] {
    font-weight: var(--weight-strong);
  }

  /* Shelf: a heading row (title, caption, View all) over one row of covers that scrolls sideways,
     with 44px arrow buttons on computers. */
  .shelf {
    display: grid;
    gap: var(--space-3);
  }
  .shelf__head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-1) var(--space-4);
  }
  .shelf__caption {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }
  .shelf__end {
    display: flex;
    gap: var(--space-2);
    margin-left: auto;
  }
  .shelf__track {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: var(--cover-w, 148px);
    gap: var(--space-4);
    margin: 0;
    padding: 0 0 var(--space-2);
    list-style: none;
    overflow-x: auto;
    scroll-snap-type: x proximity;
  }
  .shelf__track > * {
    scroll-snap-align: start;
  }

  /* Calendar month: a real table, so screen readers read day by day. */
  .calendar {
    width: 100%;
    table-layout: fixed;
    border-collapse: collapse;
  }
  .calendar th {
    padding: var(--space-2);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    font-weight: var(--weight-strong);
    text-align: left;
  }
  .calendar td {
    height: 112px;
    padding: var(--space-2);
    border: 1px solid var(--color-divider);
    vertical-align: top;
    background: var(--color-surface);
  }
  .calendar td[data-outside] {
    background: var(--color-surface-sunken);
    color: var(--color-text-muted);
  }
  .calendar td[aria-current="date"] {
    box-shadow: inset 0 var(--current-bar-w) 0 var(--color-accent);
  }
  .calendar__day {
    font-size: var(--text-sm);
    font-weight: var(--weight-strong);
  }
  .calendar td[aria-current="date"] .calendar__day::after {
    content: " · today";
    font-weight: var(--weight-regular);
  }
  /* A day with releases: small covers, the first title and "N releases", which opens the day's slide-out. */
  .calendar__covers {
    display: flex;
    gap: var(--space-1);
    margin-top: var(--space-1);
  }
  .cover--mini {
    width: 24px;
    flex: none;
  }
  .calendar__titles {
    margin-top: var(--space-1);
  }
  .calendar__open {
    margin-top: var(--space-1);
  }
  .calendar__short {
    display: none;
    text-decoration: none;
  }

  /* Selection bar: "3 selected" with bulk actions; same place and look as the save bar. */
  .selection-bar {
    position: sticky;
    bottom: 0;
    z-index: var(--layer-sticky);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2) var(--space-4);
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-border);
    border-left: var(--status-edge-w) solid var(--color-accent);
    background: var(--color-surface-raised);
    box-shadow: var(--shadow-raised);
  }
  .selection-bar__text {
    flex: 1 1 160px;
    font-weight: var(--weight-strong);
  }

  /* Extra buttons beside Close in a panel header (Previous, Next). */
  .panel-header__actions {
    display: flex;
    gap: var(--space-1);
    margin-left: auto;
  }
  /* A settings section head with an action (Add channel) on the right. */
  .settings-section__head--action {
    display: flex;
    flex-wrap: wrap;
    align-items: end;
    justify-content: space-between;
    gap: var(--space-2) var(--space-4);
  }
  /* A count inside a pressed chip keeps a light frame so it stays readable on the accent. */
  .filter-chip[aria-pressed="true"] .count {
    outline: 1px solid var(--color-accent-text);
  }

  /* Wizard: one question per step, with Continue first, then Back and Skip. */
  .wizard {
    display: grid;
    gap: var(--space-6);
    max-width: 720px;
  }
  .wizard__foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    padding-top: var(--space-4);
    border-top: 1px solid var(--color-divider);
  }
  .wizard__foot .btn--quiet:last-child {
    margin-left: auto;
  }
  .steps__short {
    display: none;
    font-size: var(--text-sm);
    font-weight: var(--weight-strong);
  }

  /* Wizard rules (4 November 2026, docs/WIZARD.md): the progress bar and, for a step in parts, a
     small step bar under it. What happens next and the decision tree are review aids for testing
     (TheMightyTRexUK, 4 November 2026): shown beside the step only while an app's testing setting is on, never
     in the finished product. Without them, the step has the whole width. */
  .wizard-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-8);
    align-items: start;
  }
  .wizard-layout:has(> .wizard-aside:not([hidden])) {
    grid-template-columns: minmax(0, 1fr) minmax(280px, 360px);
  }
  .wizard-aside[hidden] {
    display: none;
  }
  .wizard-layout > .wizard,
  .wizard-layout > .wizard-main {
    max-width: none;
    min-width: 0;
  }
  .wizard-aside {
    display: grid;
    gap: var(--space-4);
    min-width: 0;
    overflow-wrap: anywhere;
  }
  .wizard-aside h2 {
    margin: 0;
    font-size: var(--text-lg);
  }
  .wizard-progress {
    margin-bottom: var(--space-6);
  }
  .wizard-progress .numeric {
    white-space: nowrap;
  }
  /* The step bar for a step in parts: as wide as the progress bar, the parts numbered 1, 2, 3 and
     sharing it equally, with a line between them. Each part is still a step in the progress bar's total. */
  .steps--sub {
    flex-wrap: nowrap;
    gap: var(--space-6);
    width: 100%;
    margin: calc(var(--space-4) * -1) 0 var(--space-6);
  }
  .steps--sub li {
    position: relative;
    flex: 1 1 0;
    min-width: 0;
    padding: var(--space-1) var(--space-2);
    font-size: var(--text-xs);
    hyphens: auto;
  }
  .steps--sub li + li::after {
    content: "";
    position: absolute;
    top: 50%;
    right: calc(100% + 1px);
    width: var(--space-6);
    border-top: 2px solid var(--color-divider);
  }
  .steps--sub li[data-done] + li::after,
  .steps--sub li[data-done] + li[aria-current="step"]::after {
    border-top-color: var(--color-accent);
  }
  .steps--sub li::before {
    width: max(20px, 1.7em);
    height: max(20px, 1.7em);
  }
  .steps[hidden] {
    display: none;
  }

  /* The first question: each job as a card with its stages and number of steps. */
  .wizard-paths {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr));
    gap: var(--space-3);
  }
  .wizard-paths__path {
    display: grid;
    align-content: start;
    gap: var(--space-1);
    padding: var(--space-3);
    border: 1px solid var(--color-border);
    background: var(--color-surface-raised);
  }
  .wizard-paths__path[data-here] {
    border: 2px solid var(--color-accent);
    background: var(--color-accent-soft);
  }
  .wizard-paths__path h3,
  .wizard-paths__path p {
    margin: 0;
  }
  .wizard-paths__path ol {
    display: grid;
    gap: 2px;
    margin: var(--space-1) 0 0;
    padding-left: var(--space-5);
    font-size: var(--text-sm);
  }

  /* Review aid. What happens next: one item for each choice on this step, the current choice first. */
  .wizard-next {
    display: grid;
    gap: var(--space-3);
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .wizard-next li {
    min-width: 0;
    padding: var(--space-3);
    border: 1px solid var(--color-divider);
    overflow-wrap: anywhere;
  }
  .wizard-next li[data-chosen] {
    border-color: var(--color-accent);
    background: var(--color-accent-soft);
  }
  .wizard-next__if {
    display: block;
    font-weight: var(--weight-strong);
  }
  .wizard-next__then {
    display: block;
    margin-top: var(--space-1);
  }
  .wizard-next__tag {
    display: inline-block;
    margin-left: var(--space-2);
    color: var(--color-accent);
    font-size: var(--text-xs);
    font-weight: var(--weight-heading);
  }

  /* Review aid. Decision tree: the chosen job's stages and steps, and the choices that branch from them. */
  .wizard-tree-toggle > summary {
    display: flex;
    align-items: center;
    min-height: var(--control-h);
    cursor: pointer;
    list-style-position: inside;
  }
  .wizard-tree-toggle > summary h2 {
    display: inline;
  }
  .wizard-tree__flow {
    margin: 0 0 var(--space-2);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    font-weight: var(--weight-strong);
  }
  .wizard-tree {
    --mark: max(24px, 1.5rem); /* grows with Text size, so the number stays inside */
  }
  .wizard-tree,
  .wizard-tree ol,
  .wizard-tree__branches {
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .wizard-tree__stage {
    margin: var(--space-4) 0 var(--space-2);
    color: var(--color-text-muted);
    font-size: var(--text-xs);
    font-weight: var(--weight-heading);
  }
  .wizard-tree > li:first-child .wizard-tree__stage {
    margin-top: 0;
  }
  .wizard-tree__step {
    position: relative;
    padding: 0 0 var(--space-3) calc(var(--mark) + var(--space-3));
  }
  /* The line that joins one step to the next. */
  .wizard-tree__step::before {
    content: "";
    position: absolute;
    left: calc(var(--mark) / 2 - 1px);
    top: var(--mark);
    bottom: 0;
    border-left: 2px solid var(--color-divider);
  }
  .wizard-tree__step:last-child::before {
    display: none;
  }
  .wizard-tree__mark {
    position: absolute;
    left: 0;
    top: 0;
    display: grid;
    place-items: center;
    width: var(--mark);
    height: var(--mark);
    border: 2px solid var(--color-border);
    background: var(--color-surface-raised);
    color: var(--color-text-muted);
    font-size: var(--text-xs);
    font-weight: var(--weight-heading);
  }
  .wizard-tree__mark svg {
    width: 60%;
    height: 60%;
  }
  /* A decision is a diamond, so it does not rest on colour. */
  .wizard-tree__step[data-kind="decision"] .wizard-tree__mark {
    transform: rotate(45deg) scale(0.8);
  }
  .wizard-tree__step[data-kind="decision"] .wizard-tree__mark > * {
    transform: rotate(-45deg);
  }
  .wizard-tree__step[data-state="done"] .wizard-tree__mark {
    border-color: var(--color-accent);
    background: var(--color-accent);
    color: var(--color-accent-text);
  }
  .wizard-tree__step[data-state="current"] .wizard-tree__mark {
    border-color: var(--color-accent);
    outline: var(--focus-w) solid var(--color-accent-soft);
    color: var(--color-accent);
  }
  .wizard-tree__step[data-state="left-out"] {
    color: var(--color-text-muted);
  }
  .wizard-tree__step[data-state="left-out"] .wizard-tree__mark {
    border-style: dashed;
  }
  .wizard-tree__title {
    display: block;
    padding-top: 2px;
    font-weight: var(--weight-strong);
    line-height: var(--leading-tight);
  }
  .wizard-tree__step[data-state="current"] .wizard-tree__title {
    color: var(--color-accent);
  }
  .wizard-tree__step[data-state="left-out"] .wizard-tree__title {
    font-weight: var(--weight-regular);
    text-decoration: line-through;
  }
  .wizard-tree__meta {
    display: block;
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }
  .wizard-tree__branches {
    display: grid;
    gap: var(--space-1);
    margin-top: var(--space-2);
  }
  .wizard-tree__branch {
    border-left: 2px solid var(--color-divider);
    font-size: var(--text-sm);
  }
  .wizard-tree__branch[data-chosen] {
    border-left-color: var(--color-accent);
    background: var(--color-accent-soft);
  }
  .wizard-tree__branch summary {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--space-2);
    align-items: start;
    min-height: 32px;
    padding: var(--space-1) var(--space-2);
    cursor: pointer;
    list-style: none;
  }
  .wizard-tree__branch summary::-webkit-details-marker {
    display: none;
  }
  .wizard-tree__branch summary:focus-visible {
    outline: var(--focus-w) solid var(--color-focus);
    outline-offset: -2px;
  }
  .wizard-tree__branch summary svg {
    width: 16px;
    height: 16px;
    margin-top: 2px;
  }
  .wizard-tree__branch:not([data-chosen]) details[open] summary svg {
    transform: rotate(90deg);
  }
  .wizard-tree__branch details p {
    margin: 0;
    padding: 0 var(--space-2) var(--space-2) calc(var(--space-4) + 16px);
    color: var(--color-text-muted);
  }

  /* "What is…" links at the end of the part of a step they explain; each opens a dialog. */
  .help-links {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-4);
    margin: 0;
  }
  .help-link {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    min-height: var(--control-h);
    padding: 0;
    border: 0;
    background: none;
    color: var(--color-accent);
    font: inherit;
    text-align: left;
    text-decoration: underline;
    text-underline-offset: 3px;
    cursor: pointer;
  }
  .help-link:hover {
    text-decoration-thickness: 2px;
  }
  .help-link svg {
    flex: none;
  }

  /* A server address: the link, opening in a new tab, then its copy button. */
  .address {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0 var(--space-1);
    max-width: 100%;
    vertical-align: middle;
  }
  .address a {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    min-height: var(--control-h);
    font-weight: var(--weight-strong);
    overflow-wrap: anywhere;
  }
  .address a svg {
    flex: none;
  }

  /* The end of a wizard: what was set and what was skipped. */
  .wizard-summary {
    display: grid;
    gap: var(--space-2);
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .wizard-summary li {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--space-2) var(--space-3);
    align-items: start;
    padding: var(--space-3);
    border: 1px solid var(--color-divider);
    background: var(--color-surface-raised);
  }
  .wizard-summary li > svg {
    width: 20px;
    height: 20px;
    margin-top: 2px;
  }
  .wizard-summary li[data-tone="warning"] {
    border-left: var(--status-edge-w) solid var(--status-warning-fg);
  }
  .wizard-summary .small {
    display: block;
  }

  /* Footer: on every page, under main. Left: the app's icon, name and version, then Help (when the
     app has a help centre), About and Updates. Right: the maker mark, the T-rex and TheMightyTRexUK,
     linking to the GitHub profile. On phones the maker mark goes under the app's links. */
  .site-footer {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2) var(--space-6);
    padding: var(--space-3) var(--space-8) var(--space-4);
    border-top: 1px solid var(--color-divider);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }
  .site-footer__app {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0 var(--space-4);
  }
  .site-footer__brand {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    color: var(--color-text);
    font-weight: var(--weight-strong);
  }
  .site-footer__brand svg,
  .site-footer__maker svg.maker-mark {
    flex: none;
    width: 24px;
    height: 24px;
  }
  .site-footer__links {
    display: flex;
    flex-wrap: wrap;
    margin: 0 calc(var(--space-2) * -1); /* the links' padding, so their text lines up */
    padding: 0;
    list-style: none;
  }
  .site-footer__links a,
  .site-footer__maker {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 44px;
    padding-inline: var(--space-2);
    color: var(--color-text-muted);
  }
  .site-footer__links a:hover,
  .site-footer__maker:hover {
    color: var(--color-text);
  }
  .site-footer__maker {
    margin-left: auto;
    text-decoration: none;
    font-weight: var(--weight-strong);
  }
  .site-footer__maker .i {
    width: var(--icon-sm, 16px);
    height: var(--icon-sm, 16px);
  }
  @media (max-width: 720px) {
    .site-footer {
      padding-inline: var(--space-4);
    }
    .site-footer__maker {
      margin-left: calc(var(--space-2) * -1);
    }
  }

  /* Prose: long text such as help topics and release notes shown in the app. Headings keep their order,
     lists and steps get room, and an on-screen label in a step is bold. Callouts are .notice. */
  .prose {
    max-width: 72ch;
    display: grid;
    gap: var(--space-4);
    line-height: 1.6;
  }
  .prose > * {
    margin: 0;
    min-width: 0;      /* a long line (code, a table) scrolls inside itself instead of widening the text */
  }
  .prose pre {
    overflow-x: auto;
  }
  .prose h1 {
    font-size: var(--text-2xl);
  }
  .prose h2 {
    margin-top: var(--space-4);
    font-size: var(--text-xl);
  }
  .prose h3 {
    margin-top: var(--space-2);
    font-size: var(--text-lg);
  }
  .prose__lead {
    font-size: var(--text-lg);
  }
  .prose__kind {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }
  .prose ul,
  .prose ol {
    display: grid;
    gap: var(--space-2);
    padding-left: var(--space-6);
  }
  .prose li > ul,
  .prose li > ol {
    margin-top: var(--space-2);
  }
  .prose ol > li::marker {
    font-weight: var(--weight-strong);
  }
  .prose a {
    text-decoration: underline;
  }
  .prose figure {
    display: grid;
    gap: var(--space-2);
  }
  .prose figure img {
    max-width: 100%;
    height: auto;
    border: 1px solid var(--color-divider);
  }
  .prose figcaption {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }
  .prose table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--text-sm);
  }
  .prose th,
  .prose td {
    padding: var(--space-2) var(--space-3);
    border-top: 1px solid var(--color-divider);
    text-align: left;
    vertical-align: top;
  }
  .prose th {
    background: var(--color-surface-sunken);
  }

  /* 7. Phones and narrow windows ---------------------------------------------------------------- */
  @media (max-width: 1100px) {
    .wizard-layout:has(> .wizard-aside:not([hidden])) {
      grid-template-columns: minmax(0, 1fr);
    }
    .calendar__long {
      display: none;
    }
    .calendar__short {
      display: inline;
    }
    .page {
      padding: var(--space-6) var(--space-6) var(--space-12);
    }
  }
  @media (max-width: 720px) {
    .rail {
      display: none;
    }
    .with-rail {
      padding-left: 0;
    }
    .site-header .phone-menu-button {
      display: inline-flex;
    }
    .site-header__bar {
      height: 60px;
      padding-inline: var(--space-4);
      gap: var(--space-3);
    }
    .site-header__bar > .menu-tabs,
    .site-header__search {
      display: none;
    }
    .site-header .site-header__search-btn {
      display: inline-flex;
    }
    /* On phones the header shows only the logo on the left, no names (TheMightyTRexUK, 4 November 2026). The app's
       name and the server name stay for screen readers. */
    .site-header__brand > span,
    .site-header__server {
      position: absolute;
      width: 1px;
      height: 1px;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
      padding: 0;
      border: 0;
    }
    .site-header__brand {
      display: inline-flex;
    }
    .site-header__actions {
      margin-left: auto;
    }
    .site-header__actions .label {
      position: absolute;
      width: 1px;
      height: 1px;
      overflow: hidden;
      clip-path: inset(50%);
    }
    .page {
      padding: var(--space-4) var(--space-4) var(--space-12);
      gap: var(--space-4);
    }
    .page-head .actions {
      width: 100%;
      margin-left: 0;
    }
    .row {
      grid-template-columns: auto minmax(0, 1fr);
    }
    .row__side {
      grid-column: 1 / -1;
      justify-content: start;
    }
    .setting {
      grid-template-columns: minmax(0, 1fr);
    }
    .list-detail {
      grid-template-columns: minmax(0, 1fr);
    }
    .list-detail__list {
      position: static;
      max-height: none;
    }
    .item-header {
      grid-template-columns: 96px minmax(0, 1fr);
      gap: var(--space-4);
    }
    .item-header__art {
      width: 96px;
    }
    /* On phones the step bar is just its numbers, joined by lines; a done part's number becomes a
       tick. The names stay for screen readers (font-size 0 hides them from sight only). */
    .steps--sub {
      --mark: max(28px, calc(1.7 * var(--text-sm)));
      gap: 0;
    }
    .steps--sub li {
      justify-content: center;
      padding: 0;
      border: 0;
      font-size: 0;
    }
    .steps--sub li::before {
      width: var(--mark);
      height: var(--mark);
      border-width: 2px;
      font-size: var(--text-sm);
    }
    .steps--sub li[aria-current="step"]::before {
      border-color: var(--color-accent);
      outline: var(--focus-w) solid var(--color-accent-soft);
      color: var(--color-accent);
    }
    .steps--sub li + li::after {
      right: calc(50% + var(--mark) / 2);
      width: calc(100% - var(--mark));
    }
    /* Wizard steps become "Step 3 of 7" on phones. */
    .steps--collapse li {
      display: none;
    }
    .steps--collapse + .steps__short,
    .steps__short:only-child {
      display: block;
    }
    /* On phones the month becomes an agenda: only the days with releases, each with its full date. */
    .calendar thead {
      display: none;
    }
    .calendar,
    .calendar tbody,
    .calendar tr,
    .calendar td {
      display: block;
      width: 100%;
    }
    .calendar td {
      height: auto;
      border-width: 1px 0 0;
    }
    .calendar td[data-empty] {
      display: none;
    }
    .calendar td::before {
      content: attr(data-label);
      font-weight: var(--weight-strong);
    }
    .calendar td .calendar__day {
      display: none;
    }
    .setting__control {
      justify-content: start;
    }
    .filter-chips {
      flex-wrap: nowrap;
      overflow-x: auto;
      padding-bottom: var(--space-1);
    }
    .filter-chip {
      flex: none;
    }
    .toolbar__row > .filter-chips {
      flex: 1 1 100%;
    }
    .toolbar__search {
      flex-basis: 100%;
      max-width: none;
    }
    .toolbar__end {
      margin-left: 0;
    }
    .side-panel,
    .side-panel--wide {
      width: 100vw;
      border-left: 0;
    }
    .dialog {
      width: calc(100vw - 16px);
      max-height: calc(100dvh - 16px);
    }
    .dialog__body {
      padding: var(--space-4);
    }
    .dialog__foot {
      padding: 0 var(--space-4) var(--space-4);
    }
    .dialog__foot .btn {
      flex: 1 1 auto;
    }
    .panel {
      padding: var(--space-4);
    }
    .facts {
      grid-template-columns: minmax(0, 1fr);
      gap: 0 0;
    }
    .facts dd {
      margin-bottom: var(--space-2);
    }
    .table-wrap[data-stack] .table thead {
      display: none;
    }
    .table-wrap[data-stack] .table tr {
      display: grid;
      padding: var(--space-2) 0;
      border-top: 1px solid var(--color-divider);
    }
    .table-wrap[data-stack] .table td {
      border: 0;
      padding: 2px var(--space-3);
    }
    .table-wrap[data-stack] .table td::before {
      content: attr(data-label) ": ";
      color: var(--color-text-muted);
    }
  }

  /* 8. Motion ----------------------------------------------------------------------------------- */
  @keyframes slide-in-right {
    from {
      transform: translateX(100%);
    }
  }
  @keyframes slide-in-left {
    from {
      transform: translateX(-100%);
    }
  }
  @keyframes fade-in {
    from {
      opacity: 0;
    }
  }
  @keyframes shimmer {
    from {
      background-position: 200% 0;
    }
    to {
      background-position: -200% 0;
    }
  }
  @keyframes loading {
    from {
      transform: translateX(-100%);
    }
    to {
      transform: translateX(300%);
    }
  }
  @media (prefers-reduced-motion: reduce) {
    .skeleton,
    .loading-bar[data-active]::before {
      animation: none;
    }
    .loading-bar[data-active]::before {
      width: 100%;
      opacity: 0.6;
    }
  }
  :root.pref-motion .skeleton,
  :root.pref-motion .loading-bar[data-active]::before,
  :root.pref-motion .side-panel,
  :root.pref-motion .phone-menu {
    animation: none;
  }
  /* UX notes (ux-notes.js): a tester's Note an issue button in the corner, a banner while a part of the page
     is chosen, an outline round that part, and the note and list dialogs. Only while testing is turned on. */
  .ux-dock {
    position: fixed;
    z-index: var(--layer-panel);
    right: max(var(--space-4), env(safe-area-inset-right, 0px));
    bottom: max(var(--space-4), env(safe-area-inset-bottom, 0px));
    display: flex;
    gap: var(--space-2);
    box-shadow: var(--shadow-raised);
  }
  .ux-dock .btn {
    gap: var(--space-2);
  }
  .ux-dock__count {
    font-variant-numeric: tabular-nums;
  }
  .ux-banner {
    position: fixed;
    z-index: var(--layer-toast);
    bottom: max(var(--space-4), env(safe-area-inset-bottom, 0px)); /* clear of the header, which may be the part */
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    width: max-content;
    max-width: calc(100vw - 32px);
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-border);
    background: var(--color-surface-raised);
    color: var(--color-text);
    box-shadow: var(--shadow-raised);
  }
  .ux-banner p {
    margin: 0;
    font-size: var(--text-sm);
  }
  .ux-banner__actions {
    display: flex;
    gap: var(--space-2);
  }
  .ux-outline {
    position: fixed;
    z-index: var(--layer-dialog);
    pointer-events: none;
    outline: var(--focus-w) solid var(--color-focus);
    outline-offset: 1px;
    background: color-mix(in srgb, var(--color-focus) 10%, transparent);
  }
  .is-ux-picking body,
  .is-ux-picking body * {
    cursor: crosshair;
  }
  .is-ux-picking .ux-banner,
  .is-ux-picking .ux-banner * {
    cursor: auto;
  }
  .ux-notes--wide {
    width: min(calc(var(--dialog-w) * 1.4), calc(100vw - 32px));
  }
  .ux-notes__form {
    display: grid;
    gap: var(--space-4);
  }
  .ux-notes__where {
    margin: 0;
  }
  .ux-notes__selector {
    font-size: var(--text-xs);
    overflow-wrap: anywhere;
    color: var(--color-text-muted);
  }
  .ux-notes__list {
    display: grid;
    gap: var(--space-3);
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .ux-notes__item {
    display: grid;
    gap: var(--space-1);
    padding: var(--space-3);
    border: 1px solid var(--color-border);
  }
  .ux-notes__item.is-fixed .ux-notes__what {
    text-decoration: line-through;
  }
  .ux-notes__meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-3);
    align-items: baseline;
  }
  .ux-notes__meta span,
  .ux-notes__part {
    margin: 0;
    font-size: var(--text-sm);
    overflow-wrap: anywhere;
  }
  .ux-notes__what {
    margin: 0;
    white-space: pre-wrap;
  }
  .ux-notes__actions {
    display: flex;
    gap: var(--space-2);
    margin-top: var(--space-1);
  }
  @media print {
    .ux-dock {
      display: none;
    }
  }
  /* Sortable list (UX-51): rows in an order the person sets, by dragging the handle or with Move up and Move down.
     A marker row (such as "Stop upgrading here") is a dashed line with its words, moved the same way. */
  .sortable {
    display: grid;
    gap: var(--space-1);
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .sortable__row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto auto;
    align-items: center;
    gap: var(--space-2);
    min-height: var(--control-h);
    padding: var(--space-1) var(--space-2);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    background: var(--color-surface);
  }
  .sortable__handle {
    display: inline-grid;
    place-items: center;
    inline-size: 32px;
    block-size: 32px;
    color: var(--color-text-muted);
    cursor: grab;
    touch-action: none;
  }
  .sortable__label {
    min-width: 0;
    overflow-wrap: anywhere;
  }
  .sortable__meta {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }
  .sortable__moves {
    display: inline-flex;
    gap: var(--space-1);
  }
  .sortable__row.is-dragging {
    border-color: var(--color-accent);
    box-shadow: var(--shadow-raised);
    cursor: grabbing;
  }
  .sortable__row--marker {
    border: 0;
    border-block-start: 2px dashed var(--color-accent);
    border-radius: 0;
    background: transparent;
    color: var(--color-accent);
    font-size: var(--text-sm);
    font-weight: var(--weight-strong);
  }
  .sortable__row--off .sortable__label {
    color: var(--color-text-muted);
  }
  /* On a phone the extra detail goes under the row's words, and Move up and Move down stay on the row. */
  @media (max-width: 480px) {
    .sortable__row { grid-template-columns: auto minmax(0, 1fr) auto; }
    .sortable__meta { grid-column: 2; grid-row: 2; }
    .sortable__moves { grid-column: 3; grid-row: 1; }
  }
  @media (forced-colors: active) {
    .sortable__row--marker { border-block-start-color: Highlight; color: Highlight; }
    .sortable__row.is-dragging { outline: 2px solid Highlight; }
  }
}

/* A plug-in style's two colours in the Display panel (DASH-284): decoration only, the name says it. */
.style-swatch { display: inline-block; inline-size: 1em; block-size: 1em; margin-inline-end: var(--space-2); vertical-align: -0.1em;
  border: 1px solid var(--color-border); background: linear-gradient(135deg, var(--sw-a) 50%, var(--sw-b) 50%); }

/* The upgrade guide in a slide-out (Choose now): the frame fills the panel, and the page inside shows only the guide. */
.panel-frame { display: block; inline-size: 100%; block-size: 100%; min-block-size: 70vh; border: 0; background: var(--color-bg); }
.side-panel__body:has(> .panel-frame) { padding: 0; display: flex; }
:root.in-panel :is(.rail, .site-header, .site-footer, .skip-link, .menu-tabs, [data-sign-out]) { display: none !important; }
:root.in-panel body.with-rail { padding-inline-start: 0; }
:root.in-panel .page-head h1 { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); }

/* Send feedback (Snaglist) sits in the bottom-right corner of every page (DASH-288). It must never cover a control:
   it steps aside while a slide-out or dialog is open, whose own buttons sit in that corner, and pages keep room at the
   bottom so their last controls can scroll clear of it. Snaglist itself is copied unchanged; only its host is styled. */
body:has(.side-panel-layer:not([hidden]), dialog[open]) snag-list { display: none; }
:root:has(snag-list) #main { padding-block-end: 72px; }

/* Send feedback (Snaglist) sits in the bottom left corner: the side menu's end keeps room above it. */
:root.has-feedback .rail__end:not(:has(.rail__feedback)) { padding-block-end: 64px; }

/* The set-up wizard's step marks (DASH-289): past, now and to come; the text above says the same in words. */
.wizard-steps { display: flex; gap: var(--space-1); }
.wizard-steps span { flex: 1; block-size: 6px; background: var(--color-divider); border-radius: var(--radius); }
.wizard-steps .is-past { background: var(--color-text-muted); }
.wizard-steps .is-now { background: var(--color-accent); }
@media (forced-colors: active) { .wizard-steps span { border: 1px solid CanvasText; } .wizard-steps .is-now { background: Highlight; } }

/* Still to set up (DASH-289, owner, 10 November 2026): a Starboard step shows its own page in the slide-out, under
   the step's title, so the set-up is done there. The page fills what is left of the slide-out and scrolls itself. */
.wizard-step { display: flex; flex-direction: column; gap: var(--space-4); block-size: 100%; }
.wizard-step--framed { gap: 0; }
.wizard-step--framed .wizard-step__head { padding: var(--space-4); border-block-end: 1px solid var(--color-divider); }
.side-panel__body:has(> .wizard-step--framed) { padding: 0; display: flex; }
.side-panel__body > .wizard-step--framed { flex: 1; min-block-size: 0; }
.wizard-frame { flex: 1; min-block-size: 50vh; }

/* The folder picker (DASH-240): the field with Browse beside it, what Starboard found under it, and the slide-out list. */
.folder-field { display: flex; gap: var(--space-2); align-items: stretch; }
.folder-field .input { flex: 1; min-inline-size: 0; }
.folder-check p { margin: var(--space-1) 0 0; }
.folder-check[data-tone="danger"] { color: var(--status-danger-fg); }
.folder-crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1); }
.folder-list { list-style: none; margin: 0; padding: 0; border: 1px solid var(--color-border); max-block-size: 60vh; overflow: auto; }
.folder-list [role="option"] { display: flex; gap: var(--space-2); align-items: center; min-block-size: var(--control-h); padding-inline: var(--space-3); cursor: pointer; overflow-wrap: anywhere; }
.folder-list [role="option"][aria-selected="true"] { background: var(--color-control-active); outline: 2px solid var(--color-focus); outline-offset: -2px; }
.folder-list [role="option"]:hover { background: var(--color-surface-sunken); }
@media (max-width: 560px) { .folder-field { flex-wrap: wrap; } .folder-field .btn { inline-size: 100%; } }
