/* pages-full.css - everything above the shared base: the webfonts and the
   app's whole component and view vocabulary. Loaded by the app shell
   (index.html, and by gs-upgrade.js at page boot) and linked by every
   pre-rendered page for the classes the two surfaces share (.chip, .card,
   .markdown, …), so a generated page renders the app's own treatment by
   construction. The design tokens, theme gates, reset and body/anchor base
   live in pages-core.css — this sheet only consumes those vars and declares
   none of its own. */

  /* Webfonts (SIL OFL 1.1), shipped in the shell under fonts/. The ONLY
     url() consumers in either sheet, and the reason they live here: this file
     is always fetched from the prefix root, while pages-core.css is inlined
     into pages at arbitrary depths where a relative URL would break. EB
     Garamond is one variable file; IBM Plex Mono is four static weights, and
     a browser only downloads the weights a page actually renders. */
  @font-face {
    font-family: 'EB Garamond';
    font-style: normal;
    font-weight: 400 800;
    font-display: swap;
    src: url('fonts/eb-garamond.woff2') format('woff2');
  }
  @font-face {
    font-family: 'IBM Plex Mono';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('fonts/ibm-plex-mono.woff2') format('woff2');
  }
  @font-face {
    font-family: 'IBM Plex Mono';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('fonts/ibm-plex-mono-medium.woff2') format('woff2');
  }
  @font-face {
    font-family: 'IBM Plex Mono';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('fonts/ibm-plex-mono-semibold.woff2') format('woff2');
  }
  @font-face {
    font-family: 'IBM Plex Mono';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('fonts/ibm-plex-mono-bold.woff2') format('woff2');
  }

  /* The tokens, theme gates, global reset and body/anchor base all live in
     pages-core.css (shared with the pre-rendered pages, which inline it).
     Only the app shell's flex frame is body-level styling of this sheet's
     own. */
  body { min-height: 100vh; display: flex; flex-direction: column; }
  /* One shared keyboard-focus ring for native controls. The component :focus
     rules keep their border-color shifts but no longer suppress the outline.
     (Browsers treat a focused text input as :focus-visible, so inputs show the
     ring on click-focus too — accepted.) */
  :is(button, input):focus-visible { outline: 2px solid color-mix(in srgb, var(--link) 60%, transparent); outline-offset: 1px; }
  /* Button system: three bases — bordered control, pill, and bare text — each
     stated once; a component's own later rule carries only its deviations
     (layout, a link color, an intentional size). */
  .diff-btn, .version-diff-btn, .gran-btn, .pref-btn, .share-link, .fs-btn {
    background: var(--btn); border: 1px solid var(--line); border-radius: var(--r-ctl);
    padding: var(--pad-ctl); cursor: pointer; font-family: var(--mono); letter-spacing: var(--mono-ls);
    font-size: var(--fs-ui); color: var(--muted); line-height: 1.2;
  }
  .diff-btn:hover, .version-diff-btn:hover, .gran-btn:hover, .pref-btn:hover,
  .share-link:hover, .fs-btn:hover { color: var(--text); background: var(--chip); }
  .pref-btn:hover, .share-link:hover { border-color: var(--link); }
  /* .facet-chip rides the pill base for the shared recipe (border, radius,
     cursor, mono voice) and keeps only its deviations at its own rule below
     (flex layout for the count, chip fill, tighter padding, text color). */
  .filter-chip, .load-more, .board-lane-jump, .facet-chip {
    background: var(--btn); border: 1px solid var(--line); border-radius: var(--r-pill);
    padding: 0.15rem var(--sp-3); cursor: pointer; font-family: var(--mono); letter-spacing: var(--mono-ls);
    font-size: var(--fs-ui); color: var(--muted); line-height: 1.2;
  }
  .filter-chip:hover, .load-more:hover, .board-lane-jump:hover { color: var(--text); background: var(--chip); }
  .load-more:hover, .board-lane-jump:hover { border-color: var(--link); }
  .view-toggle, .board-col-toggle, .board-col-hide, .body-clamp-toggle,
  .board-more, .facet-more {
    background: transparent; border: none; cursor: pointer;
    font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-ui); color: var(--muted);
  }
  .view-toggle:hover, .board-col-toggle:hover, .board-col-hide:hover,
  .body-clamp-toggle:hover { color: var(--text); }
  /* One input recipe (in-page search, tree filter, contributor filter): mono at
     the UI scale on the panel surface, one shared link-tinted focus shift. */
  .search-input, .tree-search, .contrib-filter {
    border: 1px solid var(--line); border-radius: var(--r-ctl); background: var(--panel);
    color: var(--text); font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-ui);
  }
  .search-input::placeholder, .tree-search::placeholder, .contrib-filter::placeholder { color: var(--muted); }
  .search-input:focus, .tree-search:focus, .contrib-filter:focus { border-color: color-mix(in srgb, var(--link) 50%, var(--line)); }
  /* Callout base: one bordered, rounded message box. .err is the danger
     variant; .notice is neutral-informational (muted) — a notice that really
     warns takes .notice-warn explicitly. */
  .err, .notice { border: 1px solid var(--line); border-radius: var(--r-panel); }
  /* Two-panel shell echoing the TUI nav + content split. No header bar: the
     theme control and the brand credit both live in the sidebar. */
  .shell {
    flex: 1; width: 100%; max-width: var(--shell-max); margin: 0 auto;
    display: grid; grid-template-columns: var(--nav-w) 1fr; gap: var(--nav-gap);
    padding: var(--shell-pad) var(--shell-pad) var(--sp-6);
  }
  /* Sidebar column: sticky to the viewport and independently scrollable while the
     footer is pinned to the viewport bottom (position:fixed, below). The nav's own
     bottom padding reserves the footer's height so the last links scroll fully
     into view above it, never hidden behind it. 100dvh (with a 100vh fallback)
     tracks mobile browser chrome. */
  .nav {
    min-width: 0; display: flex; flex-direction: column;
    position: sticky; top: var(--shell-pad); align-self: start;
    height: calc(100vh - 2.5rem); height: calc(100dvh - 2.5rem);
    overflow-y: auto; padding-bottom: calc(var(--sp-6) + var(--sp-2));
  }
  /* The sidebar header row (repo title, theme toggle, collapse control) is one
     line; the row and the title itself are shared vocabulary in pages-core.css,
     and only the app's own controls sit here. */
  .theme-icon, .nav-collapse {
    flex: 0 0 auto; background: transparent; border: none; cursor: pointer;
    color: var(--muted); padding: 0; line-height: 1; display: inline-flex;
    align-items: center; justify-content: center;
    width: 18px; height: 18px;
  }
  .theme-icon:hover, .nav-collapse:hover { color: var(--text); }
  .theme-icon svg { width: 13px; height: 13px; display: block; }
  .nav-collapse { font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-md); }
  .nav-handle {
    display: none; position: fixed; top: var(--sp-3); left: var(--sp-3); z-index: 6;
    background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-ctl);
    color: var(--muted); font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-md);
    padding: var(--sp-1) var(--sp-3); cursor: pointer;
  }
  .nav-handle:hover { color: var(--text); background: var(--chip); }
  body.nav-collapsed .nav { display: none; }
  body.nav-collapsed .shell { grid-template-columns: 1fr; }
  body.nav-collapsed .nav-handle { display: block; }
  /* Layout width: fixed (max-width shell, the reading default) or full width
     (code/diff-friendly), toggled in the sidebar header and persisted. */
  body.wide .shell { max-width: none; }
  /* The nav list itself — groups, section labels, item rows, hover and active
     states, item glyphs — is shared vocabulary in pages-core.css, so the
     generated pages' sidebar IS this sidebar. Only the app's own additions
     (the code search affordance, the memo disabled state) sit here. */
  /* Code nav search magnifier: pinned to the item's right edge, muted like the
     glyphs, brightening on hover. Clicking navigates to Code (if elsewhere) and
     focuses the tree search input — no second search box. */
  .nav-search {
    flex: 0 0 auto; margin-left: auto; display: inline-flex; align-items: center;
    justify-content: center; width: 1rem; height: 1rem; color: var(--muted); cursor: pointer;
  }
  #nav a:hover .nav-search, .nav-search:hover { color: var(--text); }
  .nav-search .gs-icon { width: 0.85rem; height: 0.85rem; color: inherit; }
  #nav .nav-disabled { color: var(--muted); opacity: 0.4; cursor: default; }
  #nav .nav-disabled::after {
    content: "M1"; font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-dense);
    margin-left: var(--sp-2); opacity: 0.8;
  }
  .content { min-width: 0; }
  /* Panel surface: the one bordered, rounded, panel-filled box every list-like
     component shares; a component's own rule below carries only its deviations
     (overflow mode, padding, margins). */
  .tree-list, .blob, .blob-img, .diff-file, .asset-list, .contrib-list, .version-node {
    border: 1px solid var(--line); border-radius: var(--r-panel);
    overflow: hidden; background: var(--panel);
  }
  /* Cards / items. */
  .card {
    background: var(--chip); border: 1px solid var(--line); border-radius: var(--r-panel);
    padding: var(--pad-panel); margin: 0 0 var(--sp-3);
  }
  /* Whole-card navigation: the card links to its item detail, so it takes a
     pointer cursor and a tree-row-like hover (chip tint + link-tinted border).
     Inner anchors and text selection keep their own behavior in JS. */
  .card.clickable { cursor: pointer; transition: border-color 0.12s ease, background 0.12s ease; }
  .card.clickable:hover {
    border-color: color-mix(in srgb, var(--link) 45%, var(--line));
    background: var(--chip-hover);
  }
  /* Subject scale: one semantic element, weight 600 outside a card, sized by context —
     card = mono at --fs-md, detail = --fs-h1 (it is the page title), diff-head =
     --fs-h4, board-card = --fs-ui. The size tiers live on the context rules
     (.detail/.card here, diff-head and board-card at their own rules below);
     cards nest inside .detail views, so the card tier is stated after the
     detail tier to win the card size back. */
  .subject { font-weight: 600; }
  /* Card head: glyph + state chip + subject + trailing chips on one baseline row,
     with a small gap so chips never touch (and the chip row aligns to the subject
     column). The leading glyph drops its own inline margin — the flex gap spaces
     it — so subject text and any wrapped chips share one left edge. */
  .card-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2); }
  .card-head .subject { flex: 1 1 0; min-width: 0; }
  .card-head .type-glyph { margin-right: 0; font-size: var(--fs-md); width: calc(var(--sp-5) + var(--sp-3) - var(--sp-2)); }
  /* A detail head is a card head at page scale. */
  .card-head > h1.subject { font-size: var(--fs-h1); line-height: 1; margin: var(--sp-2) 0 var(--sp-1); }
  .card .body { white-space: pre-wrap; word-break: break-word; margin-top: var(--sp-2); }
  .card .body.dim { color: var(--muted); }
  /* A card body holding RENDERED markdown lays itself out: its blocks carry
     their own spacing, and hard breaks are <br> elements rather than newlines
     the wrapper would have to preserve. */
  .card .body.body-md { white-space: normal; }
  /* A card is a row in a list, not a page: an image inside one is a badge or a
     thumbnail, and is bounded so a single screenshot cannot own the timeline. */
  .card .body-md img { max-height: 12rem; width: auto; }
  /* List-card body clamp: long messages cap at ~10 rendered lines (--lh-body
     em-steps, matching the base font shorthand) with a theme-independent
     mask fade over the last ~2 lines; fixed-em gradient stops keep bodies
     shorter than the cap fully opaque. JS removes the class when the text fits
     and attaches the Show more toggle only when it overflows. */
  .body-clamp {
    max-height: calc(10 * var(--lh-body) * 1em); overflow: hidden;
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(8 * var(--lh-body) * 1em), transparent calc(10 * var(--lh-body) * 1em));
    mask-image: linear-gradient(to bottom, #000 calc(8 * var(--lh-body) * 1em), transparent calc(10 * var(--lh-body) * 1em));
  }
  .body-clamp-toggle { display: block; margin-top: var(--sp-1); padding: 0.15rem 0; }
  .meta { color: var(--muted); font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-ui); display: inline-flex; gap: var(--sp-2); flex-wrap: wrap; align-items: center; margin-left: calc(var(--sp-5) + var(--sp-3)); }
  /* The indent above aligns a meta row under a card head's subject, past the
     glyph column. A card with no head (post, comment, memo) leads the meta row
     with the glyph itself, which then has to start AT the glyph column like
     every other card — same specificity ladder as the rule it overrides. */
  #gs-page .card .meta.meta-lead, .meta.meta-lead { margin-left: 0; }
  /* Under a head the row is a block, so it sits one step below the subject instead of on a body-size line of its own. */
  .card-head + .meta { display: flex; margin-top: var(--sp-1); }
  .hash { font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-ui); color: var(--muted); }
  /* Type glyphs carry one consistent semantic scheme across every view (timeline,
     search, lists, board, milestones, detail): a state-bearing glyph (issue,
     pull-request) is colored by state (open/closed/merged), and every neutral TYPE
     glyph (release, post, milestone, sprint, memo, …) shares the single muted
     color. This mirrors the TUI, which dims card glyphs and conveys state through
     the state chip. */
  .type-glyph { display: inline-block; width: 1.1em; margin-right: 0.3em; text-align: center; line-height: 1; vertical-align: middle; font-family: var(--mono); letter-spacing: var(--mono-ls); color: var(--muted); }
  .type-glyph.tg-open { color: var(--open); }
  .type-glyph.tg-closed { color: var(--closed); }
  .type-glyph.tg-merged { color: var(--merged); }
  .chip {
    display: inline-block; background: var(--code-bg); border-radius: var(--r-pill);
    padding: 0.05rem var(--sp-3); font-size: var(--fs-ui); color: var(--muted);
    font-family: var(--mono); letter-spacing: var(--mono-ls); font-weight: 500;
  }
  .chip.state { color: #fff; }
  .chip.open { background: var(--open); }
  .chip.closed { background: var(--closed); }
  .chip.merged { background: var(--merged); }
  .chip.pre { background: var(--warn); }
  /* Milestone/sprint lifecycle state chips. Every state carries a solid fill so
     the white .chip.state text stays legible (an unmapped state would fall back
     to the translucent --chip background and vanish). Colors are chosen for
     WCAG-AA contrast with white in both themes: active reuses the link blue,
     completed the done green, planned a solid neutral grey, canceled/unknown a
     muted slate. */
  .chip.active { background: #1f6feb; }
  .chip.completed { background: var(--open); }
  .chip.planned { background: #6b7280; }
  .chip.canceled, .chip.unknown { background: #6e7681; }
  .empty, .loading { color: var(--muted); padding: var(--sp-6) 0; text-align: center; }
  /* The one loading icon, from font-size and currentColor; pages-core.css keeps its own boot copy. */
  .spinner { display: inline-block; width: 1em; height: 1em; vertical-align: -0.15em; }
  .spinner::before {
    content: ""; display: block; width: 100%; height: 100%;
    border: 2px solid color-mix(in srgb, var(--muted) 35%, transparent);
    border-top-color: currentColor; border-radius: 50%;
    animation: gs-spin 0.8s linear infinite;
  }
  .loading .spinner { font-size: 1.5em; }
  @keyframes gs-spin { to { transform: rotate(360deg); } }
  @keyframes gs-pulse { 50% { opacity: 0.35; } }
  @media (prefers-reduced-motion: reduce) {
    .spinner::before, html.gs-loading #view::before { animation: gs-pulse 1.2s ease-in-out infinite; }
  }
  /* Page-entry boot, second half (gs-upgrade.js). The boot reveals the chrome as
     soon as THIS sheet governs the page, well before the first view's data has
     been read, so the visitor gets the site's frame for most of the wait instead
     of a blank page. `gs-loading` on <html> is what makes that safe: it holds the
     content slot on the same loading treatment as `.loading` above and hides
     whatever the app has rendered into it, so the slot goes from loading to the
     finished view in one step rather than assembling itself in view. The boot
     removes the class at that transition; the plain shell never sets it. */
  html.gs-loading #view > * { display: none; }
  html.gs-loading #view::before {
    content: ""; display: block; width: 1.5em; height: 1.5em;
    margin: var(--sp-6) auto; color: var(--muted);
    border: 2px solid color-mix(in srgb, var(--muted) 35%, transparent);
    border-top-color: currentColor; border-radius: 50%;
    animation: gs-spin 0.8s linear infinite;
  }
  .err {
    color: var(--danger); border-color: var(--danger);
    padding: var(--pad-panel); background: var(--danger-t1);
  }
  /* Definition rows: the trailer list, and a release's commit rows, which take
     the same treatment on both surfaces (a generated page has no .detail). */
  .detail dl, dl.release-notes { margin: var(--sp-3) 0 0; display: grid; grid-template-columns: max-content 1fr; gap: var(--sp-1) var(--sp-4); }
  .detail dt, .release-notes dt { color: var(--muted); font-size: var(--fs-ui); font-family: var(--mono); letter-spacing: var(--mono-ls); }
  /* A trailer value and a release's commit row are metadata, so they read in their key's voice. */
  .detail dd, .release-notes dd { margin: 0; font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-ui); word-break: break-word; }
  .detail .subject { font-size: var(--fs-h1); }
  /* A code commit's subject is chrome, like its rows in the commits list and the graph. */
  .detail .subject.subject-commit { font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-h4); }
  .detail .card .subject, .card .subject { font-size: var(--fs-md); font-family: var(--mono); letter-spacing: var(--mono-ls); font-weight: 400; }
  .back { display: inline-block; margin-bottom: var(--sp-4); color: var(--muted); font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-ui); }
  /* Detail top bar: the back link on the left, the page's controls on the right,
     on one row so neither disturbs the meta line. */
  .detail-topbar { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap; }
  .detail-topbar .back { margin-bottom: var(--sp-4); }
  .detail-topbar .page-actions, .detail-topbar .view-modes { margin: 0; }
  .share-link {
    flex: 0 0 auto; max-width: 100%; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap;
  }
  .mono { font-family: var(--mono); letter-spacing: var(--mono-ls); }
  /* File browser: breadcrumb, tree listing, blob view. */
  .breadcrumb { font-size: var(--fs-ui); margin-bottom: var(--sp-4); word-break: break-all; }
  .breadcrumb a { color: var(--link); }
  /* Rows use the code-block mono at the blob scale, no inter-row separators
     (the outer .tree-list border stays), so the browser reads like the blob.
     Compact vertical rhythm (trees.software density): ~1.6em line boxes. */
  .tree-row {
    display: flex; align-items: center; gap: var(--sp-3); line-height: 1.6;
    padding: 0.12rem var(--sp-3); font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-code); color: var(--text);
  }
  .tree-row:hover { background: var(--chip); text-decoration: none; }
  .home-toggle::-webkit-details-marker { display: none; }
  .tree-icon { width: 1.1rem; text-align: center; opacity: 0.85; }
  /* File-type icons (vendored @pierre/vscode-icons, monochrome currentColor, so
     they read on both themes via the inherited --muted color). Cloned from
     cached templates; the render layer falls back to the text glyphs when
     icons.js is absent. */
  .tree-icon.gs-icon { width: 1.1rem; height: 1.1rem; opacity: 1; }
  /* Per-filetype icon color: the monochrome Pierre SVGs inherit currentColor, so
     tinting is just a color on the wrapper, switched per theme by the --i-* hue
     tokens. Compound selectors keep specificity above the base .gs-icon --muted.
     Uncolored keys (folders, unknown, symlink, and the monochrome types) carry
     no i-* class and stay --muted. */
  .gs-icon.i-cyan { color: var(--i-cyan); }
  .gs-icon.i-yellow { color: var(--i-yellow); }
  .gs-icon.i-orange { color: var(--i-orange); }
  .gs-icon.i-indigo { color: var(--i-indigo); }
  .gs-icon.i-green { color: var(--i-green); }
  .gs-icon.i-blue { color: var(--i-blue); }
  .gs-icon.i-red { color: var(--i-red); }
  .gs-icon.i-vermilion { color: var(--i-vermilion); }
  /* In-place hierarchical tree (Code view + directory routes): directory rows
     expand children inline. Nesting reads from plain indentation alone (a
     rail-free spacer sized by depth), no vertical guides. The chevron toggles
     expansion; the directory name is a real anchor that navigates. */
  .tree-dir { cursor: pointer; }
  .tree-indent { flex: 0 0 auto; align-self: stretch; }
  .tree-chevron-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 0.85rem; flex: 0 0 auto; color: var(--muted); cursor: pointer;
  }
  .tree-chevron-btn:hover { color: var(--text); }
  .tree-name { color: inherit; }
  .tree-name:hover { text-decoration: none; }
  .tree-chevron { width: 0.85rem; height: 0.85rem; flex: 0 0 auto; opacity: 0.7; transform: rotate(-90deg); transition: transform 0.12s ease; }
  .tree-chevron.open { transform: rotate(0); }
  .tree-chevron-spacer { width: 0.85rem; flex: 0 0 auto; }
  .tree-dir:focus-visible { outline: 1px solid var(--link); outline-offset: -1px; }
  .tree-more { color: var(--muted); font-style: italic; }
  /* In-place file/dir search (Code view): a subtle mono input above the tree,
     matching the visual language. Matches highlight with the mark tint; a
     truncation notice reuses the .notice look. */
  .tree-search-wrap { margin-bottom: var(--sp-3); }
  .tree-search { width: 100%; padding: var(--sp-2) var(--sp-3); }
  .tree-row mark.tree-mark {
    background: var(--warn-t3);
    color: inherit; border-radius: 2px; padding: 0 1px;
  }
  /* Active row tint (current file/dir in the sidebar tree), matching the nav
     active fill — a link-tinted background, no left border. */
  .tree-active, .tree-active:hover { background: var(--link-t2); }
  /* Code-context sidebar file tree: a compact repo tree nested under the Code
     nav item so files stay navigable while the content pane shows a blob.
     Smaller than the content tree to fit the 220px column; long names ellipsize
     with the full name in a title attr. Hidden when empty, when the sidebar is
     collapsed (the nav is hidden then), and on the narrow top-nav layout.
     The tree scrolls in its own box, so a deep expansion cannot push the rows
     below it under the viewport-pinned .nav-footer. */
  .nav-tree-slot { margin: 0.1rem 0 var(--sp-1) var(--sp-2); max-height: 18rem; overflow-x: hidden; overflow-y: auto; }
  .nav-tree-slot:empty { display: none; }
  .nav-tree-list { border: none; background: transparent; }
  .nav-tree-list .tree-row { padding: 0.06rem var(--sp-2); line-height: 1.4; font-size: var(--fs-ui); border-radius: var(--r-ctl); }
  .nav-tree-list .tree-row .mono { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .bc-icon { margin-left: var(--sp-2); width: 0.95rem; height: 0.95rem; }
  .diff-file-icon { width: 0.95rem; height: 0.95rem; }
  .blob-meta { margin-bottom: var(--sp-3); }
  .blob-head { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; margin-bottom: var(--sp-4); }
  .blob-head .breadcrumb { flex: 1 1 auto; margin-bottom: 0; }
  .blob-head .view-modes, .blob-head .blob-meta { margin-bottom: 0; flex: 0 0 auto; }
  .notice {
    color: var(--muted);
    padding: var(--sp-2) var(--sp-3); margin-bottom: var(--sp-3); font-size: var(--fs-ui);
  }
  .notice.notice-warn { color: var(--warn); }
  /* Code leading, three deliberate tiers: fenced blocks, .raw-body and the
     diff surfaces share the diff standard 1.45; the blob viewer reads at 1.5
     (a whole screen of code earns the extra air); .tree-row sits at 1.6 (its
     line box IS the row rhythm). Anything else is a bug, not a fourth tier. */
  .blob {
    overflow: auto;
    font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-code); line-height: 1.5;
  }
  .blob-row { display: flex; }
  .blob-row.hl { background: var(--warn-t1); }
  .blob-row .ln {
    flex: 0 0 auto; min-width: 3rem; text-align: right; padding: 0 var(--sp-3);
    color: var(--muted); user-select: none; border-right: 1px solid var(--line);
  }
  .blob-row .ln:hover { color: var(--text); text-decoration: none; }
  .blob-row .lc { white-space: pre; padding: 0 var(--sp-3); }
  .blob-img { padding: var(--sp-4); text-align: center; }
  .blob-img img { max-width: 100%; height: auto; }
  /* README / Markdown. */
  .markdown { margin-top: var(--sp-4); word-break: break-word; }
  .markdown h1, .markdown h2, .markdown h3, .markdown h4 { margin: var(--sp-4) 0 var(--sp-3); line-height: 1.2; }
  .markdown h1 { font-size: var(--fs-h1); line-height: 1; padding-top: var(--sp-1); border-bottom: 1px solid var(--line); padding-bottom: var(--sp-2); }
  .markdown h2 { font-size: var(--fs-h2); padding-top: var(--sp-4); border-bottom: 1px solid var(--line); padding-bottom: var(--sp-1); }
  /* Air above a heading follows its level: one spacing step less per step down,
     so a reader sees the hierarchy before reading the words. */
  .markdown h3 { padding-top: var(--sp-3); }
  .markdown h4 { padding-top: var(--sp-2); }
  .markdown p { margin: var(--sp-4) 0; }
  .markdown ul, .markdown ol { margin: var(--sp-3) 0 var(--sp-3) var(--sp-5); }
  .markdown li { margin: var(--sp-1) 0; }
  .markdown code {
    font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: 1rem;
    background: var(--code-bg); border: 1px solid var(--line);
    padding: 0 var(--sp-2); border-radius: 4px;
  }
  .markdown pre.codeblock {
    background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-panel);
    /* Tighter than --pad-panel on purpose, and a step less on top, where the
       first line's own leading adds to it: code-only, so cards and the other
       panel surfaces keep the shared pair. */
    padding: var(--sp-2) var(--sp-4) var(--sp-3); overflow: auto; margin: var(--sp-3) 0; line-height: 0.8;
  }
  .markdown pre.codeblock code { background: none; border: none; padding: 0; font-size: var(--fs-code); }
  .markdown img { max-width: 100%; height: auto; }
  .markdown div[align="center"], .markdown center, .markdown [align="center"] { text-align: center; }
  .markdown blockquote {
    margin: var(--sp-3) 0; padding: var(--sp-1) var(--sp-4); color: var(--muted);
    border-left: 3px solid var(--line);
  }
  /* ::: admonitions: a tinted panel per type, its label a small mono line. */
  .markdown .admonition {
    margin: var(--sp-4) 0; padding: var(--sp-2) var(--sp-4) var(--sp-3);
    border-left: 3px solid var(--line); background: var(--panel);
  }
  .markdown .admonition p { margin: var(--sp-2) 0; }
  .markdown .admonition p.admonition-title {
    font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-ui);
    font-weight: 600; color: var(--muted); margin: var(--sp-2) 0 var(--sp-1);
  }
  .markdown .admonition-tip { border-left-color: var(--open); background: var(--open-t1); }
  .markdown .admonition-tip p.admonition-title { color: var(--open); }
  .markdown .admonition-info { border-left-color: var(--i-blue); background: color-mix(in srgb, var(--i-blue) 14%, transparent); }
  .markdown .admonition-info p.admonition-title { color: var(--i-blue); }
  .markdown .admonition-warning, .markdown .admonition-caution { border-left-color: var(--warn); background: var(--warn-t1); }
  .markdown .admonition-warning p.admonition-title, .markdown .admonition-caution p.admonition-title { color: var(--warn); }
  .markdown .admonition-danger { border-left-color: var(--danger); background: var(--danger-t1); }
  .markdown .admonition-danger p.admonition-title { color: var(--danger); }
  .markdown del, .markdown s, .markdown strike { text-decoration: line-through; opacity: 0.75; }
  .markdown kbd {
    font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: 0.78em; padding: 0.05rem var(--sp-2);
    border: 1px solid var(--line); border-radius: 4px; background: var(--chip);
  }
  .markdown table { border-collapse: collapse; margin: var(--sp-4) 0; display: block; overflow: auto; max-width: 100%; }
  .markdown th, .markdown td { border: 1px solid var(--line); padding: var(--sp-2) var(--sp-3); text-align: left; background: var(--chip); }
  .markdown th { background: color-mix(in srgb, var(--text) 6%, var(--chip)); font-weight: 600; }
  .markdown li.task { list-style: none; margin-left: calc(var(--sp-5) * -1); }
  .markdown li.task input { margin-right: var(--sp-2); }
  .markdown ul ul, .markdown ol ol, .markdown ul ol, .markdown ol ul { margin: var(--sp-1) 0 var(--sp-1) var(--sp-5); }
  /* Rendered|Raw toggle for .md blob views, reusing the diff-mode toggle look. */
  .view-modes { display: inline-flex; border: 1px solid var(--line); border-radius: var(--r-ctl); overflow: hidden; margin-bottom: var(--sp-3); }
  .body-modes { margin: var(--sp-3) 0 var(--sp-1); }
  /* Raw commit-message pane: the verbatim message (subject + body + GitMsg
     trailer block) reads as a code block — mono at the code scale, panel
     background, bordered, rounded. Kept pre-wrap (not pre + horizontal scroll):
     the message is mostly prose that wants to wrap, and word-break folds the
     long GitMsg trailer refs rather than forcing the pane to scroll
     sideways (overflow-x stays as a safety valve for anything unbreakable). */
  .raw-body {
    white-space: pre-wrap; word-break: break-word; overflow-x: auto;
    font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-code); line-height: 1.45;
    background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-panel);
    padding: var(--pad-panel);
  }
  .view-toggle { padding: var(--sp-1) var(--sp-3); }
  .view-toggle:hover { background: var(--chip); }
  .view-toggle.active { color: var(--text); background: var(--chip); font-weight: 600; }
  /* Fullscreen affordance: expand button on code/diff/fenced blocks, and the
     full-viewport overlay it opens. */
  .fs-btn { padding: 0.15rem var(--sp-2); line-height: 1; }
  .fs-wrap { position: relative; }
  .fs-wrap > .fs-btn { position: absolute; top: var(--sp-2); right: var(--sp-2); z-index: 2; }
  .fs-overlay { position: fixed; inset: 0; z-index: 50; background: var(--bg); }
  /* Floating close: a semi-transparent circled X sitting at the content block's
     top-right corner, one step in from the viewport edge. The text-mixed
     backing and currentColor glyph read over code on both themes; dimmed while
     idle, full opacity on hover. */
  .fs-close.circle {
    position: fixed; top: var(--sp-1); right: var(--sp-1); z-index: 51;
    width: 2rem; height: 2rem; border-radius: 50%; border: none; padding: 0;
    display: flex; align-items: center; justify-content: center;
    background: color-mix(in srgb, var(--text) 12%, transparent);
    color: var(--text); cursor: pointer; font-size: var(--fs-md); line-height: 1;
    opacity: 0.6; backdrop-filter: blur(2px);
    transition: opacity 0.12s ease;
  }
  .fs-close.circle:hover { opacity: 1; }
  .fs-content { position: absolute; inset: 0; overflow: auto; padding: var(--shell-pad); }
  .fs-content .blob, .fs-content .diff-file-body, .fs-content pre.codeblock { max-width: 100%; }
  .meta-strip { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: center; margin-bottom: var(--sp-3); }
  /* Diff views: changed-file list, unified + split hunks. */
  .diff-section { margin-top: var(--sp-5); }
  .diff-head {
    display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
    padding-bottom: var(--sp-3); margin-bottom: var(--sp-3); border-bottom: 1px solid var(--line);
  }
  .diff-head .subject { font-size: var(--fs-h4); }
  .chip.caveat { background: var(--warn-t2); color: var(--text); }
  /* Diff bulk-control row: the expand/collapse toggle, the unified|split mode
     toggle (one button each, label = the action a click performs), and
     fullscreen cluster at the row's right edge. */
  .diff-controls { margin-left: auto; display: inline-flex; gap: var(--sp-2); align-items: center; flex-wrap: wrap; }
  .diff-file { margin-bottom: var(--sp-3); }
  .diff-file-head {
    display: flex; align-items: center; gap: var(--sp-3); padding: var(--pad-row);
    cursor: pointer; font-size: var(--fs-ui);
  }
  .diff-file-head:hover { background: var(--chip); }
  .diff-status {
    flex: 0 0 auto; width: 1.1rem; text-align: center; font-family: var(--mono); letter-spacing: var(--mono-ls);
    font-size: var(--fs-ui); font-weight: 700; border-radius: 4px; color: #fff;
  }
  .diff-status.s-added { background: var(--open); }
  .diff-status.s-deleted { background: var(--danger); }
  .diff-status.s-modified { background: var(--warn); }
  .diff-path { flex: 1 1 auto; word-break: break-all; font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-code); }
  .diff-counts { flex: 0 0 auto; display: inline-flex; gap: var(--sp-3); font-size: var(--fs-ui); }
  .cnt-add { color: var(--open); }
  .cnt-del { color: var(--danger); }
  .diff-file-body { border-top: 1px solid var(--line); overflow: auto; }
  .diff-hunk-head {
    font-size: var(--fs-ui); color: var(--muted);
    background: var(--link-t1);
    padding: 0.15rem var(--sp-3);
  }
  .diff-line { display: flex; font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-ui); line-height: 1.45; }
  .diff-line.add { background: var(--open-t1); }
  .diff-line.del { background: var(--danger-t1); }
  .dl-num {
    flex: 0 0 auto; min-width: 2.6rem; text-align: right; padding: 0 var(--sp-3);
    color: var(--muted); user-select: none; font-size: var(--fs-dense);
  }
  .dl-sign { flex: 0 0 auto; width: 1rem; text-align: center; user-select: none; color: var(--muted); }
  .dl-text { white-space: pre-wrap; word-break: break-word; padding-right: var(--sp-3); flex: 1 1 auto; }
  /* Split view is one CSS grid (num/code/num/code) so the four columns align
     and each row's two sides stay height-locked when one wraps. .ds-contents
     groups a row's four cells yet lets them be direct grid items. */
  .diff-body.split { display: grid; grid-template-columns: max-content minmax(0, 1fr) max-content minmax(0, 1fr); }
  .ds-contents { display: contents; }
  .ds-full { grid-column: 1 / -1; }
  .ds-num {
    text-align: right; padding: 0 var(--sp-3); color: var(--muted); user-select: none;
    font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-dense); line-height: 1.45;
  }
  .ds-code {
    white-space: pre-wrap; word-break: break-word; padding: 0 var(--sp-3);
    font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-ui); line-height: 1.45;
  }
  .ds-code.ds-left { border-right: 1px solid var(--line); }
  .ds-num.add, .ds-code.add { background: var(--open-t1); }
  .ds-num.del, .ds-code.del { background: var(--danger-t1); }
  .ds-empty { background: color-mix(in srgb, var(--text) 4%, transparent); }
  /* Word-level intra-line marks: stronger tint over the row's base add/del tint. */
  mark.dw, mark.aw { color: inherit; border-radius: 2px; padding: 0 1px; }
  mark.dw { background: var(--danger-t3); }
  mark.aw { background: var(--open-t3); }
  /* Collapsed-context divider between hunks (spans all split columns via grid). */
  .diff-expand {
    display: flex; align-items: center; gap: var(--sp-3); cursor: pointer;
    grid-column: 1 / -1; font-size: var(--fs-ui); color: var(--link);
    background: var(--link-t1); padding: 0.15rem var(--sp-3);
  }
  .diff-expand:hover { background: var(--link-t2); }
  .diff-expand-icon { opacity: 0.8; }
  /* Prism tokens mapped onto the existing palette (subtle; this is a read
     surface). Both themes inherit through the --link/--warn/--text tokens. */
  .token.comment, .token.prolog, .token.doctype, .token.cdata { color: var(--muted); font-style: italic; }
  .token.punctuation, .token.operator { color: color-mix(in srgb, var(--text) 75%, var(--bg)); }
  .token.keyword, .token.tag, .token.selector, .token.atrule, .token.important, .token.builtin { color: var(--link); }
  .token.function, .token.class-name, .token.attr-name, .token.property { color: color-mix(in srgb, var(--link) 78%, var(--text)); }
  .token.string, .token.char, .token.attr-value, .token.regex, .token.url { color: var(--warn); }
  .token.number, .token.boolean, .token.constant, .token.symbol { color: color-mix(in srgb, var(--warn) 65%, var(--text)); }
  .token.deleted { color: var(--danger); }
  .token.inserted { color: var(--open); }
  .token.bold { font-weight: 600; }
  .token.italic { font-style: italic; }
  .token.namespace { opacity: 0.7; }
  /* M3: release assets, filters, threads, embedded context, pm groups, contributors. */
  .selectable { user-select: all; }
  .assets { margin-top: var(--sp-4); }
  /* Section-label voice, family included, stated once for every uppercase
     in-content label; the margins stay contextual on each label's own rule
     (heads take the --sp-4 over --sp-3 rhythm, flex-row labels take none).
     The sidebar's #nav .nav-section keeps its own denser voice. */
  .assets-head, .thread-head, .contrib-head, .version-history-head,
  .review-summary-head, .diff-offscreen-head, .pm-members-head, .pm-subissues-head,
  .pm-rel-label, .board-col-head, .board-groupby-label, .board-lane-head,
  .facet-label, .search-group-head, .config-head, .view-count {
    font-family: var(--mono); font-size: var(--fs-ui); color: var(--muted);
    text-transform: uppercase; letter-spacing: 0.05em;
  }
  /* Page total-count line (branches/tags/releases); hidden while a count that
     fills in asynchronously (releases) is not yet known. */
  .view-count { margin: 0.1rem 0 var(--sp-3); }
  .view-count:empty { display: none; }
  .assets-head, .thread-head, .contrib-head,
  .version-history-head, .review-summary-head, .diff-offscreen-head {
    margin: var(--sp-4) 0 var(--sp-3);
  }
  .asset-list { margin-bottom: var(--sp-3); }
  .asset-row {
    display: flex; align-items: center; gap: var(--sp-3); padding: var(--pad-row);
    font-size: var(--fs-ui); color: var(--text); border-top: 1px solid var(--line); word-break: break-all;
  }
  .asset-list .asset-row:first-child { border-top: none; }
  a.asset-row:hover { background: var(--chip); text-decoration: none; }
  .asset-signed { font-size: var(--fs-ui); margin-top: var(--sp-2); }
  .filter-bar { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-bottom: var(--sp-4); }
  .filter-chip.active { color: var(--text); background: var(--chip); border-color: var(--link); font-weight: 600; }
  /* Load-more continuation control for walk-backed lists (timeline/issues/PRs/
     releases/memos/branch log): a centered pill that walks the next history
     window and appends its resolved items. */
  .load-more-wrap { display: flex; flex-direction: column; align-items: center; gap: var(--sp-3); margin: var(--sp-4) 0 var(--sp-2); }
  .search-tier-note { max-width: 34rem; margin: 0 auto; text-align: center; line-height: 1.5; color: var(--muted); font-size: var(--fs-ui); }
  /* The list continuation keeps a larger hit area than the base pill. */
  .load-more { padding: var(--sp-2) var(--sp-5); }
  .load-more:disabled { cursor: default; opacity: 0.6; }
  .thread { margin-top: var(--sp-5); }
  /* Reply-context ("In reply to") block: the parent chain shown above a reply's
     own post. De-emphasized so the anchor post stays visually primary — smaller
     text, tighter padding, and a slight fade on the context cards. */
  .reply-context { margin-top: 0; margin-bottom: var(--sp-4); font-size: var(--fs-ui); opacity: 0.82; }
  .reply-context .card.comment { padding: var(--sp-3); }
  /* Thread hierarchy: a comment-row lays `depth` rail guides beside the card;
     each rail-guide is a vertical connector line (one per reply level; indent is
     capped in JS at THREAD_MAX_DEPTH). align-items:stretch runs the guides the
     card's full height so they read as continuous connectors, like the TUI. */
  .comment-row { display: flex; align-items: stretch; }
  .thread-rail { display: flex; flex: 0 0 auto; }
  .rail-guide { width: 1.25rem; border-left: 2px solid var(--line); }
  .comment-row > .card { flex: 1 1 auto; min-width: 0; }
  /* Detail meta line: author/time on the left, the small Rendered|Raw toggle
     right-aligned on the same line (the blob-head pattern). */
  .detail-meta { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; margin: 0.1rem 0; }
  /* The global .meta indent exists to clear a card's glyph column;
     no glyph precedes the detail meta line, so the author name aligns flush
     with the content edge here. */
  .detail-meta .meta { flex: 1 1 auto; margin-left: 0; }
  .detail-meta .view-modes { margin: 0; flex: 0 0 auto; }
  /* An item's excerpt of what it answers or quotes, carried by its own commit:
     .reply-quote on a card, .embedded on a detail page and under a cross-repo
     reference. One kind, so one marker; the excerpt is dimmed under a comment
     or quote and full-strength under a repost, whose original is the item
     rather than context for it. */
  .embedded, .reply-quote {
    margin-top: var(--sp-3); padding: var(--sp-2) var(--sp-3); border-left: 5px solid var(--line);
    background: var(--code-bg); border-radius: 0 var(--r-ctl) var(--r-ctl) 0;
  }
  .embedded-from { font-style: italic; }
  /* The block is its own navigation target — it opens the PARENT, not the reply
     that carries it — so it takes the card's clickable treatment and says so on
     hover: the border lights and the ground tints the way a card's does, which
     is what a reader reads as "this goes somewhere else". Without it the
     excerpt looks like part of the reply's own card, and a click reads as a
     mis-navigation. */
  .reply-quote.clickable { cursor: pointer; transition: border-color 0.12s ease, background 0.12s ease; }
  .reply-quote.clickable:hover {
    border-left-color: color-mix(in srgb, var(--link) 55%, var(--line));
    background: color-mix(in srgb, var(--link) 8%, var(--code-bg));
  }
  .reply-quote-head { margin-bottom: var(--sp-1); }
  .reply-quote.dimmed .reply-quote-body { color: var(--muted); }
  .embedded-quote { white-space: pre-wrap; word-break: break-word; color: var(--muted); margin: var(--sp-1) 0; font-size: var(--fs-ui); }
  /* PM detail extras (milestone/sprint members, sub-issues, relations),
     progress bars, the kanban board, and the in-bucket item search. */
  /* Member rows: flex so the leading state chip sits in a fixed-width slot and
     the subjects line up vertically no matter the chip label's width. */
  .pm-member { margin-top: var(--sp-2); font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-ui); padding-left: var(--sp-1); display: flex; align-items: baseline; gap: var(--sp-3); }
  .pm-member > .chip.state { flex: 0 0 auto; min-width: 4.2rem; text-align: center; }
  .pm-members-head, .pm-subissues-head { margin: var(--sp-4) 0 var(--sp-2); }
  .pm-members, .pm-subissues { margin-top: var(--sp-3); }
  /* Progress bar: a slim track with a fill in the closed color + "n closed of m" label. */
  .pm-progress { display: flex; align-items: center; gap: var(--sp-3); margin: var(--sp-2) 0 var(--sp-3); }
  .pm-bar { flex: 1 1 auto; height: 0.5rem; max-width: 16rem; background: var(--chip); border-radius: var(--r-pill); overflow: hidden; }
  .pm-bar-fill { height: 100%; background: var(--closed); border-radius: var(--r-pill); }
  .pm-progress-label { font-size: var(--fs-ui); color: var(--muted); flex: 0 0 auto; }
  /* Issue detail relationship links (parent / milestone / sprint). */
  .pm-rels { margin: var(--sp-3) 0; display: flex; flex-direction: column; gap: var(--sp-2); }
  .pm-rel { display: inline-flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; font-size: var(--fs-ui); }
  .pm-rel-label { min-width: 4.5rem; }
  .chip.pm-sub-chip { background: var(--link-t2); color: var(--text); }
  /* Card enrichment chips (labels/assignees/priority/retracted) on issue/PR list
     cards: a wrapping row below the meta, mirroring the TUI card stats. */
  .card-chips { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-top: var(--sp-2); }
  /* Under a head the chips share the meta row's indent past the glyph column. */
  .card-head ~ .card-chips { margin-left: calc(var(--sp-5) + var(--sp-3)); }
  .chip.chip-assignee { background: var(--link-t2); color: var(--text); }
  .chip.chip-priority { color: var(--text); }
  .chip.chip-priority.prio-critical { background: var(--danger-t3); }
  .chip.chip-priority.prio-high { background: var(--danger-t2); }
  .chip.chip-priority.prio-medium { background: var(--warn-t2); }
  .chip.chip-retracted { background: var(--danger-t2); color: var(--text); }
  /* An automated author's mark: muted, because it tells a reader what to SKIM
     past, and a loud chip on the majority of a mirrored repo's rows would be
     the noise it is meant to label. */
  .chip.chip-bot { color: var(--muted); }
  .chip.chip-due { background: var(--warn-t2); color: var(--text); }
  .chip.chip-review { color: var(--text); }
  .chip.chip-review .review-ok { color: var(--open); font-weight: 600; }
  .chip.chip-review .review-no { color: var(--danger); font-weight: 600; }
  /* Kanban board: horizontally scrolling columns, each a header + issue cards.
     The row scrolls in its own box inside the fixed shell; the scroll box's
     edge fades say that more columns sit past a clipped edge. */
  .board { display: flex; gap: var(--sp-4); overflow-x: auto; padding-bottom: var(--sp-3); align-items: flex-start; }
  /* Columns grow from the 15rem floor, so the space a collapsed or hidden
     column frees goes to the expanded ones instead of staying blank. */
  .board-col { flex: 1 1 15rem; min-width: 15rem; background: var(--chip); border-radius: var(--r-panel); padding: var(--sp-3); }
  /* A collapsed column shows only its slim header, so it stops taking a full lane
     of horizontal space while remaining a visible, re-expandable stub. A column
     with no issues starts collapsed (boardColumnEl), so it keeps its place and
     its count without an empty lane. */
  .board-col.board-col-collapsed { flex: 0 0 auto; min-width: 0; }
  /* Edge fades on the scroll box, the .home-fade cue turned sideways: a quiet
     gradient over a clipped edge, toggled from the scroll position in JS. The
     fades stop above the row's scrollbar gutter. */
  .board-scroll { position: relative; }
  .board-scroll::before, .board-scroll::after {
    content: ""; position: absolute; top: 0; bottom: var(--sp-3); width: var(--sp-6);
    pointer-events: none; opacity: 0; transition: opacity 0.12s ease;
  }
  .board-scroll::before { left: 0; background: linear-gradient(to right, var(--bg), transparent); }
  .board-scroll::after { right: 0; background: linear-gradient(to left, var(--bg), transparent); }
  .board-scroll.board-clip-left::before { opacity: 1; }
  .board-scroll.board-clip-right::after { opacity: 1; }
  .board-col-head { margin-bottom: var(--sp-3); display: flex; align-items: center; gap: var(--sp-2); }
  .board-col-name { flex: 1 1 auto; white-space: nowrap; }
  .board-col-toggle, .board-col-hide { flex: 0 0 auto; line-height: 1; padding: 0 0.1rem; }
  .board-empty { color: var(--muted); text-align: center; padding: var(--sp-3) 0; font-size: var(--fs-ui); }
  /* A board card is chrome, not prose: mono at the UI size, set tighter than
     the serif body so a wrapped title stays one compact block in the lane. */
  .card.board-card { margin-bottom: var(--sp-3); padding: var(--sp-3); font-family: var(--mono); letter-spacing: var(--mono-ls); line-height: 1; }
  .card.board-card .subject { font-size: var(--fs-ui); }
  /* Board card title line: glyph + subject as ONE flex child so a long subject
     wraps its text under itself without the glyph splitting onto its own row in a
     narrow column (G8). The glyph hugs the first line; the subject text flows. */
  .board-card-title { display: inline; }
  .board-card-title .type-glyph { margin-right: 0.3em; }
  .board-more {
    display: inline-block; margin-top: 0.15rem; font-size: var(--fs-dense);
    color: var(--link); padding: 0.1rem var(--sp-1);
  }
  .board-card-labels { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-top: var(--sp-2); }
  .chip.board-wip-over { background: var(--danger-t2); color: var(--text); }
  /* Board group-by (swimlane) control + lane bands. The control is a labeled
     chip row; each lane band carries a header and its own columns×issues grid. */
  .board-controls { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; margin-bottom: var(--sp-3); }
  .board-col-restore { border-style: dashed; }
  .board-groupby-indicator { font-size: var(--fs-ui); color: var(--muted); margin: var(--sp-1) 0 var(--sp-3); }
  /* Lane index: a compact wrap of clickable lane labels that scroll to a lane, so
     long lanes don't bury the ones after them. */
  .board-lane-index { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: 0 0 var(--sp-3); }
  .board-lane { margin-bottom: var(--sp-4); scroll-margin-top: var(--sp-3); }
  .board-lane-head {
    padding: var(--sp-1) 0.1rem; margin-bottom: var(--sp-2); border-bottom: 1px solid var(--line);
    cursor: pointer; display: flex; align-items: center; gap: var(--sp-2);
    position: sticky; top: 0; z-index: 2; background: var(--bg);
  }
  .board-lane-caret { flex: 0 0 auto; font-family: var(--mono); letter-spacing: var(--mono-ls); }
  /* In-bucket item search (#/search): input, grouped results, highlighted marks. */
  .search-input { width: 100%; padding: var(--sp-3) var(--sp-4); margin-bottom: var(--sp-3); }
  .search-status { margin-bottom: var(--sp-3); }
  .search-facets { display: flex; flex-direction: column; gap: var(--sp-2); margin: 0.1rem 0 var(--sp-4); }
  .facet-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2); }
  .search-sort { margin: 0 0 var(--sp-3); }
  /* Freshness pill: fixed and unobtrusive; a click reloads for the new push. */
  .update-pill { position: fixed; right: var(--sp-4); bottom: var(--sp-4); z-index: 50; font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-ui); color: var(--bg); background: var(--link); border: none; border-radius: var(--r-pill); padding: var(--sp-2) var(--sp-4); cursor: pointer; box-shadow: 0 2px 10px color-mix(in srgb, var(--link) 40%, transparent); }
  .facet-label { flex: 0 0 auto; min-width: 4.2rem; }
  .facet-chip { display: inline-flex; align-items: baseline; gap: var(--sp-2); color: var(--text); background: var(--chip); padding: 0.1rem var(--sp-3); }
  .facet-chip:hover { border-color: var(--link); }
  .facet-chip.selected { background: var(--link-t3); border-color: var(--link); }
  .facet-count { color: var(--muted); font-variant-numeric: tabular-nums; }
  .facet-chip.selected .facet-count { color: var(--text); }
  .facet-more { color: var(--link); padding: 0.1rem var(--sp-2); }
  .search-group-head { margin: var(--sp-4) 0 var(--sp-3); }
  .search-snippet { margin-top: var(--sp-2); white-space: normal; }
  .search-help { text-align: left; padding: var(--sp-5) 0; }
  .search-help-scope { margin-top: var(--sp-3); font-size: var(--fs-ui); color: var(--muted); }
  mark.search-mark { background: var(--warn-t3); color: var(--text); border-radius: 2px; padding: 0 1px; }
  .contrib { margin: var(--sp-3) 0 var(--sp-4); }
  .contrib-row {
    display: flex; align-items: center; justify-content: space-between;
    padding: var(--pad-row); font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-ui); border-top: 1px solid var(--line);
  }
  .contrib-list .contrib-row:first-child { border-top: none; }
  .contrib-name { word-break: break-word; }
  .contrib-meta { display: inline-flex; gap: var(--sp-3); align-items: center; }
  .contrib-pct { color: var(--muted); }
  /* Top-authors heading row: label on the left, a live filter input on the same
     line; the list scrolls once it grows past a few rows and reveals more on
     scroll (infinite autoscroll window). */
  .contrib-head {
    display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap;
  }
  .contrib-filter { padding: var(--sp-1) var(--sp-3); min-width: 8rem; max-width: 14rem; }
  .contrib-scroll { max-height: 22rem; overflow-y: auto; }
  /* Item version history picker (detail pages, N>1 versions). A compact list of
     version rows; selecting one repaints the body pane, and a per-row "diff to
     previous" toggle reveals that version's header deltas and message-body
     diff. */
  .version-history { margin: var(--sp-4) 0; }
  .version-node { margin-bottom: var(--sp-2); }
  .version-row {
    display: flex; align-items: center; gap: var(--sp-3); padding: var(--pad-row);
    cursor: pointer; font-size: var(--fs-ui);
  }
  .version-row:hover { background: var(--chip); }
  /* Selection follows the active-state doctrine (#nav): link-tinted fill +
     bolder text, no left-border indicator (which also shifted the row 3px). */
  .version-row.active { background: var(--link-t2); font-weight: 600; }
  .version-row-meta { display: inline-flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; color: var(--muted); flex: 1 1 auto; }
  .chip.version-label { color: var(--text); font-family: var(--mono); letter-spacing: var(--mono-ls); }
  .version-diff-ctl { margin-left: auto; }
  .version-diff-pane:not(:empty) { border-top: 1px solid var(--line); }
  .version-diff { overflow: auto; }
  /* Header deltas lead the diff pane: one "key from → to" row per changed
     GitMsg field, which is where a close/merge lives (the body never moves). */
  .version-header-delta { display: flex; flex-direction: column; gap: var(--sp-2); padding: var(--pad-panel); }
  .version-delta-row { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; font-size: var(--fs-ui); }
  .version-delta-key, .version-delta-arrow, .version-delta-none { color: var(--muted); }
  .version-delta-val { word-break: break-all; }
  /* PR review summary strip: count line + status chip, per-reviewer chips, and
     verdict feedback bodies. */
  .review-summary { margin-top: var(--sp-5); }
  .review-summary-line { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; margin-bottom: var(--sp-2); }
  .review-chips { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-bottom: var(--sp-3); }
  /* verdict-<state> tints a chip and colors a feedback card's left border. */
  .chip.verdict-approved { background: var(--open-t2); color: var(--text); }
  .chip.verdict-changes-requested { background: var(--danger-t2); color: var(--text); }
  .chip.verdict-commented { color: var(--text); }
  /* The signed-tag chip is a positive verdict: the approved tint, not the base chip. */
  .chip.chip-signed { background: var(--open-t2); color: var(--text); }
  /* The diff's feedback containers hold its dense size; the cards inherit it. */
  .diff-feedback { grid-column: 1 / -1; padding: var(--sp-2) var(--sp-3) var(--sp-2) var(--sp-6); background: var(--link-t1); border-top: 1px dashed var(--line); font-size: var(--fs-ui); }
  /* The left border is a kind marker, the verdict, never selection; padding and radius come from .card. */
  .card.feedback { border-left: 3px solid var(--muted); }
  .card.feedback.verdict-approved { border-left-color: var(--open); }
  .card.feedback.verdict-changes-requested { border-left-color: var(--danger); }
  .diff-feedback > .card { margin-bottom: 0; }
  .suggestion { margin-top: var(--sp-2); }
  .suggestion-head { font-size: var(--fs-ui); color: var(--muted); margin-bottom: var(--sp-1); }
  pre.suggestion-code { background: var(--code-bg); border: 1px solid var(--line); border-radius: var(--r-ctl); padding: var(--sp-3); overflow: auto; }
  pre.suggestion-code code { font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-code); }
  .diff-offscreen { border-top: 1px solid var(--line); padding: var(--sp-2) var(--sp-3); font-size: var(--fs-ui); }
  .diff-offscreen > .card { margin-bottom: var(--sp-2); }
  /* Sidebar footer: fixed to the viewport bottom, spanning the sidebar's own
     width (--nav-w) at the sidebar's horizontal position. The shell is centered
     (max-width --shell-max, margin auto) with --shell-pad inset, so the sidebar's
     left edge from the viewport is the centering margin plus the pad; max() clamps
     the margin to 0 when the viewport is narrower than the shell. Opaque bg over
     the scrolling nav; the nav reserves matching bottom padding so links clear it.
     Sits above the content (z-index) but the content column never underlaps it.
     The mono uppercase treatment comes from the shared #nav .nav-section rule;
     the 0.5rem here overrides its --fs-dense so the credit stays tiny. */
  .nav-footer {
    position: fixed; bottom: 0; z-index: 6;
    left: calc(max(0px, (100vw - var(--shell-max)) / 2) + var(--shell-pad));
    width: var(--nav-w);
    padding: var(--sp-3); background: var(--bg);
    border-top: 1px solid var(--line);
    font-size: 0.5rem;
  }
  /* Full-width layout drops the shell max-width, so the sidebar's left edge is just
     the shell pad from the viewport (no centering margin) — track it so the fixed
     footer stays aligned under the sidebar in wide mode too, not shifted right. */
  body.wide .nav-footer { left: var(--shell-pad); }
  /* A collapsed desktop sidebar hides the nav, so its fixed footer must hide too. */
  body.nav-collapsed .nav-footer { display: none; }
  /* Analytics: per-extension item counts (stat grid) and the commits-per-month
     activity bar row (CSS-only, no chart library). */
  .analytics-section { margin: var(--sp-5) 0; }
  .stat-grid { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-top: var(--sp-3); }
  .stat-cell {
    flex: 1 1 6rem; min-width: 6rem; background: var(--panel); border: 1px solid var(--line);
    border-radius: var(--r-panel); padding: var(--sp-3); text-align: center;
  }
  .stat-value { font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-h3); font-weight: 600; }
  .stat-value-text { font-size: var(--fs-ui); font-weight: 600; padding-top: var(--sp-2); }
  .stat-label { font-size: var(--fs-ui); color: var(--muted); margin-top: 0.1rem; }
  .activity-plot { display: flex; align-items: flex-start; gap: var(--sp-2); }
  .activity-plot .activity-chart { flex: 1 1 0; min-width: 0; }
  .activity-yaxis {
    flex: 0 0 auto; height: 192px; min-width: 1.6rem;
    display: flex; flex-direction: column; justify-content: space-between;
    padding: var(--sp-3) 0 var(--sp-5); text-align: right; font-size: var(--fs-dense); color: var(--muted);
  }
  .activity-ytick { line-height: 1; }
  .activity-chart {
    display: flex; align-items: flex-end; gap: 3px; height: 192px;
    border: 1px solid var(--line); border-radius: var(--r-panel); background: var(--panel);
    padding: var(--sp-3) var(--sp-3) var(--sp-2); overflow-x: auto;
  }
  /* Fixed ~12-periods-per-view: columns grow to fill when few, but never shrink
     below 1/12 of the visible width, so a long history stays legible and scrolls
     horizontally (the chart's overflow-x) instead of squishing. 33px = 11 * 3px gap. */
  .activity-col { flex: 1 0 calc((100% - 33px) / 12); min-width: 0; display: flex; flex-direction: column; align-items: center; height: 100%; }
  .activity-bar-wrap { flex: 1 1 auto; width: 100%; display: flex; align-items: flex-end; }
  .activity-bar { width: 100%; min-height: 2px; border-radius: 2px 2px 0 0; background: color-mix(in srgb, var(--link) 55%, transparent); }
  .activity-label { width: 100%; text-align: center; font-size: var(--fs-dense); color: var(--muted); margin-top: var(--sp-1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* Stacked activity chart: a full-height bar per period, split into per-kind
     color segments (bottom-up), with a granularity toggle and color legend. */
  .analytics-chart-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap; }
  .activity-chart.stacked { align-items: flex-end; }
  .activity-stack { width: 100%; min-height: 2px; display: flex; flex-direction: column-reverse; border-radius: 2px 2px 0 0; overflow: hidden; }
  .activity-seg { width: 100%; min-height: 1px; }
  .akind-posts { background: var(--link); }
  .akind-issues { background: var(--open); }
  .akind-prs { background: var(--merged); }
  .akind-releases { background: var(--warn); }
  .akind-memos { background: color-mix(in srgb, var(--link) 45%, var(--warn)); }
  .akind-commits { background: color-mix(in srgb, var(--text) 45%, var(--bg)); }
  .chart-filter { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: var(--sp-3) 0; }
  .chart-filter .filter-chip { display: inline-flex; align-items: center; gap: var(--sp-2); }
  .legend-swatch { width: 0.7rem; height: 0.7rem; border-radius: 2px; display: inline-block; }
  .gran-toggle { display: inline-flex; gap: var(--sp-1); }
  .gran-btn { text-transform: capitalize; }
  .gran-btn.active { color: var(--text); background: var(--chip); border-color: var(--link); }
  /* Author names carry an email tooltip on hover (title attr). */
  .author[title] { cursor: help; }
  /* Configuration page: reader-preference control rows + read-only ext configs. */
  .config-section { margin: var(--sp-5) 0; }
  .config-head { margin-bottom: var(--sp-3); }
  .pref-row {
    display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
    padding: var(--pad-row); border: 1px solid var(--line); border-radius: var(--r-panel);
    background: var(--panel); margin-bottom: var(--sp-2);
  }
  .pref-label { font-size: var(--fs-ui); }
  .pref-btn { color: var(--text); min-width: 6rem; }
  .config-ext { margin-bottom: var(--sp-3); }
  .config-ext-head { font-weight: 600; margin-bottom: var(--sp-2); }
  /* Site config section: an accent color swatch and the favicon preview. */
  .config-swatch { display: inline-flex; align-items: center; gap: var(--sp-2); }
  .config-swatch-dot { width: 0.85rem; height: 0.85rem; border-radius: 3px; border: 1px solid var(--line); }
  .config-favicon { width: 1.1rem; height: 1.1rem; vertical-align: middle; border-radius: 3px; }
  /* Mobile hamburger + slide-over drawer (base = desktop, hidden). The bar and
     scrim only appear at <=720px; the drawer CSS lives in the media block. */
  .mobile-bar { display: none; }
  .nav-scrim { display: none; }
  /* Inline SVG icon (not a ☰ text glyph, whose ink sits high in the em box and
     defeats box-centering): the icon box is the ink, so flex centering is exact. */
  .nav-hamburger {
    background: transparent; border: none; color: var(--text); cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    padding: 0.1rem var(--sp-2); flex: 0 0 auto;
  }
  .nav-hamburger svg { display: block; }
  /* Mobile title links home (same route as the desktop repo-title) and centers on
     the bar's cross axis so it sits level with the hamburger despite the smaller
     font — display:flex + align-items:center pins the text box to the row center. */
  .mobile-title {
    display: flex; align-items: center; min-width: 0;
    font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-ui); line-height: 1; color: var(--muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .mobile-title:hover { color: var(--text); text-decoration: none; }
  @media (max-width: 720px) {
    /* Compact top bar with the hamburger + repo title, sticky at the top. */
    .mobile-bar {
      display: flex; align-items: center; gap: var(--sp-3);
      position: sticky; top: 0; z-index: 7;
      background: var(--bg); border-bottom: 1px solid var(--line);
      padding: var(--sp-3);
    }
    .shell { grid-template-columns: 1fr; gap: var(--sp-3); padding-top: var(--sp-3); }
    /* The nav becomes an off-canvas drawer. display:flex !important overrides the
       desktop body.nav-collapsed hide so a persisted desktop-collapse never traps
       a mobile user; visibility is driven by the transform + body.nav-open. */
    .nav {
      position: fixed; top: 0; left: 0; bottom: 0; z-index: 9;
      width: 82%; max-width: 300px; max-height: none; overflow-y: auto;
      background: var(--bg); border-right: 1px solid var(--line);
      padding: var(--sp-4); transform: translateX(-100%);
      transition: transform 0.2s ease; display: flex !important;
    }
    /* In the drawer the footer is a normal block (position:static) so it moves
       with the drawer's transform (a fixed footer would escape the off-canvas
       transform and stay on screen while the drawer is closed). margin-top:auto
       keeps it at the drawer's bottom; the nav's desktop bottom-padding reservation
       is unneeded here. */
    .nav-footer {
      position: static; left: auto; width: auto; margin-top: auto; margin-bottom: calc(var(--sp-4) * -1);
      padding: var(--sp-3) 0; z-index: auto;
    }
    .nav { padding-bottom: var(--sp-4); }
    /* The drawer's footer is in flow, so the tree scrolls with the drawer. */
    .nav-tree-slot { max-height: none; overflow-y: visible; }
    body.nav-collapsed .nav-footer { display: flex; }
    body.nav-open .nav { transform: translateX(0); }
    body.nav-open .nav-scrim { display: block; position: fixed; inset: 0; z-index: 8; background: color-mix(in srgb, #000 45%, transparent); }
    body.nav-collapsed .shell { grid-template-columns: 1fr; }
    /* The desktop floating handle is never the mobile mechanism. */
    .nav-handle { display: none !important; }
    #nav { flex-direction: column; }
  }

  /* Page-level actions above a list (compare links, "browse files"). */
  .page-actions { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); align-items: baseline; margin: var(--sp-1) 0 var(--sp-4); }
  .action-link { font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-ui); color: var(--link); }
  .compare-link { margin-left: var(--sp-3); font-size: var(--fs-ui); }

  /* Compare page: base/head pickers, then a PR-style commits + files diff. */
  .compare-pickers { display: flex; flex-wrap: wrap; align-items: end; gap: var(--sp-3) var(--sp-4); margin: var(--sp-2) 0 var(--sp-4); }
  .compare-field { display: flex; flex-direction: column; gap: 0.15rem; }
  .compare-select { padding: var(--sp-1) var(--sp-2); background: var(--panel); color: var(--text); border: 1px solid var(--line); border-radius: var(--r-ctl); }
  .compare-dots { align-self: center; padding-bottom: var(--sp-2); }
  .compare-commits { margin: var(--sp-4) 0; }

  /* Commit graph: SVG lane gutter beside a fixed-row-height text column. */
  .graph-scroll { overflow-x: auto; }
  .graph-body { display: flex; align-items: flex-start; gap: var(--sp-3); }
  .graph-gutter-wrap { flex: 0 0 auto; }
  .graph-gutter { display: block; }
  /* The text column keeps a readable floor: a branchy window widens the gutter,
     and the row must overflow into .graph-scroll instead of crushing the text. */
  .graph-text-col { flex: 1 1 auto; min-width: 22rem; }
  .graph-line { display: flex; align-items: center; overflow: hidden; border-bottom: 1px solid color-mix(in srgb, var(--line) 40%, transparent); }
  .graph-row-text { display: flex; align-items: baseline; flex-wrap: nowrap; gap: var(--sp-3); min-width: 0; width: 100%; font-family: var(--mono); letter-spacing: var(--mono-ls); font-size: var(--fs-ui); }
  .graph-subject { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* One fixed-height row must stay one line: a long branch name (dependabot/…)
     gets a single-line ellipsis cap instead of wrapping over neighboring rows,
     and chips shrink to a readable stub before they crush the subject (the
     full name rides the title tooltip; a crowded row ends in a +N chip). */
  .graph-row-text .chip { flex: 0 1 auto; min-width: 3.5rem; max-width: 14rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* Tag chips are exempt from the stub cap: tag names are short version labels
     (and links to their tag page), so they render in full. */
  .graph-row-text .chip.tag-tip { flex: 0 0 auto; min-width: 0; max-width: none; }
  .graph-meta { flex: 0 0 auto; }
  .chip.branch-tip { background: var(--link-t3); color: var(--link); }
  .chip.branch-tip.default { background: var(--link); color: var(--bg); }
  /* Tags get a solid border, no fill: filled chips are reserved for branches.
     Rendered as links to the tag page; no underline, like .chip.merged-branch. */
  .chip.tag-tip { background: transparent; border: 1px solid color-mix(in srgb, var(--open) 60%, transparent); color: var(--open); text-decoration: none; }
  /* A merged-PR head branch is historical (the ref may be deleted): dashed
     outline + dimmed, so it never reads as a live ref. */
  .chip.merged-branch {
    background: transparent; border: 1px dashed color-mix(in srgb, var(--merged) 60%, transparent);
    color: color-mix(in srgb, var(--merged) 85%, var(--text)); text-decoration: none;
  }
